Skip to content

Commit df16b96

Browse files
denrasemarandaneto
andauthored
Vendor intl NumberFormat (#1269)
Co-authored-by: Manoel Aranda Neto <[email protected]> Co-authored-by: Manoel Aranda Neto <[email protected]>
1 parent 8a7f528 commit df16b96

File tree

4 files changed

+375
-5
lines changed

4 files changed

+375
-5
lines changed

dart/lib/src/sentry_tracer.dart

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import 'dart:async';
22

3-
import 'package:intl/intl.dart';
43
import 'package:meta/meta.dart';
54

65
import '../sentry.dart';
76
import 'sentry_tracer_finish_status.dart';
7+
import 'utils/sample_rate_format.dart';
88

99
@internal
1010
class SentryTracer extends ISentrySpan {
@@ -349,9 +349,7 @@ class SentryTracer extends ISentrySpan {
349349
if (!isValidSampleRate(sampleRate)) {
350350
return null;
351351
}
352-
// requires intl package
353-
final formatter = NumberFormat('#.################');
354-
return formatter.format(sampleRate);
352+
return sampleRate != null ? SampleRateFormat().format(sampleRate) : null;
355353
}
356354

357355
bool _isHighQualityTransactionName(SentryTransactionNameSource source) {
+323
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
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+
}

dart/pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ dependencies:
1616
meta: ^1.3.0
1717
stack_trace: ^1.10.0
1818
uuid: ^3.0.0
19-
intl: '>=0.17.0 <1.0.0'
2019

2120
dev_dependencies:
2221
mockito: ^5.1.0
@@ -25,3 +24,4 @@ dev_dependencies:
2524
yaml: ^3.1.0 # needed for version match (code and pubspec)
2625
collection: ^1.16.0
2726
coverage: ^1.3.0
27+
intl: '>=0.17.0 <1.0.0'

0 commit comments

Comments
 (0)