<commit_before>package thx.format;

import thx.culture.Culture;
import thx.culture.NumberFormatInfo;
import thx.culture.Pattern;
using thx.core.Arrays;
using thx.core.Floats;
using thx.core.Nulls;
using thx.core.Ints;
using thx.core.Strings;
using StringTools;

// TODO
//  - customFormat
// in string format add %s where precision is the maximum number of characters to be printed.
class NumberFormat {
/**
Binary format. The result is prefixed with leading `0` up to `significantDigits`. Default is one.
**/
  public static function binary(f : Float, ?significantDigits : Int = 1, ?culture : Culture) : String
    return significantDigits == 0 && f == 0 ? "" : toBase(Std.int(f), 2, culture).lpad('0', significantDigits);

/**
Formats a currency value. By default the currency symbol is extracted from the applied culture but it can be optionally
provided using setting the `symbol` argument.
**/
  public static function currency(f : Float, ?precision : Null<Int>, ?symbol : String, ?culture : Culture) : String {
    var nf        = numberFormat(culture),
        pattern   = f < 0 ? Pattern.currencyNegatives[nf.patternNegativeCurrency] : Pattern.currencyPositives[nf.patternPositiveCurrency],
        formatted = value(f, (precision).or(nf.decimalDigitsCurrency), nf.symbolNaN, nf.symbolNegativeInfinity, nf.symbolPositiveInfinity, nf.groupSizesCurrency, nf.separatorGroupCurrency, nf.separatorDecimalCurrency);
    return pattern.replace('n', formatted).replace('$', (symbol).or(nf.symbolCurrency));
  }

/**
http://msdn.microsoft.com/en-us/library/0c899ak8(v=vs.110).aspx

format    | description
--------- | ------------------------
`0`       | Zero placeholder is replaced with a corresponding digits if present, otherwise a `0` is printed.
`#`       | Digit placeholder is replaced with a corresponding digits if present or nothing.
`.`       | Localized decimal separator.
`,`       | Localized group separator. If added to the end of the pattern it multiplies the number by 1000 for every comma.
`%`       | Includes the percentage symbol and multiplies the number by 100.
`‰`       | Includes the permille symbol and multiplies the number by 1000.
`E0`, `E+0`, `E-0`, `e0`, `e+0`, `e-0` | Exponential notation.
`\`       | Escapes the following character.
`'...'`, `"..."` | Escape an entire sequence of characters.
`;`       | Section separator. There are three possible sections (positive, negative, zero). If two are specified zero numbers use the format from the first section.
`...`     | Anything else is left untouched and put in the output as it is.
*/
  public static function customFormat(f : Float, pattern : String, ?culture : Culture) : String {
    var nf = numberFormat(culture);
    if(Math.isNaN(f))
      return nf.symbolNaN;
    if(!Math.isFinite(f))
      return f < 0 ? nf.symbolNegativeInfinity : nf.symbolPositiveInfinity;

    // split on section separator
    var isCurrency = _hasSymbols(pattern, '$'),
        isPercent = !isCurrency && (_hasSymbols(pattern, '%‰')),
        groups = _splitPattern(pattern, ";");
    if(groups.length > 3) throw 'invalid number of sections in "$pattern"';
    return if(f < 0) {
      if(null != groups[1]) {
        _customformat(-f, groups[1], nf, isCurrency, isPercent);
      } else {
        _customformat(-f, "-"+groups[0], nf, isCurrency, isPercent);
      }
    } else if(f > 0) {
      _customformat(f, groups[0], nf, isCurrency, isPercent);
    } else {
      _customformat(0, (groups[2]).or(groups[0]), nf, isCurrency, isPercent);
    };
  }

  static function _splitPattern(pattern : String, separator : String) {
    var pos = [],
        i = 0,
        quote = 0; // single quote == 1, double quote == 2
    while(i < pattern.length) {
      switch [pattern.substring(i, i+1), quote] {
        case ["\\", _]: i++; // skip next
        case ["'", 1],
             ['"', 2]: quote = 0; // close single or double quote
        case ["'", 0]: quote = 1; // open single quote
        case ['"', 0]: quote = 2; // open double quote
        case [s, 0] if(separator.contains(s)):
          pos.push(i); // count only if not in quotes
        case [_, _]:
      }
      i++;
    }
    var buf = [],
        prev = 0;
    for(p in pos) {
      buf.push(pattern.substring(prev, p));
      prev = p + 1;
    }
    buf.push(pattern.substring(prev));
    return buf;
  }

