All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.squarespace.cldrengine.numbers.NumberInternals Maven / Gradle / Ivy

The newest version!
package com.squarespace.cldrengine.numbers;

import static com.squarespace.cldrengine.utils.StringUtils.isEmpty;
import static com.squarespace.cldrengine.utils.TypeUtils.defaulter;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.squarespace.cldrengine.api.AltType;
import com.squarespace.cldrengine.api.Bundle;
import com.squarespace.cldrengine.api.CurrencyFormatOptions;
import com.squarespace.cldrengine.api.CurrencyFormatStyleType;
import com.squarespace.cldrengine.api.CurrencyFractions;
import com.squarespace.cldrengine.api.CurrencySymbolWidthType;
import com.squarespace.cldrengine.api.CurrencyType;
import com.squarespace.cldrengine.api.Decimal;
import com.squarespace.cldrengine.api.DecimalAdjustOptions;
import com.squarespace.cldrengine.api.DecimalFormatOptions;
import com.squarespace.cldrengine.api.DecimalFormatStyleType;
import com.squarespace.cldrengine.api.MathContext;
import com.squarespace.cldrengine.api.NumberFormatOptions;
import com.squarespace.cldrengine.api.NumberSymbolType;
import com.squarespace.cldrengine.api.Pair;
import com.squarespace.cldrengine.api.Part;
import com.squarespace.cldrengine.api.PluralRules;
import com.squarespace.cldrengine.api.PluralType;
import com.squarespace.cldrengine.api.RoundingModeType;
import com.squarespace.cldrengine.internal.CurrenciesSchema;
import com.squarespace.cldrengine.internal.CurrencyFormats;
import com.squarespace.cldrengine.internal.DecimalFormats;
import com.squarespace.cldrengine.internal.DigitsArrow;
import com.squarespace.cldrengine.internal.FieldArrow;
import com.squarespace.cldrengine.internal.Internals;
import com.squarespace.cldrengine.internal.NumberExternalData;
import com.squarespace.cldrengine.internal.NumberSystemInfo;
import com.squarespace.cldrengine.internal.NumbersSchema;
import com.squarespace.cldrengine.parsing.NumberPattern;
import com.squarespace.cldrengine.parsing.NumberPatternParser;
import com.squarespace.cldrengine.utils.Cache;
import com.squarespace.cldrengine.utils.StringUtils;

public class NumberInternals {

  private static final DecimalAdjustOptions ADJUST_OPTIONS = DecimalAdjustOptions.build()
      .minimumIntegerDigits(0)
      .round(RoundingModeType.HALF_EVEN);

  private static final NumberPattern ADJUST_PATTERN = NumberPatternParser.parse("0")[0];

  private static final Map CURRENCY_FRACTIONS = new HashMap<>();

  private static final Map CURRENCY_REGIONS = new HashMap<>();

  private static final CurrencyFractions DEFAULT_CURRENCY_FRACTIONS =
      new CurrencyFractions(2, 0, 2, 0);

  static {
    String[] parts = NumberExternalData.CURRENCYFRACTIONSRAW.split("\\|");
    for (String part : parts) {
      String[] row = part.split(":");
      CurrencyType code = CurrencyType.fromString(row[0]);
      int[] values = StringUtils.intArray(row[1], 10);
      CurrencyFractions frac = new CurrencyFractions(values[0], values[1], values[2], values[3]);
      CURRENCY_FRACTIONS.put(code, frac);
    }

    parts = NumberExternalData.CURRENCYREGIONSRAW.split("\\|");
    for (String part : parts) {
      String[] row = part.split(":");
      CURRENCY_REGIONS.put(row[0], CurrencyType.fromString(row[1]));
    }
  }

  private final Internals internals;
  private final CurrenciesSchema currencies;
  private final NumbersSchema numbers;
  private final Cache numberPatternCache;

  public NumberInternals(Internals internals) {
    this.internals = internals;
    this.currencies = internals.schema.Currencies;
    this.numbers = internals.schema.Numbers;
    this.numberPatternCache = new Cache<>(NumberPatternParser::parse, 256);
  }

  public Decimal adjustDecimal(Decimal num, DecimalAdjustOptions options) {
    options = defaulter(options, DecimalAdjustOptions::build)
        .mergeIf(ADJUST_OPTIONS);
    NumberContext ctx = new NumberContext(NumberFormatOptions.fromSuper(options), options.round.get(), false, false);
    ctx.setPattern(ADJUST_PATTERN, false);
    return ctx.adjust(num);
  }

  public NumberRenderer stringRenderer(NumberParams params) {
    return new StringNumberFormatter(params);
  }

