blob: 66c9ce3ae8d64edad1a0896bdf9bf26c6f565bdb [file] [log] [blame]
/*******************************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*******************************************************************************/
package org.apache.ofbiz.base.util;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.Locale;
import com.ibm.icu.text.RuleBasedNumberFormat;
public final class UtilNumber {
private static final String MODULE = UtilNumber.class.getName();
// properties file name for arithmetic configuration
private static final String ARITH_PROP_FILE = "arithmetic.properties";
// default scale and rounding mode for BigDecimals
private static final int DEFAULT_BD_SCALE = 2;
private static final RoundingMode DEFAULT_BD_ROUNDING_MODE = RoundingMode.HALF_UP;
// ICU4J rule sets for the en_US locale. To add more rules, expand this string.
// For reference, see the RbnfSampleRuleSets.java file distributed with ICU4J
private static final String RULE_SET_EN_US =
/*
* These rules format a number in one of the two styles often used
* on checks. %dollars-and-hundredths formats cents as hundredths of
* a dollar (23.40 comes out as "twenty-three and 40/100 dollars").
* %dollars-and-cents formats in dollars and cents (23.40 comes out as
* "twenty-three dollars and forty cents")
*/
"%dollars-and-cents:\n"
+ " x.0: << [and >%%cents>];\n"
+ " 0.x: >%%cents>;\n"
+ " 0: zero dollars; one dollar; =%%main= dollars;\n"
+ "%%main:\n"
+ " zero; one; two; three; four; five; six; seven; eight; nine;\n"
+ " ten; eleven; twelve; thirteen; fourteen; fifteen; sixteen;\n"
+ " seventeen; eighteen; nineteen;\n"
+ " 20: twenty[->>];\n"
+ " 30: thirty[->>];\n"
+ " 40: forty[->>];\n"
+ " 50: fifty[->>];\n"
+ " 60: sixty[->>];\n"
+ " 70: seventy[->>];\n"
+ " 80: eighty[->>];\n"
+ " 90: ninety[->>];\n"
+ " 100: << hundred[ >>];\n"
+ " 1000: << thousand[ >>];\n"
+ " 1,000,000: << million[ >>];\n"
+ " 1,000,000,000: << billion[ >>];\n"
+ " 1,000,000,000,000: << trillion[ >>];\n"
+ " 1,000,000,000,000,000: =#,##0=;\n"
+ "%%cents:\n"
+ " 100: <%%main< cent[s];\n"
+ "%dollars-and-hundredths:\n"
+ " x.0: <%%main< and >%%hundredths>/100;\n" // this used to end in 'dollars' but that should be added later
+ "%%hundredths:\n"
+ " 100: <00<;\n";
// ICU4J rule sets for the th_TH locale. To add more rules, expand this string.
// For reference, see the RbnfSampleRuleSets.java file distributed with ICU4J
private static final String RULE_SET_TH_TH =
/*
* These rules format a number in one of the two styles often used
* on checks. %bahts-and-hundredths formats stangs as hundredths of
* a baht (23.40 comes out as "twenty-three and 40/100 bahts").
* %bahts-and-stangs formats in bahts and stangs (23.40 comes out as
* "twenty-three bahts and forty stangs")
*/
"%bahts-and-stangs:\n"
+ " x.0: << [and >%%stangs>];\n"
+ " 0.x: >%%stangs>;\n"
+ " 0: zero bahts; one baht; =%%main= bahts;\n"
+ "%%main:\n"
+ " zero; one; two; three; four; five; six; seven; eight; nine;\n"
+ " ten; eleven; twelve; thirteen; fourteen; fifteen; sixteen;\n"
+ " seventeen; eighteen; nineteen;\n"
+ " 20: twenty[->>];\n"
+ " 30: thirty[->>];\n"
+ " 40: forty[->>];\n"
+ " 50: fifty[->>];\n"
+ " 60: sixty[->>];\n"
+ " 70: seventy[->>];\n"
+ " 80: eighty[->>];\n"
+ " 90: ninety[->>];\n"
+ " 100: << hundred[ >>];\n"
+ " 1000: << thousand[ >>];\n"
+ " 1,000,000: << million[ >>];\n"
+ " 1,000,000,000: << billion[ >>];\n"
+ " 1,000,000,000,000: << trillion[ >>];\n"
+ " 1,000,000,000,000,000: =#,##0=;\n"
+ "%%stangs:\n"
+ " 100: <%%main< stang[s];\n"
+ "%bahts-and-hundredths:\n"
+ " x.0: <%%main< and >%%hundredths>/100;\n" // this used to end in 'bahts' but that should be added later
+ "%%hundredths:\n"
+ " 100: <00<;\n";
// ICU4J rule sets for the en_IN locale. To add more rules, expand this string.
// For reference, see the RbnfSampleRuleSets.java file distributed with ICU4J
public static final String RULE_SET_EN_IN =
/*
* These rules format a number in one of the two styles often used
* on checks. %simplified formats paise as hundredths of
* a rupees (23.40 comes out as "twenty three rupees and forty paise").
* %default formats in rupees and paise (23.40 comes out as
* "twenty three point four")
*/
"%simplified:\n"
+ " x.0: << [rupees and >%%paise>];\n"
+ " 0.x: >%%paise>;\n"
+ " zero; one; two; three; four; five; six; seven; eight; nine;\n"
+ " ten; eleven; twelve; thirteen; fourteen; fifteen; sixteen;\n"
+ " seventeen; eighteen; nineteen;\n"
+ " 20: twenty[ >>];\n"
+ " 30: thirty[ >>];\n"
+ " 40: forty[ >>];\n"
+ " 50: fifty[ >>];\n"
+ " 60: sixty[ >>];\n"
+ " 70: seventy[ >>];\n"
+ " 80: eighty[ >>];\n"
+ " 90: ninety[ >>];\n"
+ " 100: << hundred[ >%%and>];\n"
+ " 1000: << thousand[ >%%and>];\n"
+ " 1,00,000: << lakh[>%%commas>];\n"
+ " 1,00,00,000: << crore[>%%commas>];\n"
+ " 1,00,00,00,000: =#,##0=;\n"
+ "%default:\n"
+ " -x: minus >>;\n"
+ " x.x: << point >>;\n"
+ " =%simplified=;\n"
+ " 100: << hundred[ >%%and>];\n"
+ " 1000: << thousand[ >%%and>];\n"
+ " 1,00,000: << lakh[>%%commas>];\n"
+ " 1,00,00,000: << crore[>%%commas>];\n"
+ " 10,00,00,000: =#,##0=;\n"
+ "%%paise:\n"
+ " 100: <%simplified< paise;\n"
+ "%%and:\n"
+ " and =%default=;\n"
+ " 100: =%default=;\n"
+ "%%commas:\n"
+ " ' and =%default=;\n"
+ " 100: , =%default=;\n"
+ " 1000: , <%default< thousand, >%default>;\n"
+ " 1,00,000: , =%default=;"
+ "%%lenient-parse:\n"
+ " & ' ' , ',' ;\n";
// hash map to store ICU4J rule sets keyed to Locale
private static HashMap<Locale, String> rbnfRuleSets;
static {
rbnfRuleSets = new HashMap<>();
rbnfRuleSets.put(Locale.US, RULE_SET_EN_US);
rbnfRuleSets.put(new Locale("th"), RULE_SET_TH_TH);
rbnfRuleSets.put(new Locale("en", "IN"), RULE_SET_EN_IN);
}
private UtilNumber() { }
/**
* Method to get BigDecimal scale factor from a property
* @param file - Name of the property file
* @param property - Name of the config property from ARITH_PROP_FILE (e.g., "invoice.decimals")
* @return int - Scale factor to pass to BigDecimal's methods. Defaults to DEFAULT_BD_SCALE (2)
*/
public static int getBigDecimalScale(String file, String property) {
if (UtilValidate.isEmpty(file) || UtilValidate.isEmpty(property)) {
return DEFAULT_BD_SCALE;
}
int scale = -1;
String value = UtilProperties.getPropertyValue(file, property);
try {
scale = Integer.parseInt(value);
} catch (NumberFormatException e) {
Debug.logWarning(e, e.getMessage(), MODULE);
}
if (scale == -1) {
Debug.logWarning("Could not set decimal precision from " + property + "=" + value + ". Using default scale of "
+ DEFAULT_BD_SCALE + ".", MODULE);
scale = DEFAULT_BD_SCALE;
}
return scale;
}
/**
* Method to get BigDecimal scale factor from a property. Use the default ARITH_PROP_FILE properties file
* @param property - Name of the config property from ARITH_PROP_FILE (e.g., "invoice.decimals")
* @return int - Scale factor to pass to BigDecimal's methods. Defaults to DEFAULT_BD_SCALE (2)
*/
public static int getBigDecimalScale(String property) {
return getBigDecimalScale(ARITH_PROP_FILE, property);
}
/**
* Method to get BigDecimal rounding mode from a property
* @param file - Name of the property file
* @param property - Name of the config property from ARITH_PROP_FILE (e.g., "invoice.rounding")
* @return int - Rounding mode to pass to BigDecimal's methods. Defaults to BigDecimal.ROUND_HALF_UP
* @deprecated Use {@link #getRoundingMode(String, String)} instead
*/
@Deprecated
public static int getBigDecimalRoundingMode(String file, String property) {
return getRoundingMode(file, property).ordinal();
}
/**
* Method to get BigDecimal rounding mode from a property. Use the default ARITH_PROP_FILE properties file
* @param property - Name of the config property from ARITH_PROP_FILE (e.g., "invoice.rounding")
* @return int - Rounding mode to pass to BigDecimal's methods. Defaults to BigDecimal.ROUND_HALF_UP
* @deprecated Use {@link #getRoundingMode(String)} instead
*/
@Deprecated
public static int getBigDecimalRoundingMode(String property) {
return getRoundingMode(ARITH_PROP_FILE, property).ordinal();
}
/**
* Method to get BigDecimal rounding mode from a property
* @param file - Name of the property file
* @param property - Name of the config property from ARITH_PROP_FILE (e.g., "invoice.rounding")
* @return RoundingMode - Rounding mode to pass to BigDecimal's methods. Defaults to DEFAULT_BD_ROUNDING_MODE (RoundingMode.HALF_UP)
*/
public static RoundingMode getRoundingMode(String file, String property) {
if (UtilValidate.isEmpty(file) || UtilValidate.isEmpty(property)) {
return DEFAULT_BD_ROUNDING_MODE;
}
String value = UtilProperties.getPropertyValue(file, property);
RoundingMode mode = roundingModeFromString(value);
if (mode == null) {
Debug.logWarning("Could not set decimal rounding mode from " + property + "=" + value + ". Using default mode of "
+ DEFAULT_BD_SCALE + ".", MODULE);
return DEFAULT_BD_ROUNDING_MODE;
}
return mode;
}
/**
* Method to get BigDecimal rounding mode from a property. Use the default ARITH_PROP_FILE properties file
* @param property - Name of the config property from ARITH_PROP_FILE (e.g., "invoice.rounding")
* @return RoundingMode - Rounding mode to pass to BigDecimal's methods. Defaults to DEFAULT_BD_ROUNDING_MODE (RoundingMode.HALF_UP)
*/
public static RoundingMode getRoundingMode(String property) {
return getRoundingMode(ARITH_PROP_FILE, property);
}
/**
* Method to get the RoundingMode rounding mode int value from a string name.
* @param value - The name of the mode (e.g., "ROUND_HALF_UP")
* @return RoundingMode - The rounding mode value of the mode (e.g, RoundingMode.HALF_UP) or null if the input was bad.
*/
public static RoundingMode roundingModeFromString(String value) {
if (value == null) {
return null;
}
value = value.trim();
if ("ROUND_HALF_UP".equals(value)) {
return RoundingMode.HALF_UP;
} else if ("ROUND_HALF_DOWN".equals(value)) {
return RoundingMode.HALF_DOWN;
} else if ("ROUND_HALF_EVEN".equals(value)) {
return RoundingMode.HALF_EVEN;
} else if ("ROUND_UP".equals(value)) {
return RoundingMode.UP;
} else if ("ROUND_DOWN".equals(value)) {
return RoundingMode.DOWN;
} else if ("ROUND_CEILING".equals(value)) {
return RoundingMode.CEILING;
} else if ("ROUND_FLOOR".equals(value)) {
return RoundingMode.FLOOR;
} else if ("ROUND_UNNECCESSARY".equals(value)) {
return RoundingMode.UNNECESSARY;
}
return null;
}
/**
* Method to format an amount using a custom rule set.
* Current rule sets available:
* @param amount - the amount to format
* @param locale - the Locale
* @return formatted string or an empty string if there was an error
*/
public static String formatRuleBasedAmount(double amount, Locale locale) {
String ruleSet = rbnfRuleSets.get(locale);
if (ruleSet == null) {
Debug.logWarning("Cannot format rule based amount for locale " + locale.toString()
+ " because rule set for that locale does not exist", MODULE);
return "";
}
return formatRuleBasedAmount(amount, ruleSet, null, locale);
}
/**
* Method to format an amount using a custom rule set.
* Current rule sets available:
* en_US
* %dollars-and-cents - 1,225.25 becomes "one thousand two hundred twenty five dollars and twenty five cents" (useful for checks)
* %dollars-and-hundreths - 1,225.25 becomes "one thousand two hundred twenty five and 25/00" (alternate for checks)
* @param amount - the amount to format
* @param ruleSet - ruleSet to use
* @param rule - the name of the rule set to use (e.g., %dollars-and-hundredths)
* @param locale - the Locale
* @return formatted string or an empty string if there was an error
*/
public static String formatRuleBasedAmount(double amount, String ruleSet, String rule, Locale locale) {
RuleBasedNumberFormat formatter = new RuleBasedNumberFormat(ruleSet, locale);
String result = "";
try {
result = formatter.format(amount, rule != null ? rule : formatter.getDefaultRuleSetName());
} catch (Exception e) {
Debug.logError(e, "Failed to format amount " + amount + " using rule " + rule, MODULE);
}
return result;
}
/**
* Method to turn a number such as "0.9853" into a nicely formatted percent, "98.53%".
* @param number The number object to format
* @param scale How many places after the decimal to include
* @param roundingMode The BigDecimal rounding mode to apply
* @return The formatted string or "" if there were errors.
* @deprecated Use {@link #toPercentString(Number number, int scale, RoundingMode roundingMode)} instead
*/
@Deprecated
public static String toPercentString(Number number, int scale, int roundingMode) {
// convert to BigDecimal
if (!(number instanceof BigDecimal)) {
number = new BigDecimal(number.doubleValue());
}
// cast it so we can use BigDecimal methods
BigDecimal bd = (BigDecimal) number;
// multiply by 100 and set the scale
bd = bd.multiply(new BigDecimal(100.0)).setScale(scale, roundingMode);
return (bd.toString() + "%");
}
/**
* Method to turn a number such as "0.9853" into a nicely formatted percent, "98.53%".
* @param number The number object to format
* @param scale How many places after the decimal to include
* @param roundingMode the RoundingMode rounding mode to apply
* @return The formatted string or "" if there were errors.
*/
public static String toPercentString(Number number, int scale, RoundingMode roundingMode) {
// convert to BigDecimal
if (!(number instanceof BigDecimal)) {
number = new BigDecimal(number.doubleValue());
}
// cast it so we can use BigDecimal methods
BigDecimal bd = (BigDecimal) number;
// multiply by 100 and set the scale
bd = bd.multiply(new BigDecimal(100.0)).setScale(scale, roundingMode);
return (bd.toString() + "%");
}
/**
* A null-aware method for adding BigDecimal, but only for the right operand.
* @param left The number to add to
* @param right The number being added; if null, then nothing will be added
* @return The result of the addition, or left if right is null.
*/
public static BigDecimal safeAdd(BigDecimal left, BigDecimal right) {
return right != null ? left.add(right) : left;
}
}