-
-
Notifications
You must be signed in to change notification settings - Fork 241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Vendor intl
NumberFormat
#1269
Merged
Merged
Vendor intl
NumberFormat
#1269
Changes from 24 commits
Commits
Show all changes
43 commits
Select commit
Hold shift + click to select a range
663a843
replace intl numberformat
denrase 2bab342
add changelog entry
denrase aa1fcc6
remove changelog entry
denrase 02b12e0
update formatting of > 16 digits
denrase 1476882
Merge branch 'v7.0.0' into chore/replace-intl
denrase ab4b869
vendor intl files
denrase 0303d1d
run format
denrase c8a95c5
Merge branch 'v7.0.0' into chore/replace-intl
marandaneto 526ecf8
Merge branch 'v7.0.0' into chore/replace-intl
denrase 5031f2c
remove unused codepath
denrase 030097e
remove unused factories
denrase 2f821b1
remove grouping
denrase 7c7bd6b
remvoce compact_number_format & plural_rules
denrase 4e0c337
remove dynaic number format
denrase d7c7341
remove prefixes & suffixes
denrase 9fa5a3a
remove more unused code
denrase b858d6b
remove unused number format props
denrase 3b8cc9f
rename to sample rate format
denrase 4e7222a
change to internal
denrase 20d5549
test if formatting is same as intl
denrase f29cfa8
Merge branch 'v7.0.0' into chore/replace-intl
denrase f9d9122
implemented pr feedback
denrase e78c8f8
Merge branch 'v7.0.0' into chore/replace-intl
denrase 0d00694
Update dart/test/utils/sample_rate_format_test.dart
marandaneto 9a7ae0a
Merge branch 'main' into chore/replace-intl
marandaneto 229cd30
Merge branch 'main' into chore/replace-intl
marandaneto 8272b57
Merge branch 'main' into chore/replace-intl
denrase 5062f5f
test how intl behaves on different platforms
denrase 377b72e
remove intl again
denrase 2073840
fix analyze issues
denrase f161484
Merge branch 'main' into chore/replace-intl
marandaneto 52fc2d2
Merge branch 'main' into chore/replace-intl
denrase 271e89e
compare with delta
denrase 57aa8b9
compare with numberformat
denrase 25b0488
remove unused test helper
denrase b015bcf
run format
denrase a4574fc
add license
denrase b0d2964
Merge branch 'main' into chore/replace-intl
denrase 818321b
Update dart/pubspec.yaml
denrase 14c65d5
Merge branch 'main' into chore/replace-intl
marandaneto 31e6c91
Merge branch 'main' into chore/replace-intl
marandaneto f51aaca
Merge branch 'main' into chore/replace-intl
marandaneto 510d90d
fix license
marandaneto File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,292 @@ | ||
import 'dart:math'; | ||
|
||
import 'package:meta/meta.dart'; | ||
|
||
/// Implementation details from `intl` package | ||
/// https://pub.dev/packages/intl | ||
@internal | ||
class SampleRateFormat { | ||
int _minimumIntegerDigits; | ||
int _maximumFractionDigits; | ||
int _minimumFractionDigits; | ||
|
||
/// The difference between our zero and '0'. | ||
/// | ||
/// In other words, a constant _localeZero - _zero. Initialized when | ||
/// the locale is set. | ||
final int _zeroOffset; | ||
|
||
/// Caches the symbols | ||
final _NumberSymbols _symbols; | ||
|
||
/// Transient internal state in which to build up the result of the format | ||
/// operation. We can have this be just an instance variable because Dart is | ||
/// single-threaded and unless we do an asynchronous operation in the process | ||
/// of formatting then there will only ever be one number being formatted | ||
/// at a time. In languages with threads we'd need to pass this on the stack. | ||
final StringBuffer _buffer = StringBuffer(); | ||
|
||
factory SampleRateFormat() { | ||
var symbols = _NumberSymbols( | ||
DECIMAL_SEP: '.', | ||
ZERO_DIGIT: '0', | ||
); | ||
var localeZero = symbols.ZERO_DIGIT.codeUnitAt(0); | ||
var zeroOffset = localeZero - '0'.codeUnitAt(0); | ||
|
||
return SampleRateFormat._( | ||
symbols, | ||
zeroOffset, | ||
); | ||
} | ||
|
||
SampleRateFormat._(this._symbols, this._zeroOffset) | ||
: _minimumIntegerDigits = 1, | ||
_maximumFractionDigits = 16, | ||
_minimumFractionDigits = 0; | ||
|
||
/// Format the sample rate | ||
String format(dynamic sampleRate) { | ||
try { | ||
if (_isNaN(sampleRate)) return '0'; | ||
if (_isSmallerZero(sampleRate)) { | ||
sampleRate = 0; | ||
} | ||
if (_isLargerOne(sampleRate)) { | ||
sampleRate = 1; | ||
} | ||
_formatFixed(sampleRate.abs()); | ||
|
||
var result = _buffer.toString(); | ||
_buffer.clear(); | ||
return result; | ||
} catch (_) { | ||
_buffer.clear(); | ||
return '0'; | ||
} | ||
} | ||
|
||
/// Used to test if we have exceeded integer limits. | ||
static final _maxInt = 1 is double ? pow(2, 52) : 1.0e300.floor(); | ||
static final _maxDigits = (log(_maxInt) / log(10)).ceil(); | ||
|
||
bool _isNaN(number) => number is num ? number.isNaN : false; | ||
bool _isSmallerZero(number) => number is num ? number < 0 : false; | ||
bool _isLargerOne(number) => number is num ? number > 1 : false; | ||
|
||
/// Format the basic number portion, including the fractional digits. | ||
void _formatFixed(dynamic number) { | ||
dynamic integerPart; | ||
int fractionPart; | ||
int extraIntegerDigits; | ||
var fractionDigits = _maximumFractionDigits; | ||
var minFractionDigits = _minimumFractionDigits; | ||
|
||
var power = 0; | ||
int digitMultiplier; | ||
|
||
// We have three possible pieces. First, the basic integer part. If this | ||
// is a percent or permille, the additional 2 or 3 digits. Finally the | ||
// fractional part. | ||
// We avoid multiplying the number because it might overflow if we have | ||
// a fixed-size integer type, so we extract each of the three as an | ||
// integer pieces. | ||
integerPart = _floor(number); | ||
var fraction = number - integerPart; | ||
if (fraction.toInt() != 0) { | ||
// If the fractional part leftover is > 1, presumbly the number | ||
// was too big for a fixed-size integer, so leave it as whatever | ||
// it was - the obvious thing is a double. | ||
integerPart = number; | ||
fraction = 0; | ||
} | ||
|
||
power = pow(10, fractionDigits) as int; | ||
digitMultiplier = power; | ||
|
||
// Multiply out to the number of decimal places and the percent, then | ||
// round. For fixed-size integer types this should always be zero, so | ||
// multiplying is OK. | ||
var remainingDigits = _round(fraction * digitMultiplier).toInt(); | ||
|
||
if (remainingDigits >= digitMultiplier) { | ||
// Overflow into the main digits: 0.99 => 1.00 | ||
integerPart++; | ||
remainingDigits -= digitMultiplier; | ||
} else if (_numberOfIntegerDigits(remainingDigits) > | ||
_numberOfIntegerDigits(_floor(fraction * digitMultiplier).toInt())) { | ||
// Fraction has been rounded (0.0996 -> 0.1). | ||
fraction = remainingDigits / digitMultiplier; | ||
} | ||
|
||
// Separate out the extra integer parts from the fraction part. | ||
extraIntegerDigits = remainingDigits ~/ power; | ||
fractionPart = remainingDigits % power; | ||
|
||
var integerDigits = _integerDigits(integerPart, extraIntegerDigits); | ||
var digitLength = integerDigits.length; | ||
var fractionPresent = | ||
fractionDigits > 0 && (minFractionDigits > 0 || fractionPart > 0); | ||
|
||
if (_hasIntegerDigits(integerDigits)) { | ||
// Add the padding digits to the regular digits so that we get grouping. | ||
var padding = '0' * (_minimumIntegerDigits - digitLength); | ||
integerDigits = '$padding$integerDigits'; | ||
digitLength = integerDigits.length; | ||
for (var i = 0; i < digitLength; i++) { | ||
_addDigit(integerDigits.codeUnitAt(i)); | ||
} | ||
} else if (!fractionPresent) { | ||
// If neither fraction nor integer part exists, just print zero. | ||
_addZero(); | ||
} | ||
|
||
_decimalSeparator(fractionPresent); | ||
if (fractionPresent) { | ||
_formatFractionPart((fractionPart + power).toString(), minFractionDigits); | ||
} | ||
} | ||
|
||
/// Helper to get the floor of a number which might not be num. This should | ||
/// only ever be called with an argument which is positive, or whose abs() | ||
/// is negative. The second case is the maximum negative value on a | ||
/// fixed-length integer. Since they are integers, they are also their own | ||
/// floor. | ||
dynamic _floor(dynamic number) { | ||
if (number.isNegative && !number.abs().isNegative) { | ||
throw ArgumentError( | ||
'Internal error: expected positive number, got $number'); | ||
denrase marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
return (number is num) ? number.floor() : number ~/ 1; | ||
} | ||
|
||
/// Helper to round a number which might not be num. | ||
dynamic _round(dynamic number) { | ||
if (number is num) { | ||
if (number.isInfinite) { | ||
return _maxInt; | ||
} else { | ||
return number.round(); | ||
} | ||
} else if (number.remainder(1) == 0) { | ||
// Not a normal number, but int-like, e.g. Int64 | ||
return number; | ||
} else { | ||
// TODO(alanknight): Do this more efficiently. If IntX had floor and | ||
// round we could avoid this. | ||
var basic = _floor(number); | ||
var fraction = (number - basic).toDouble().round(); | ||
return fraction == 0 ? number : number + fraction; | ||
} | ||
} | ||
|
||
// Return the number of digits left of the decimal place in [number]. | ||
static int _numberOfIntegerDigits(dynamic number) { | ||
var simpleNumber = (number.toDouble() as double).abs(); | ||
// It's unfortunate that we have to do this, but we get precision errors | ||
// that affect the result if we use logs, e.g. 1000000 | ||
if (simpleNumber < 10) return 1; | ||
if (simpleNumber < 100) return 2; | ||
if (simpleNumber < 1000) return 3; | ||
if (simpleNumber < 10000) return 4; | ||
if (simpleNumber < 100000) return 5; | ||
if (simpleNumber < 1000000) return 6; | ||
if (simpleNumber < 10000000) return 7; | ||
if (simpleNumber < 100000000) return 8; | ||
if (simpleNumber < 1000000000) return 9; | ||
if (simpleNumber < 10000000000) return 10; | ||
if (simpleNumber < 100000000000) return 11; | ||
if (simpleNumber < 1000000000000) return 12; | ||
if (simpleNumber < 10000000000000) return 13; | ||
if (simpleNumber < 100000000000000) return 14; | ||
if (simpleNumber < 1000000000000000) return 15; | ||
if (simpleNumber < 10000000000000000) return 16; | ||
if (simpleNumber < 100000000000000000) return 17; | ||
if (simpleNumber < 1000000000000000000) return 18; | ||
return 19; | ||
} | ||
|
||
/// Compute the raw integer digits which will then be printed with | ||
/// grouping and translated to localized digits. | ||
String _integerDigits(integerPart, extraIntegerDigits) { | ||
// If the integer part is larger than the maximum integer size | ||
// (2^52 on Javascript, 2^63 on the VM) it will lose precision, | ||
// so pad out the rest of it with zeros. | ||
var paddingDigits = ''; | ||
if (integerPart is num && integerPart > _maxInt) { | ||
var howManyDigitsTooBig = | ||
(log(integerPart) / log(10)).ceil() - _maxDigits; | ||
num divisor = pow(10, howManyDigitsTooBig).round(); | ||
// pow() produces 0 if the result is too large for a 64-bit int. | ||
// If that happens, use a floating point divisor instead. | ||
if (divisor == 0) divisor = pow(10.0, howManyDigitsTooBig); | ||
paddingDigits = '0' * howManyDigitsTooBig.toInt(); | ||
integerPart = (integerPart / divisor).truncate(); | ||
} | ||
|
||
var extra = extraIntegerDigits == 0 ? '' : extraIntegerDigits.toString(); | ||
var intDigits = _mainIntegerDigits(integerPart); | ||
var paddedExtra = intDigits.isEmpty ? extra : extra.padLeft(0, '0'); | ||
return '$intDigits$paddedExtra$paddingDigits'; | ||
} | ||
|
||
/// The digit string of the integer part. This is the empty string if the | ||
/// integer part is zero and otherwise is the toString() of the integer | ||
/// part, stripping off any minus sign. | ||
String _mainIntegerDigits(integer) { | ||
if (integer == 0) return ''; | ||
var digits = integer.toString(); | ||
// If we have a fixed-length int representation, it can have a negative | ||
// number whose negation is also negative, e.g. 2^-63 in 64-bit. | ||
// Remove the minus sign. | ||
return digits.startsWith('-') ? digits.substring(1) : digits; | ||
} | ||
|
||
/// Format the part after the decimal place in a fixed point number. | ||
void _formatFractionPart(String fractionPart, int minDigits) { | ||
var fractionLength = fractionPart.length; | ||
while (fractionPart.codeUnitAt(fractionLength - 1) == '0'.codeUnitAt(0) && | ||
fractionLength > minDigits + 1) { | ||
fractionLength--; | ||
} | ||
for (var i = 1; i < fractionLength; i++) { | ||
_addDigit(fractionPart.codeUnitAt(i)); | ||
} | ||
} | ||
|
||
/// Print the decimal separator if appropriate. | ||
void _decimalSeparator(bool fractionPresent) { | ||
if (fractionPresent) { | ||
_add(_symbols.DECIMAL_SEP); | ||
} | ||
} | ||
|
||
/// Return true if we have a main integer part which is printable, either | ||
/// because we have digits left of the decimal point (this may include digits | ||
/// which have been moved left because of percent or permille formatting), | ||
/// or because the minimum number of printable digits is greater than 1. | ||
bool _hasIntegerDigits(String digits) => | ||
digits.isNotEmpty || _minimumIntegerDigits > 0; | ||
|
||
/// A group of methods that provide support for writing digits and other | ||
/// required characters into [_buffer] easily. | ||
void _add(String x) { | ||
_buffer.write(x); | ||
} | ||
|
||
void _addZero() { | ||
_buffer.write(_symbols.ZERO_DIGIT); | ||
} | ||
|
||
void _addDigit(int x) { | ||
_buffer.writeCharCode(x + _zeroOffset); | ||
} | ||
} | ||
|
||
// Suppress naming issues as changes would be breaking. | ||
// ignore_for_file: non_constant_identifier_names | ||
class _NumberSymbols { | ||
final String DECIMAL_SEP, ZERO_DIGIT; | ||
|
||
const _NumberSymbols({required this.DECIMAL_SEP, required this.ZERO_DIGIT}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import 'package:sentry/src/utils/sample_rate_format.dart'; | ||
import 'package:test/test.dart'; | ||
|
||
void main() { | ||
test('format', () { | ||
final inputsAndOutputs = [ | ||
Tuple(0.0, '0'), | ||
Tuple(1.0, '1'), | ||
Tuple(0.1, '0.1'), | ||
Tuple(0.11, '0.11'), | ||
Tuple(0.19, '0.19'), | ||
Tuple(0.191, '0.191'), | ||
Tuple(0.1919, '0.1919'), | ||
Tuple(0.19191, '0.19191'), | ||
Tuple(0.191919, '0.191919'), | ||
Tuple(0.1919191, '0.1919191'), | ||
Tuple(0.19191919, '0.19191919'), | ||
Tuple(0.191919191, '0.191919191'), | ||
Tuple(0.1919191919, '0.1919191919'), | ||
Tuple(0.19191919191, '0.19191919191'), | ||
Tuple(0.191919191919, '0.191919191919'), | ||
Tuple(0.1919191919191, '0.1919191919191'), | ||
Tuple(0.19191919191919, '0.19191919191919'), | ||
Tuple(0.191919191919191, '0.191919191919191'), | ||
Tuple(0.1919191919191919, '0.1919191919191919'), | ||
Tuple(0.19191919191919199, '0.191919191919192'), | ||
]; | ||
|
||
for (final inputAndOutput in inputsAndOutputs) { | ||
expect(SampleRateFormat().format(inputAndOutput.i), inputAndOutput.o); | ||
} | ||
}); | ||
|
||
test('input smaller 0 is capped', () { | ||
expect(SampleRateFormat().format(-1), '0'); | ||
}); | ||
|
||
test('input larger 1 is capped', () { | ||
expect(SampleRateFormat().format(1.1), '1'); | ||
}); | ||
|
||
test('call with NaN returns 0', () { | ||
expect(SampleRateFormat().format(double.nan), '0'); | ||
}); | ||
} | ||
|
||
class Tuple { | ||
Tuple(this.i, this.o); | ||
final double i; | ||
final String o; | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@chadwhitacre I'd like to double check if this is fine, License https://github.com/dart-lang/intl/blob/master/LICENSE
BSD 3
Should we copy the license text on top of this file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we actually just clarified the policy around this🔒 (in getsentry/team-ospo#120):
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@denrase lets do this then, make CI happy and merge it.