  public NumberRenderer> partsRenderer(NumberParams params) {
    return new PartsNumberFormatter(params);
  }

  public String getCurrencySymbol(Bundle bundle, CurrencyType code, CurrencySymbolWidthType width) {
    AltType alt = width == CurrencySymbolWidthType.NARROW ? AltType.NARROW : AltType.NONE;
    return this._getCurrencySymbol(bundle, code, alt);
  }

  private String _getCurrencySymbol(Bundle bundle, CurrencyType code, AltType alt) {
   String symbol = this.currencies.symbol.get(bundle, alt, code);
   if (symbol.isEmpty()) {
     symbol = this.currencies.symbol.get(bundle, AltType.NONE, code);
   }
   if (symbol.isEmpty() && this.currencies.symbol.valid(bundle, AltType.NONE, code)) {
     return code.value();
   }
   return symbol;
  }

  public String getCurrencyDisplayName(Bundle bundle, CurrencyType code) {
    return this.currencies.displayName.get(bundle, code);
  }

  public String getCurrencyPluralName(Bundle bundle, CurrencyType code, PluralType plural) {
    String name = this.currencies.pluralName.get(bundle, plural, code);
    return isEmpty(name) ? this.currencies.displayName.get(bundle, code) : name;
  }

  public NumberPattern getNumberPattern(String raw, boolean negative) {
    return numberPatternCache.get(raw)[negative ? 1 : 0];
  }

  public  Pair formatDecimal(Bundle bundle, NumberRenderer renderer, Decimal n,
      DecimalFormatOptions options, NumberParams params) {

    // TODO: abstract away pattern selection defaulting
    DecimalFormatStyleType style = options.style.or(DecimalFormatStyleType.DECIMAL);
    T result;
    PluralType plural = PluralType.OTHER;
    RoundingModeType round = options.round.or(RoundingModeType.HALF_EVEN);

    NumberSystemInfo latnInfo = this.numbers.numberSystem.get("latn");
    NumberSystemInfo info = this.numbers.numberSystem.get(params.numberSystemName.value());
    if (info == null) {
      info = latnInfo;
    }

    DecimalFormats decimalFormats = info.decimalFormats;
    DecimalFormats latnDecimalFormats = latnInfo.decimalFormats;
    String standardRaw = decimalFormats.standard.get(bundle);
    if (isEmpty(standardRaw)) {
      standardRaw = latnDecimalFormats.standard.get(bundle);
    }

    PluralRules plurals = bundle.plurals();

    switch (style) {
      case LONG:
      case SHORT: {
        boolean isShort = style == DecimalFormatStyleType.SHORT;
        boolean useLatn = decimalFormats.short_.get(bundle, PluralType.OTHER, 4)._1.isEmpty();
        DigitsArrow patternImpl = isShort
            ? (useLatn ? latnInfo.decimalFormats.short_ : decimalFormats.short_)
            : (useLatn ? latnInfo.decimalFormats.long_ : decimalFormats.long_);
        NumberContext ctx = new NumberContext(options, round, true, false);

        // Adjust the number using the compact pattern and divisor.
        Pair pair;
        if (options.divisor.ok()) {
          pair = this.setupCompactDivisor(bundle, n, ctx, standardRaw, options.divisor.get(), patternImpl);
        } else {
          pair = this.setupCompact(bundle, n, ctx, standardRaw, patternImpl);
        }
        Decimal q2 = pair._1;
        int ndigits = pair._2;

        q2 = negzero(q2, options.negativeZero.or(false));

        // Compute the plural category for the final q2.
        plural = plurals.cardinal(q2);

        // Select the final pluralized compact pattern based on the integer
        // digits of n and the plural category of the rounded / shifted number q2.
        String raw = patternImpl.get(bundle, plural, ndigits)._1;
        if (isEmpty(raw)) {
     	  raw = patternImpl.get(bundle, PluralType.OTHER, ndigits)._1;
        }
        if (isEmpty(raw)) {
       	  raw = standardRaw;
        }

        // Re-select pattern as number may have changed sign due to rounding.
        NumberPattern pattern = this.getNumberPattern(raw, q2.isNegative());
        result = renderer.render(q2, pattern, "", "", "", ctx.minInt, options.group.get(), null);
        break;
      }

      case PERCENT:
      case PERCENT_SCALED:
      case PERMILLE:
      case PERMILLE_SCALED: {
        // Get percent pattern
        String raw = info.percentFormat.get(bundle);
        if (isEmpty(raw)) {
          raw = latnInfo.percentFormat.get(bundle);
        }
        NumberPattern pattern = this.getNumberPattern(raw, n.isNegative());

        // Scale the number to a percent or permille form as needed.
        if (style == DecimalFormatStyleType.PERCENT) {
          n = n.movePoint(2);
        } else if (style == DecimalFormatStyleType.PERMILLE) {
          n = n.movePoint(3);
        }

        // Select percent or permille symbol.
        String symbol = (style == DecimalFormatStyleType.PERCENT || style == DecimalFormatStyleType.PERCENT_SCALED) ?
            params.symbols.get(NumberSymbolType.PERCENTSIGN) : params.symbols.get(NumberSymbolType.PERMILLE);

        // Adjust number using pattern and options, then render.
        NumberContext ctx = new NumberContext(options, round, false, false, -1);
        ctx.setPattern(pattern, false);
        n = ctx.adjust(n);
        n = negzero(n, options.negativeZero.or(false));
        plural = plurals.cardinal(n);

        // Re-select pattern as number may have changed sign due to rounding
        pattern = this.getNumberPattern(raw, n.isNegative());
        result = renderer.render(n, pattern, "", symbol, "", ctx.minInt, options.group.get(), null);
        break;
      }

      case DECIMAL: {
        // Get decimal pattern
        NumberPattern pattern = this.getNumberPattern(standardRaw, n.isNegative());

        // Adjust number using pattern and options, then render.
        NumberContext ctx = new NumberContext(options, round, false, false, -1);
        ctx.setPattern(pattern, false);
        n = ctx.adjust(n);
        n = negzero(n, options.negativeZero.or(false));
        plural = plurals.cardinal(n);

        // Re-select pattern as number may have changed sign due to rounding.
        pattern = this.getNumberPattern(standardRaw, n.isNegative());
        result = renderer.render(n, pattern, "", "", "", ctx.minInt, options.group.get(), null);
        break;
      }

      case SCIENTIFIC: {
        NumberContext ctx = new NumberContext(options, round, false, true, -1);
        String format = info.scientificFormat.get(bundle);
        if (isEmpty(format)) {
          format = latnInfo.scientificFormat.get(bundle);
        }
        NumberPattern pattern = this.getNumberPattern(format, n.isNegative());

        ctx.setPattern(pattern, true);
        n = ctx.adjust(n, true);
        // output negative zero by default for scientific
        n = negzero(n, options.negativeZero.or(true) != false);

        pattern = this.getNumberPattern(format, n.isNegative());

        // Split number into coefficient and exponent
        Decimal.Scientific sci = n.scientific(ctx.minInt);

        Decimal adjcoeff = ctx.adjust(sci.coefficient, true);
        result = renderer.render(adjcoeff, pattern, "", "", "", 1, false, sci.exponent);
        break;
      }


      default:
        result = renderer.empty();
        break;
    }

    return Pair.of(result, plural);
  }