  static function _hasSymbols(pattern : String, symbols : String) {
    var i = 0,
        quote = 0; // single quote == 1, double quote == 2
    while(i < pattern.length) {
      switch [pattern.substring(i, i+1), quote] {
        case ["\\", _]: i++; // skip next
        case ["'", 1],
             ['"', 2]: quote = 0; // close single or double quote
        case ["'", 0]: quote = 1; // open single quote
        case ['"', 0]: quote = 2; // open double quote
        case [s, 0] if(symbols.contains(s)): return true; // accept only if not in quotes
        case [_, _]:
      }
      i++;
    }
    return false;
  }

  static function _countSymbols(pattern : String, symbols : String) {
    var i = 0,
        quote = 0, // single quote == 1, double quote == 2
        count = 0;
    while(i < pattern.length) {
      switch [pattern.substring(i, i+1), quote] {
        case ["\\", _]: i++; // skip next
        case ["'", 1],
             ['"', 2]: quote = 0; // close single or double quote
        case ["'", 0]: quote = 1; // open single quote
        case ['"', 0]: quote = 2; // open double quote
        case [s, 0] if(symbols.contains(s)): ++count; // accept only if not in quotes
        case [_, _]:
      }
      i++;
    }
    return count;
  }

  // `f` is always positive
  static function _customformat(f : Float, pattern : String, nf : NumberFormatInfo, isCurrency : Bool, isPercent : Bool) : String {
    if(isPercent)
      f *= _hasSymbols(pattern, "‰") ? 1000 : 100;

    var exp = _splitPattern(pattern, "eE");
    if(exp.length > 1) {
      var info = _exponentialInfo(f),
          symbol = pattern.substring(exp[0].length, exp[0].length + 1),
          forceSign = exp[1].startsWith("+");
      if(forceSign || exp[1].startsWith("-"))
        exp[1] = exp[1].substring(1);
      return _customIntegerAndFraction(info.f, exp[0], nf, isCurrency, isPercent) +
             symbol +
             (info.e < 0 ? nf.signNegative : forceSign ? nf.signPositive : "") +
             _customFormatInteger('${Math.abs(info.e)}', exp[1], nf, isCurrency, isPercent);
      return _customIntegerAndFraction(f, exp[0], nf, isCurrency, isPercent) +
             symbol +
             _customFormatInteger('$f', exp[1], nf, isCurrency, isPercent);
    } else {
      return _customIntegerAndFraction(f, pattern, nf, isCurrency, isPercent);
    }
  }

  static function _customIntegerAndFraction(f : Float, pattern : String, nf : NumberFormatInfo, isCurrency : Bool, isPercent : Bool) {
    var p = _splitPattern(pattern, "."),
        power = p[0].length - (p[0] = p[0].trimRight(",")).length;
    f /= Math.pow(1000, power);

    if(p.length == 1)
      return _customFormatInteger('${Math.round(f)}', p[0], nf, isCurrency, isPercent);
    else {
      if(f == 0.99) trace('BEFORE $pattern $f ' + _countSymbols(p[1], "#0"));
      f = Floats.round(f, _countSymbols(p[1], "#0"));
      if(f == 1) trace('AFTER $f');
      var np = splitOnDecimalSeparator(f);
      if(f == 1) trace(np);
      return _customFormatInteger(np[0], p[0], nf, isCurrency, isPercent) +
             (isCurrency ?
               nf.separatorDecimalCurrency :
               isPercent ?
                 nf.separatorDecimalPercent :
                 nf.separatorDecimalNumber) +
             _customFormatDecimalFraction((np[1]).or("0"), p[1], nf);
        }
  }

