|
| 1 | +/// Code ported & adapted from `intl` package |
| 2 | +/// https://pub.dev/packages/intl |
| 3 | +/// |
| 4 | +/// License: |
| 5 | +/// |
| 6 | +/// Copyright 2013, the Dart project authors. |
| 7 | +/// |
| 8 | +/// Redistribution and use in source and binary forms, with or without |
| 9 | +/// modification, are permitted provided that the following conditions are |
| 10 | +/// met: |
| 11 | +/// |
| 12 | +/// * Redistributions of source code must retain the above copyright |
| 13 | +/// notice, this list of conditions and the following disclaimer. |
| 14 | +/// * Redistributions in binary form must reproduce the above |
| 15 | +/// copyright notice, this list of conditions and the following |
| 16 | +/// disclaimer in the documentation and/or other materials provided |
| 17 | +/// with the distribution. |
| 18 | +/// * Neither the name of Google LLC nor the names of its |
| 19 | +/// contributors may be used to endorse or promote products derived |
| 20 | +/// from this software without specific prior written permission. |
| 21 | +/// |
| 22 | +/// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| 23 | +/// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| 24 | +/// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| 25 | +/// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| 26 | +/// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| 27 | +/// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| 28 | +/// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| 29 | +/// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| 30 | +/// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 31 | +/// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 32 | +/// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 33 | +
|
| 34 | +import 'dart:math'; |
| 35 | + |
| 36 | +import 'package:meta/meta.dart'; |
| 37 | + |
| 38 | +@internal |
| 39 | +class SampleRateFormat { |
| 40 | + int _minimumIntegerDigits; |
| 41 | + int _maximumFractionDigits; |
| 42 | + int _minimumFractionDigits; |
| 43 | + |
| 44 | + /// The difference between our zero and '0'. |
| 45 | + /// |
| 46 | + /// In other words, a constant _localeZero - _zero. Initialized when |
| 47 | + /// the locale is set. |
| 48 | + final int _zeroOffset; |
| 49 | + |
| 50 | + /// Caches the symbols |
| 51 | + final _NumberSymbols _symbols; |
| 52 | + |
| 53 | + /// Transient internal state in which to build up the result of the format |
| 54 | + /// operation. We can have this be just an instance variable because Dart is |
| 55 | + /// single-threaded and unless we do an asynchronous operation in the process |
| 56 | + /// of formatting then there will only ever be one number being formatted |
| 57 | + /// at a time. In languages with threads we'd need to pass this on the stack. |
| 58 | + final StringBuffer _buffer = StringBuffer(); |
| 59 | + |
| 60 | + factory SampleRateFormat() { |
| 61 | + var symbols = _NumberSymbols( |
| 62 | + DECIMAL_SEP: '.', |
| 63 | + ZERO_DIGIT: '0', |
| 64 | + ); |
| 65 | + var localeZero = symbols.ZERO_DIGIT.codeUnitAt(0); |
| 66 | + var zeroOffset = localeZero - '0'.codeUnitAt(0); |
| 67 | + |
| 68 | + return SampleRateFormat._( |
| 69 | + symbols, |
| 70 | + zeroOffset, |
| 71 | + ); |
| 72 | + } |
| 73 | + |
| 74 | + SampleRateFormat._(this._symbols, this._zeroOffset) |
| 75 | + : _minimumIntegerDigits = 1, |
| 76 | + _maximumFractionDigits = 16, |
| 77 | + _minimumFractionDigits = 0; |
| 78 | + |
| 79 | + /// Format the sample rate |
| 80 | + String format(dynamic sampleRate) { |
| 81 | + try { |
| 82 | + if (_isNaN(sampleRate)) return '0'; |
| 83 | + if (_isSmallerZero(sampleRate)) { |
| 84 | + sampleRate = 0; |
| 85 | + } |
| 86 | + if (_isLargerOne(sampleRate)) { |
| 87 | + sampleRate = 1; |
| 88 | + } |
| 89 | + _formatFixed(sampleRate.abs()); |
| 90 | + |
| 91 | + var result = _buffer.toString(); |
| 92 | + _buffer.clear(); |
| 93 | + return result; |
| 94 | + } catch (_) { |
| 95 | + _buffer.clear(); |
| 96 | + return '0'; |
| 97 | + } |
| 98 | + } |
| 99 | + |
| 100 | + /// Used to test if we have exceeded integer limits. |
| 101 | + static final _maxInt = 1 is double ? pow(2, 52) : 1.0e300.floor(); |
| 102 | + static final _maxDigits = (log(_maxInt) / log(10)).ceil(); |
| 103 | + |
| 104 | + bool _isNaN(number) => number is num ? number.isNaN : false; |
| 105 | + bool _isSmallerZero(number) => number is num ? number < 0 : false; |
| 106 | + bool _isLargerOne(number) => number is num ? number > 1 : false; |
| 107 | + |
| 108 | + /// Format the basic number portion, including the fractional digits. |
| 109 | + void _formatFixed(dynamic number) { |
| 110 | + dynamic integerPart; |
| 111 | + int fractionPart; |
| 112 | + int extraIntegerDigits; |
| 113 | + var fractionDigits = _maximumFractionDigits; |
| 114 | + var minFractionDigits = _minimumFractionDigits; |
| 115 | + |
| 116 | + var power = 0; |
| 117 | + int digitMultiplier; |
| 118 | + |
| 119 | + // We have three possible pieces. First, the basic integer part. If this |
| 120 | + // is a percent or permille, the additional 2 or 3 digits. Finally the |
| 121 | + // fractional part. |
| 122 | + // We avoid multiplying the number because it might overflow if we have |
| 123 | + // a fixed-size integer type, so we extract each of the three as an |
| 124 | + // integer pieces. |
| 125 | + integerPart = _floor(number); |
| 126 | + var fraction = number - integerPart; |
| 127 | + if (fraction.toInt() != 0) { |
| 128 | + // If the fractional part leftover is > 1, presumbly the number |
| 129 | + // was too big for a fixed-size integer, so leave it as whatever |
| 130 | + // it was - the obvious thing is a double. |
| 131 | + integerPart = number; |
| 132 | + fraction = 0; |
| 133 | + } |
| 134 | + |
| 135 | + power = pow(10, fractionDigits) as int; |
| 136 | + digitMultiplier = power; |
| 137 | + |
| 138 | + // Multiply out to the number of decimal places and the percent, then |
| 139 | + // round. For fixed-size integer types this should always be zero, so |
| 140 | + // multiplying is OK. |
| 141 | + var remainingDigits = _round(fraction * digitMultiplier).toInt(); |
| 142 | + |
| 143 | + if (remainingDigits >= digitMultiplier) { |
| 144 | + // Overflow into the main digits: 0.99 => 1.00 |
| 145 | + integerPart++; |
| 146 | + remainingDigits -= digitMultiplier; |
| 147 | + } else if (_numberOfIntegerDigits(remainingDigits) > |
| 148 | + _numberOfIntegerDigits(_floor(fraction * digitMultiplier).toInt())) { |
| 149 | + // Fraction has been rounded (0.0996 -> 0.1). |
| 150 | + fraction = remainingDigits / digitMultiplier; |
| 151 | + } |
| 152 | + |
| 153 | + // Separate out the extra integer parts from the fraction part. |
| 154 | + extraIntegerDigits = remainingDigits ~/ power; |
| 155 | + fractionPart = remainingDigits % power; |
| 156 | + |
| 157 | + var integerDigits = _integerDigits(integerPart, extraIntegerDigits); |
| 158 | + var digitLength = integerDigits.length; |
| 159 | + var fractionPresent = |
| 160 | + fractionDigits > 0 && (minFractionDigits > 0 || fractionPart > 0); |
| 161 | + |
| 162 | + if (_hasIntegerDigits(integerDigits)) { |
| 163 | + // Add the padding digits to the regular digits so that we get grouping. |
| 164 | + var padding = '0' * (_minimumIntegerDigits - digitLength); |
| 165 | + integerDigits = '$padding$integerDigits'; |
| 166 | + digitLength = integerDigits.length; |
| 167 | + for (var i = 0; i < digitLength; i++) { |
| 168 | + _addDigit(integerDigits.codeUnitAt(i)); |
| 169 | + } |
| 170 | + } else if (!fractionPresent) { |
| 171 | + // If neither fraction nor integer part exists, just print zero. |
| 172 | + _addZero(); |
| 173 | + } |
| 174 | + |
| 175 | + _decimalSeparator(fractionPresent); |
| 176 | + if (fractionPresent) { |
| 177 | + _formatFractionPart((fractionPart + power).toString(), minFractionDigits); |
| 178 | + } |
| 179 | + } |
| 180 | + |
| 181 | + /// Helper to get the floor of a number which might not be num. This should |
| 182 | + /// only ever be called with an argument which is positive, or whose abs() |
| 183 | + /// is negative. The second case is the maximum negative value on a |
| 184 | + /// fixed-length integer. Since they are integers, they are also their own |
| 185 | + /// floor. |
| 186 | + dynamic _floor(dynamic number) { |
| 187 | + if (number.isNegative && !number.abs().isNegative) { |
| 188 | + throw ArgumentError( |
| 189 | + 'Internal error: expected positive number, got $number'); |
| 190 | + } |
| 191 | + return (number is num) ? number.floor() : number ~/ 1; |
| 192 | + } |
| 193 | + |
| 194 | + /// Helper to round a number which might not be num. |
| 195 | + dynamic _round(dynamic number) { |
| 196 | + if (number is num) { |
| 197 | + if (number.isInfinite) { |
| 198 | + return _maxInt; |
| 199 | + } else { |
| 200 | + return number.round(); |
| 201 | + } |
| 202 | + } else if (number.remainder(1) == 0) { |
| 203 | + // Not a normal number, but int-like, e.g. Int64 |
| 204 | + return number; |
| 205 | + } else { |
| 206 | + // TODO(alanknight): Do this more efficiently. If IntX had floor and |
| 207 | + // round we could avoid this. |
| 208 | + var basic = _floor(number); |
| 209 | + var fraction = (number - basic).toDouble().round(); |
| 210 | + return fraction == 0 ? number : number + fraction; |
| 211 | + } |
| 212 | + } |
| 213 | + |
| 214 | + // Return the number of digits left of the decimal place in [number]. |
| 215 | + static int _numberOfIntegerDigits(dynamic number) { |
| 216 | + var simpleNumber = (number.toDouble() as double).abs(); |
| 217 | + // It's unfortunate that we have to do this, but we get precision errors |
| 218 | + // that affect the result if we use logs, e.g. 1000000 |
| 219 | + if (simpleNumber < 10) return 1; |
| 220 | + if (simpleNumber < 100) return 2; |
| 221 | + if (simpleNumber < 1000) return 3; |
| 222 | + if (simpleNumber < 10000) return 4; |
| 223 | + if (simpleNumber < 100000) return 5; |
| 224 | + if (simpleNumber < 1000000) return 6; |
| 225 | + if (simpleNumber < 10000000) return 7; |
| 226 | + if (simpleNumber < 100000000) return 8; |
| 227 | + if (simpleNumber < 1000000000) return 9; |
| 228 | + if (simpleNumber < 10000000000) return 10; |
| 229 | + if (simpleNumber < 100000000000) return 11; |
| 230 | + if (simpleNumber < 1000000000000) return 12; |
| 231 | + if (simpleNumber < 10000000000000) return 13; |
| 232 | + if (simpleNumber < 100000000000000) return 14; |
| 233 | + if (simpleNumber < 1000000000000000) return 15; |
| 234 | + if (simpleNumber < 10000000000000000) return 16; |
| 235 | + if (simpleNumber < 100000000000000000) return 17; |
| 236 | + if (simpleNumber < 1000000000000000000) return 18; |
| 237 | + return 19; |
| 238 | + } |
| 239 | + |
| 240 | + /// Compute the raw integer digits which will then be printed with |
| 241 | + /// grouping and translated to localized digits. |
| 242 | + String _integerDigits(integerPart, extraIntegerDigits) { |
| 243 | + // If the integer part is larger than the maximum integer size |
| 244 | + // (2^52 on Javascript, 2^63 on the VM) it will lose precision, |
| 245 | + // so pad out the rest of it with zeros. |
| 246 | + var paddingDigits = ''; |
| 247 | + if (integerPart is num && integerPart > _maxInt) { |
| 248 | + var howManyDigitsTooBig = |
| 249 | + (log(integerPart) / log(10)).ceil() - _maxDigits; |
| 250 | + num divisor = pow(10, howManyDigitsTooBig).round(); |
| 251 | + // pow() produces 0 if the result is too large for a 64-bit int. |
| 252 | + // If that happens, use a floating point divisor instead. |
| 253 | + if (divisor == 0) divisor = pow(10.0, howManyDigitsTooBig); |
| 254 | + paddingDigits = '0' * howManyDigitsTooBig.toInt(); |
| 255 | + integerPart = (integerPart / divisor).truncate(); |
| 256 | + } |
| 257 | + |
| 258 | + var extra = extraIntegerDigits == 0 ? '' : extraIntegerDigits.toString(); |
| 259 | + var intDigits = _mainIntegerDigits(integerPart); |
| 260 | + var paddedExtra = intDigits.isEmpty ? extra : extra.padLeft(0, '0'); |
| 261 | + return '$intDigits$paddedExtra$paddingDigits'; |
| 262 | + } |
| 263 | + |
| 264 | + /// The digit string of the integer part. This is the empty string if the |
| 265 | + /// integer part is zero and otherwise is the toString() of the integer |
| 266 | + /// part, stripping off any minus sign. |
| 267 | + String _mainIntegerDigits(integer) { |
| 268 | + if (integer == 0) return ''; |
| 269 | + var digits = integer.toString(); |
| 270 | + // If we have a fixed-length int representation, it can have a negative |
| 271 | + // number whose negation is also negative, e.g. 2^-63 in 64-bit. |
| 272 | + // Remove the minus sign. |
| 273 | + return digits.startsWith('-') ? digits.substring(1) : digits; |
| 274 | + } |
| 275 | + |
| 276 | + /// Format the part after the decimal place in a fixed point number. |
| 277 | + void _formatFractionPart(String fractionPart, int minDigits) { |
| 278 | + var fractionLength = fractionPart.length; |
| 279 | + while (fractionPart.codeUnitAt(fractionLength - 1) == '0'.codeUnitAt(0) && |
| 280 | + fractionLength > minDigits + 1) { |
| 281 | + fractionLength--; |
| 282 | + } |
| 283 | + for (var i = 1; i < fractionLength; i++) { |
| 284 | + _addDigit(fractionPart.codeUnitAt(i)); |
| 285 | + } |
| 286 | + } |
| 287 | + |
| 288 | + /// Print the decimal separator if appropriate. |
| 289 | + void _decimalSeparator(bool fractionPresent) { |
| 290 | + if (fractionPresent) { |
| 291 | + _add(_symbols.DECIMAL_SEP); |
| 292 | + } |
| 293 | + } |
| 294 | + |
| 295 | + /// Return true if we have a main integer part which is printable, either |
| 296 | + /// because we have digits left of the decimal point (this may include digits |
| 297 | + /// which have been moved left because of percent or permille formatting), |
| 298 | + /// or because the minimum number of printable digits is greater than 1. |
| 299 | + bool _hasIntegerDigits(String digits) => |
| 300 | + digits.isNotEmpty || _minimumIntegerDigits > 0; |
| 301 | + |
| 302 | + /// A group of methods that provide support for writing digits and other |
| 303 | + /// required characters into [_buffer] easily. |
| 304 | + void _add(String x) { |
| 305 | + _buffer.write(x); |
| 306 | + } |
| 307 | + |
| 308 | + void _addZero() { |
| 309 | + _buffer.write(_symbols.ZERO_DIGIT); |
| 310 | + } |
| 311 | + |
| 312 | + void _addDigit(int x) { |
| 313 | + _buffer.writeCharCode(x + _zeroOffset); |
| 314 | + } |
| 315 | +} |
| 316 | + |
| 317 | +// Suppress naming issues as changes would be breaking. |
| 318 | +// ignore_for_file: non_constant_identifier_names |
| 319 | +class _NumberSymbols { |
| 320 | + final String DECIMAL_SEP, ZERO_DIGIT; |
| 321 | + |
| 322 | + const _NumberSymbols({required this.DECIMAL_SEP, required this.ZERO_DIGIT}); |
| 323 | +} |
0 commit comments