  public  T formatCurrency(Bundle bundle, NumberRenderer renderer,
      Decimal n, CurrencyType code, CurrencyFormatOptions options, NumberParams params) {

    CurrencyFractions fractions = getCurrencyFractions(code);
    RoundingModeType round = options.round.or(RoundingModeType.HALF_EVEN);

    if (options.cash.or(false) && fractions.cashRounding > 1) {
      Decimal cr = new Decimal(fractions.cashRounding);
      MathContext cx = MathContext.build().round(RoundingModeType.HALF_EVEN);
      // Simple cash rounding to nearest "cash digits" increment
      n = n.divide(cr, cx);
      n = n.setScale(fractions.cashDigits, round);
      n = n.multiply(cr, cx);
    }

    // TODO: display context support
    AltType width = options.symbolWidth.get() == CurrencySymbolWidthType.NARROW
        ? AltType.NARROW : AltType.NONE;
    CurrencyFormatStyleType style = options.style.or(CurrencyFormatStyleType.SYMBOL);

    NumberSystemInfo latnInfo = this.numbers.numberSystem.get("latn");
    NumberSystemInfo info = this.numbers.numberSystem.get(params.numberSystemName.value());
    if (info == null) {
      info = latnInfo;
    }

    CurrencyFormats currencyFormats = info.currencyFormats;
    CurrencyFormats latnDecimalFormats = latnInfo.currencyFormats;

    String standardRaw = currencyFormats.standard.get(bundle);
    if (isEmpty(standardRaw)) {
      standardRaw = latnDecimalFormats.standard.get(bundle);
    }

    // Some locales have a special decimal symbol for certain currencies, e.g. pt-PT and PTE
    String decimal = this.currencies.decimal.get(bundle, code);

    PluralRules plurals = bundle.plurals();

    switch (style) {
      case CODE:
      case NAME: {
        String raw = info.decimalFormats.standard.get(bundle);
        if (isEmpty(raw)) {
          raw = latnInfo.decimalFormats.standard.get(bundle);
        }
        NumberPattern pattern = this.getNumberPattern(raw, n.isNegative());

        // Adjust number using the pattern and options, then render.
        NumberContext ctx = new NumberContext(options, round, false, false, fractions.digits);
        ctx.setPattern(pattern, false);
        n = ctx.adjust(n);
        n = negzero(n, false);

        // Re-select pattern as number may have changed sign due to rounding.
        pattern = this.getNumberPattern(raw, n.isNegative());
        T num = renderer.render(n, pattern, "", "", decimal, ctx.minInt, options.group.get(), null);

        // Compute plural category and select pluralized unit.
        PluralType plural = plurals.cardinal(n);
        String unit = style == CurrencyFormatStyleType.CODE
            ? code.value() : this.getCurrencyPluralName(bundle, code, plural);

        // Wrap number and unit together.
        // TODO: implement a more concise fallback to 'other' for pluralized lookups
        String unitWrapper = currencyFormats.unitPattern.get(bundle, plural);
        if (isEmpty(unitWrapper)) {
          unitWrapper = currencyFormats.unitPattern.get(bundle, PluralType.OTHER);
        }
        if (isEmpty(unitWrapper)) {
          unitWrapper = latnInfo.currencyFormats.unitPattern.get(bundle, plural);
        }
        if (isEmpty(unitWrapper)) {
          unitWrapper = latnInfo.currencyFormats.unitPattern.get(bundle, PluralType.OTHER);
        }
        return renderer.wrap(this.internals.general, unitWrapper, Arrays.asList(num, renderer.make("unit", unit)));
      }

      case SHORT: {
        // The extra complexity here is to deal with rounding up and selecting the
        // correct pluralized pattern for the final rounded form.
        DigitsArrow patternImpl = currencyFormats.short_;

        NumberContext ctx = new NumberContext(options, round, true, false, fractions.digits);
        String symbol = this._getCurrencySymbol(bundle, code, width);

        // Adjust the number using the compact pattern and divisor.
        Pair res;
        if (options.divisor.ok()) {
          res = this.setupCompactDivisor(bundle, n, ctx, standardRaw, options.divisor.get(), patternImpl);
        } else {
          res = this.setupCompact(bundle, n, ctx, standardRaw, patternImpl);
        }
        Decimal q2 = res._1;
        int ndigits = res._2;
        q2 = negzero(q2, false);

        // Compute the plural category for the final q2.
        PluralType plural = plurals.cardinal(q2);

        // Select the final pluralized compact pattern based on the integer
        // digits of n and the plural category of the rounded / shifted number q2.
        String raw = patternImpl.get(bundle, plural, ndigits)._1;
        if (isEmpty(raw)) {
        	raw = patternImpl.get(bundle, PluralType.OTHER, ndigits)._1;
        }
        if (isEmpty(raw) || raw.equals("0")) {
          raw = standardRaw;
        }

        NumberPattern pattern = this.getNumberPattern(raw, q2.isNegative());
        return renderer.render(q2, pattern, symbol, "", decimal, ctx.minInt, options.group.get(), null);
      }

      case ACCOUNTING:
      case SYMBOL: {
        // Select standard or accounting pattern based on style.
        FieldArrow styleArrow = style == CurrencyFormatStyleType.SYMBOL
            ? currencyFormats.standard : currencyFormats.accounting;
        String raw = styleArrow.get(bundle);
        if (isEmpty(raw)) {
          styleArrow = style == CurrencyFormatStyleType.SYMBOL
              ? latnInfo.currencyFormats.standard : latnInfo.currencyFormats.accounting;
          raw = styleArrow.get(bundle);
        }
        NumberPattern pattern = this.getNumberPattern(raw, n.isNegative());

        // Adjust number using pattern and options, then render.
        NumberContext ctx = new NumberContext(options, round, false, false, fractions.digits);
        ctx.setPattern(pattern, false);
        n = ctx.adjust(n);
        n = negzero(n, false);

        // Re-select pattern as number may have changed sign due to rounding.
        pattern = this.getNumberPattern(raw, n.isNegative());
        String symbol = this._getCurrencySymbol(bundle, code, width);
        return renderer.render(n, pattern, symbol, "", decimal, ctx.minInt, options.group.get(), null);
      }
    }

    // NO valid style matched
    return renderer.empty();
  }