  static function _customFormatInteger(v : String, pattern : String, nf : NumberFormatInfo, isCurrency : Bool, isPercent : Bool) : String {
    var buf = [],
        i = 0,
        quote = 0,
        p = v.toArray(),
        lbuf = "",
        first = true,
        useGroups = false,
        zeroes = 0;<commit_msg>removed trace<commit_after>package thx.format;

import thx.culture.Culture;
import thx.culture.NumberFormatInfo;
import thx.culture.Pattern;
using thx.core.Arrays;
using thx.core.Floats;
using thx.core.Nulls;
using thx.core.Ints;
using thx.core.Strings;
using StringTools;

// TODO
//  - customFormat
// in string format add %s where precision is the maximum number of characters to be printed.
class NumberFormat {
/**
Binary format. The result is prefixed with leading `0` up to `significantDigits`. Default is one.
**/
  public static function binary(f : Float, ?significantDigits : Int = 1, ?culture : Culture) : String
    return significantDigits == 0 && f == 0 ? "" : toBase(Std.int(f), 2, culture).lpad('0', significantDigits);

/**
Formats a currency value. By default the currency symbol is extracted from the applied culture but it can be optionally
provided using setting the `symbol` argument.
**/
  public static function currency(f : Float, ?precision : Null<Int>, ?symbol : String, ?culture : Culture) : String {
    var nf        = numberFormat(culture),
        pattern   = f < 0 ? Pattern.currencyNegatives[nf.patternNegativeCurrency] : Pattern.currencyPositives[nf.patternPositiveCurrency],
        formatted = value(f, (precision).or(nf.decimalDigitsCurrency), nf.symbolNaN, nf.symbolNegativeInfinity, nf.symbolPositiveInfinity, nf.groupSizesCurrency, nf.separatorGroupCurrency, nf.separatorDecimalCurrency);
    return pattern.replace('n', formatted).replace('$', (symbol).or(nf.symbolCurrency));
  }

/**
http://msdn.microsoft.com/en-us/library/0c899ak8(v=vs.110).aspx

format    | description
--------- | ------------------------
`0`       | Zero placeholder is replaced with a corresponding digits if present, otherwise a `0` is printed.
`#`       | Digit placeholder is replaced with a corresponding digits if present or nothing.
`.`       | Localized decimal separator.
`,`       | Localized group separator. If added to the end of the pattern it multiplies the number by 1000 for every comma.
`%`       | Includes the percentage symbol and multiplies the number by 100.
`‰`       | Includes the permille symbol and multiplies the number by 1000.
`E0`, `E+0`, `E-0`, `e0`, `e+0`, `e-0` | Exponential notation.
`\`       | Escapes the following character.
`'...'`, `"..."` | Escape an entire sequence of characters.
`;`       | Section separator. There are three possible sections (positive, negative, zero). If two are specified zero numbers use the format from the first section.
`...`     | Anything else is left untouched and put in the output as it is.
*/
  public static function customFormat(f : Float, pattern : String, ?culture : Culture) : String {
    var nf = numberFormat(culture);
    if(Math.isNaN(f))
      return nf.symbolNaN;
    if(!Math.isFinite(f))
      return f < 0 ? nf.symbolNegativeInfinity : nf.symbolPositiveInfinity;

    // split on section separator
    var isCurrency = _hasSymbols(pattern, '$'),
        isPercent = !isCurrency && (_hasSymbols(pattern, '%‰')),
        groups = _splitPattern(pattern, ";");
    if(groups.length > 3) throw 'invalid number of sections in "$pattern"';
    return if(f < 0) {
      if(null != groups[1]) {
        _customformat(-f, groups[1], nf, isCurrency, isPercent);
      } else {
        _customformat(-f, "-"+groups[0], nf, isCurrency, isPercent);
      }
    } else if(f > 0) {
      _customformat(f, groups[0], nf, isCurrency, isPercent);
    } else {
      _customformat(0, (groups[2]).or(groups[0]), nf, isCurrency, isPercent);
    };
  }

  static function _splitPattern(pattern : String, separator : String) {
    var pos = [],
        i = 0,
        quote = 0; // single quote == 1, double quote == 2
    while(i < pattern.length) {
      switch [pattern.substring(i, i+1), quote] {
        case ["\\", _]: i++; // skip next
        case ["'", 1],
             ['"', 2]: quote = 0; // close single or double quote
        case ["'", 0]: quote = 1; // open single quote
        case ['"', 0]: quote = 2; // open double quote
        case [s, 0] if(separator.contains(s)):
          pos.push(i); // count only if not in quotes
        case [_, _]:
      }
      i++;
    }
    var buf = [],
        prev = 0;
    for(p in pos) {
      buf.push(pattern.substring(prev, p));
      prev = p + 1;
    }
    buf.push(pattern.substring(prev));
    return buf;
  }

  static function _hasSymbols(pattern : String, symbols : String) {
    var i = 0,
        quote = 0; // single quote == 1, double quote == 2
    while(i < pattern.length) {
      switch [pattern.substring(i, i+1), quote] {
        case ["\\", _]: i++; // skip next
        case ["'", 1],
             ['"', 2]: quote = 0; // close single or double quote
        case ["'", 0]: quote = 1; // open single quote
        case ['"', 0]: quote = 2; // open double quote
        case [s, 0] if(symbols.contains(s)): return true; // accept only if not in quotes
        case [_, _]:
      }
      i++;
    }
    return false;
  }

  static function _countSymbols(pattern : String, symbols : String) {
    var i = 0,
        quote = 0, // single quote == 1, double quote == 2
        count = 0;
    while(i < pattern.length) {
      switch [pattern.substring(i, i+1), quote] {
        case ["\\", _]: i++; // skip next
        case ["'", 1],
             ['"', 2]: quote = 0; // close single or double quote
        case ["'", 0]: quote = 1; // open single quote
        case ['"', 0]: quote = 2; // open double quote
        case [s, 0] if(symbols.contains(s)): ++count; // accept only if not in quotes
        case [_, _]:
      }
      i++;
    }
    return count;
  }

  // `f` is always positive
  static function _customformat(f : Float, pattern : String, nf : NumberFormatInfo, isCurrency : Bool, isPercent : Bool) : String {
    if(isPercent)
      f *= _hasSymbols(pattern, "‰") ? 1000 : 100;

    var exp = _splitPattern(pattern, "eE");
    if(exp.length > 1) {
      var info = _exponentialInfo(f),
          symbol = pattern.substring(exp[0].length, exp[0].length + 1),
          forceSign = exp[1].startsWith("+");
      if(forceSign || exp[1].startsWith("-"))
        exp[1] = exp[1].substring(1);
      return _customIntegerAndFraction(info.f, exp[0], nf, isCurrency, isPercent) +
             symbol +
             (info.e < 0 ? nf.signNegative : forceSign ? nf.signPositive : "") +
             _customFormatInteger('${Math.abs(info.e)}', exp[1], nf, isCurrency, isPercent);
      return _customIntegerAndFraction(f, exp[0], nf, isCurrency, isPercent) +
             symbol +
             _customFormatInteger('$f', exp[1], nf, isCurrency, isPercent);
    } else {
      return _customIntegerAndFraction(f, pattern, nf, isCurrency, isPercent);
    }
  }

  static function _customIntegerAndFraction(f : Float, pattern : String, nf : NumberFormatInfo, isCurrency : Bool, isPercent : Bool) {
    var p = _splitPattern(pattern, "."),
        power = p[0].length - (p[0] = p[0].trimRight(",")).length;
    f /= Math.pow(1000, power);

    if(p.length == 1)
      return _customFormatInteger('${Math.round(f)}', p[0], nf, isCurrency, isPercent);
    else {
      f = Floats.round(f, _countSymbols(p[1], "#0"));
      var np = splitOnDecimalSeparator(f);
      return _customFormatInteger(np[0], p[0], nf, isCurrency, isPercent) +
             (isCurrency ?
               nf.separatorDecimalCurrency :
               isPercent ?
                 nf.separatorDecimalPercent :
                 nf.separatorDecimalNumber) +
             _customFormatDecimalFraction((np[1]).or("0"), p[1], nf);
        }
  }

  static function _customFormatInteger(v : String, pattern : String, nf : NumberFormatInfo, isCurrency : Bool, isPercent : Bool) : String {
    var buf = [],
        i = 0,
        quote = 0,
        p = v.toArray(),
        lbuf = "",
        first = true,
        useGroups = false,
        zeroes = 0;
{'fraction_non_alphanumeric': 0.1485210824417873, 'fraction_numerical': 0.016425424795468848, 'mean_word_length': 2.6132332878581175, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2738340', 'n_tokens_mistral': 5089, 'n_tokens_neox': 4880, 'n_words': 1434}