  protected CurrencyFractions getCurrencyFractions(CurrencyType code) {
    CurrencyFractions res = CURRENCY_FRACTIONS.get(code);
    return res == null ? DEFAULT_CURRENCY_FRACTIONS : res;
  }

  protected CurrencyType getCurrencyForRegion(String region) {
    return CURRENCY_REGIONS.getOrDefault(region, CurrencyType.USD);
  }

  protected Decimal negzero(Decimal n, boolean show) {
    return !show && n.isZero() && n.isNegative() ? n.abs() : n;
  }

  protected Pair setupCompact(Bundle bundle, Decimal n, NumberContext ctx,
      String standardRaw, DigitsArrow patternImpl) {

    // Select the correct divisor based on the number of integer digits in n.
    boolean negative = n.isNegative();
    int ndigits = n.integerDigits();

    // Select the initial compact pattern based on the integer digits of n.
    // The plural category doesn't matter until the final pattern is selected.
    Pair pair = patternImpl.get(bundle, PluralType.OTHER, ndigits);
    String raw = pair._1;
    int ndivisor = pair._2;
    NumberPattern pattern = this.getCompactPattern(raw, standardRaw, negative);
    int fracDigits = ctx.useSignificant ? -1 : 0;

    // Move the decimal point of n, producing q1. we always strip trailing
    // zeros on compact patterns.
    Decimal q1 = n;
    if (ndivisor > 0) {
      q1 = q1.movePoint(-ndivisor);
    }

    // Adjust q1 using the compact pattern's parameters, to produce q2.
    int q1digits = q1.integerDigits();
    ctx.setCompact(pattern, q1digits, ndivisor, fracDigits);

    Decimal q2 = ctx.adjust(q1);
    int q2digits = q2.integerDigits();
    negative = q2.isNegative();

    // Check if the number rounded up, adding another integer digit.
    if (q2digits > q1digits) {
      // Select a new divisor and pattern.
      ndigits++;

      pair = patternImpl.get(bundle, PluralType.OTHER, ndigits);
      raw = pair._1;
      int divisor = pair._2;
      pattern = this.getCompactPattern(raw, standardRaw, negative);

      // If divisor changed we need to divide and adjust again. We don't divide,
      // we just move the decimal point, since our Decimal type uses a radix that
      // is a power of 10. Otherwise q2 is ready for formatting.
      if (divisor > ndivisor) {
        // We shift right before we move the decimal point. This triggers rounding
        // of the number at its correct scale. Otherwise we would end up with
        // 999,999 becoming 0.999999 and half-even rounding truncating the
        // number to '0M' instead of '1M'.
        q1 = n.movePoint(-divisor);
        q1 = q1.shiftright(divisor, RoundingModeType.HALF_EVEN);
        ctx.setCompact(pattern, q1.integerDigits(), divisor, fracDigits);
        q2 = ctx.adjust(q1);
      }
    }

    return Pair.of(q2, ndigits);
  }

  protected Pair setupCompactDivisor(Bundle bundle, Decimal n, NumberContext ctx,
      String standardRaw, int divisor, DigitsArrow patternImpl) {
    boolean negative = n.isNegative();
    int ndigits = (int)Math.log10(divisor) + 1;

    // Select compact patterns based on number of digits in divisor
    Pair pair = patternImpl.get(bundle, PluralType.OTHER, ndigits);
    String raw = pair._1;
    int ndivisor = pair._2;

    if (ndivisor > 0) {
      n = n.movePoint(-ndivisor);
    }

    NumberPattern pattern = this.getCompactPattern(raw, standardRaw, negative);
    int fracDigits = ctx.useSignificant ? -1 : 0;
    ctx.setCompact(pattern, n.integerDigits(), ndivisor, fracDigits);

    // Hack to avoid extra leading '0' for certain divisor cases.
    // Unless explicit minimum integers is set in options, we force it to
    // 1 to override the compact pattern.
    boolean noMinInt = !ctx.options.minimumIntegerDigits.ok() || ctx.options.minimumIntegerDigits.get() < 0;
    if (noMinInt) {
      ctx.minInt = 1;
    }
    return Pair.of(ctx.adjust(n), ndigits);
  }

  protected NumberPattern getCompactPattern(String raw, String standardRaw, boolean negative) {
    if (!isEmpty(raw)) {
      return this.getNumberPattern(raw, negative);
    }
    // Adjust standard pattern to have same fraction settings as compact
    NumberPattern pattern = new NumberPattern(this.getNumberPattern(standardRaw, negative));
    pattern.minFrac = 0;
    pattern.maxFrac = 0;
    return pattern;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy