@js-draw/math 1.10.0 → 1.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. package/dist/cjs/lib.d.ts +1 -1
  2. package/dist/cjs/lib.js +16 -3
  3. package/dist/cjs/rounding/cleanUpNumber.d.ts +3 -0
  4. package/dist/cjs/rounding/cleanUpNumber.js +35 -0
  5. package/dist/cjs/rounding/constants.d.ts +1 -0
  6. package/dist/cjs/rounding/constants.js +4 -0
  7. package/dist/cjs/rounding/getLenAfterDecimal.d.ts +10 -0
  8. package/dist/cjs/rounding/getLenAfterDecimal.js +30 -0
  9. package/dist/cjs/rounding/lib.d.ts +1 -0
  10. package/dist/cjs/rounding/lib.js +5 -0
  11. package/dist/cjs/{rounding.d.ts → rounding/toRoundedString.d.ts} +1 -3
  12. package/dist/cjs/rounding/toRoundedString.js +54 -0
  13. package/dist/cjs/rounding/toStringOfSamePrecision.d.ts +2 -0
  14. package/dist/cjs/rounding/toStringOfSamePrecision.js +58 -0
  15. package/dist/cjs/rounding/toStringOfSamePrecision.test.d.ts +1 -0
  16. package/dist/cjs/shapes/Path.js +8 -7
  17. package/dist/cjs/shapes/Triangle.js +2 -2
  18. package/dist/mjs/lib.d.ts +1 -1
  19. package/dist/mjs/lib.mjs +1 -1
  20. package/dist/mjs/rounding/cleanUpNumber.d.ts +3 -0
  21. package/dist/mjs/rounding/cleanUpNumber.mjs +31 -0
  22. package/dist/mjs/rounding/cleanUpNumber.test.d.ts +1 -0
  23. package/dist/mjs/rounding/constants.d.ts +1 -0
  24. package/dist/mjs/rounding/constants.mjs +1 -0
  25. package/dist/mjs/rounding/getLenAfterDecimal.d.ts +10 -0
  26. package/dist/mjs/rounding/getLenAfterDecimal.mjs +26 -0
  27. package/dist/mjs/rounding/lib.d.ts +1 -0
  28. package/dist/mjs/rounding/lib.mjs +1 -0
  29. package/dist/mjs/{rounding.d.ts → rounding/toRoundedString.d.ts} +1 -3
  30. package/dist/mjs/rounding/toRoundedString.mjs +47 -0
  31. package/dist/mjs/rounding/toRoundedString.test.d.ts +1 -0
  32. package/dist/mjs/rounding/toStringOfSamePrecision.d.ts +2 -0
  33. package/dist/mjs/rounding/toStringOfSamePrecision.mjs +51 -0
  34. package/dist/mjs/rounding/toStringOfSamePrecision.test.d.ts +1 -0
  35. package/dist/mjs/shapes/Path.mjs +2 -1
  36. package/dist/mjs/shapes/Triangle.mjs +2 -2
  37. package/package.json +3 -3
  38. package/src/lib.ts +1 -1
  39. package/src/rounding/cleanUpNumber.test.ts +15 -0
  40. package/src/rounding/cleanUpNumber.ts +38 -0
  41. package/src/rounding/constants.ts +3 -0
  42. package/src/rounding/getLenAfterDecimal.ts +29 -0
  43. package/src/rounding/lib.ts +2 -0
  44. package/src/rounding/toRoundedString.test.ts +32 -0
  45. package/src/rounding/toRoundedString.ts +57 -0
  46. package/src/rounding/toStringOfSamePrecision.test.ts +21 -0
  47. package/src/rounding/toStringOfSamePrecision.ts +63 -0
  48. package/src/shapes/Path.ts +2 -1
  49. package/src/shapes/Triangle.ts +2 -2
  50. package/dist/cjs/rounding.js +0 -146
  51. package/dist/mjs/rounding.mjs +0 -139
  52. package/src/rounding.test.ts +0 -65
  53. package/src/rounding.ts +0 -168
  54. /package/dist/cjs/{rounding.test.d.ts → rounding/cleanUpNumber.test.d.ts} +0 -0
  55. /package/dist/{mjs/rounding.test.d.ts → cjs/rounding/toRoundedString.test.d.ts} +0 -0
@@ -0,0 +1,15 @@
1
+ import cleanUpNumber from './cleanUpNumber';
2
+
3
+ it('cleanUpNumber', () => {
4
+ expect(cleanUpNumber('000.0000')).toBe('0');
5
+ expect(cleanUpNumber('-000.0000')).toBe('0');
6
+ expect(cleanUpNumber('0.0000')).toBe('0');
7
+ expect(cleanUpNumber('0.001')).toBe('.001');
8
+ expect(cleanUpNumber('-0.001')).toBe('-.001');
9
+ expect(cleanUpNumber('-0.000000001')).toBe('-.000000001');
10
+ expect(cleanUpNumber('-0.00000000100')).toBe('-.000000001');
11
+ expect(cleanUpNumber('1234')).toBe('1234');
12
+ expect(cleanUpNumber('1234.5')).toBe('1234.5');
13
+ expect(cleanUpNumber('1234.500')).toBe('1234.5');
14
+ expect(cleanUpNumber('1.1368683772161603e-13')).toBe('0');
15
+ });
@@ -0,0 +1,38 @@
1
+ /** Cleans up stringified numbers */
2
+ export const cleanUpNumber = (text: string) => {
3
+ // Regular expression substitions can be somewhat expensive. Only do them
4
+ // if necessary.
5
+
6
+ if (text.indexOf('e') > 0) {
7
+ // Round to zero.
8
+ if (text.match(/[eE][-]\d{2,}$/)) {
9
+ return '0';
10
+ }
11
+ }
12
+
13
+ const lastChar = text.charAt(text.length - 1);
14
+ if (lastChar === '0' || lastChar === '.') {
15
+ // Remove trailing zeroes
16
+ text = text.replace(/([.]\d*[^0]+)0+$/, '$1');
17
+ text = text.replace(/[.]0+$/, '.');
18
+
19
+ // Remove trailing period
20
+ text = text.replace(/[.]$/, '');
21
+ }
22
+
23
+ const firstChar = text.charAt(0);
24
+ if (firstChar === '0' || firstChar === '-') {
25
+ // Remove unnecessary leading zeroes.
26
+ text = text.replace(/^(0+)[.]/, '.');
27
+ text = text.replace(/^-(0+)[.]/, '-.');
28
+ text = text.replace(/^(-?)0+$/, '$10');
29
+ }
30
+
31
+ if (text === '-0') {
32
+ return '0';
33
+ }
34
+
35
+ return text;
36
+ };
37
+
38
+ export default cleanUpNumber;
@@ -0,0 +1,3 @@
1
+
2
+
3
+ export const numberRegex = /^([-]?)(\d*)[.](\d+)$/;
@@ -0,0 +1,29 @@
1
+ import { numberRegex } from './constants';
2
+
3
+
4
+ /**
5
+ * Returns the length of `numberAsString` after a decimal point.
6
+ *
7
+ * For example,
8
+ * ```ts
9
+ * getLenAfterDecimal('1.001') // -> 3
10
+ * ```
11
+ */
12
+ export const getLenAfterDecimal = (numberAsString: string) => {
13
+ const numberMatch = numberRegex.exec(numberAsString);
14
+ if (!numberMatch) {
15
+ // If not a match, either the number is exponential notation (or is something
16
+ // like NaN or Infinity)
17
+ if (numberAsString.search(/[eE]/) !== -1 || /^[a-zA-Z]+$/.exec(numberAsString)) {
18
+ return -1;
19
+ // Or it has no decimal point
20
+ } else {
21
+ return 0;
22
+ }
23
+ }
24
+
25
+ const afterDecimalLen = numberMatch[3].length;
26
+ return afterDecimalLen;
27
+ };
28
+
29
+ export default getLenAfterDecimal;
@@ -0,0 +1,2 @@
1
+
2
+ export { toRoundedString } from './toRoundedString';
@@ -0,0 +1,32 @@
1
+ import { toRoundedString } from './toRoundedString';
2
+
3
+ describe('toRoundedString', () => {
4
+ it('should round up numbers endings similar to .999999999999999', () => {
5
+ expect(toRoundedString(0.999999999)).toBe('1');
6
+ expect(toRoundedString(0.899999999)).toBe('.9');
7
+ expect(toRoundedString(9.999999999)).toBe('10');
8
+ expect(toRoundedString(-10.999999999)).toBe('-11');
9
+ });
10
+
11
+ it('should round up numbers similar to 10.999999998', () => {
12
+ expect(toRoundedString(10.999999998)).toBe('11');
13
+ });
14
+
15
+ it('should round strings with multiple digits after the ending decimal points', () => {
16
+ expect(toRoundedString(292.2 - 292.8)).toBe('-.6');
17
+ expect(toRoundedString(4.06425600000023)).toBe('4.064256');
18
+ });
19
+
20
+ it('should round down strings ending endings similar to .00000001', () => {
21
+ expect(toRoundedString(10.00000001)).toBe('10');
22
+ expect(toRoundedString(-30.00000001)).toBe('-30');
23
+ expect(toRoundedString(-14.20000000000002)).toBe('-14.2');
24
+ });
25
+
26
+ it('should not round numbers insufficiently close to the next', () => {
27
+ expect(toRoundedString(-10.9999)).toBe('-10.9999');
28
+ expect(toRoundedString(-10.0001)).toBe('-10.0001');
29
+ expect(toRoundedString(-10.123499)).toBe('-10.123499');
30
+ expect(toRoundedString(0.00123499)).toBe('.00123499');
31
+ });
32
+ });
@@ -0,0 +1,57 @@
1
+ import cleanUpNumber from './cleanUpNumber';
2
+
3
+ /**
4
+ * Converts `num` to a string, removing trailing digits that were likely caused by
5
+ * precision errors.
6
+ *
7
+ * @example
8
+ * ```ts,runnable,console
9
+ * import { toRoundedString } from '@js-draw/math';
10
+ *
11
+ * console.log('Rounded: ', toRoundedString(1.000000011));
12
+ * ```
13
+ */
14
+ export const toRoundedString = (num: number): string => {
15
+ // Try to remove rounding errors. If the number ends in at least three/four zeroes
16
+ // (or nines) just one or two digits, it's probably a rounding error.
17
+ const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d{1,4}$/;
18
+ const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,})\d{1,4}$/;
19
+
20
+ let text = num.toString(10);
21
+ if (text.indexOf('.') === -1) {
22
+ return text;
23
+ }
24
+
25
+ const roundingDownMatch = hasRoundingDownExp.exec(text);
26
+ if (roundingDownMatch) {
27
+ const negativeSign = roundingDownMatch[1];
28
+ const postDecimalString = roundingDownMatch[3];
29
+ const lastDigit = parseInt(postDecimalString.charAt(postDecimalString.length - 1), 10);
30
+ const postDecimal = parseInt(postDecimalString, 10);
31
+ const preDecimal = parseInt(roundingDownMatch[2], 10);
32
+
33
+ const origPostDecimalString = roundingDownMatch[3];
34
+
35
+ let newPostDecimal = (postDecimal + 10 - lastDigit).toString();
36
+ let carry = 0;
37
+ if (newPostDecimal.length > postDecimal.toString().length) {
38
+ // Left-shift
39
+ newPostDecimal = newPostDecimal.substring(1);
40
+ carry = 1;
41
+ }
42
+
43
+ // parseInt(...).toString() removes leading zeroes. Add them back.
44
+ while (newPostDecimal.length < origPostDecimalString.length) {
45
+ newPostDecimal = carry.toString(10) + newPostDecimal;
46
+ carry = 0;
47
+ }
48
+
49
+ text = `${negativeSign + (preDecimal + carry).toString()}.${newPostDecimal}`;
50
+ }
51
+
52
+ text = text.replace(fixRoundingUpExp, '$1');
53
+
54
+ return cleanUpNumber(text);
55
+ };
56
+
57
+ export default toRoundedString;
@@ -0,0 +1,21 @@
1
+ import { toStringOfSamePrecision } from './toStringOfSamePrecision';
2
+
3
+
4
+ it('toStringOfSamePrecision', () => {
5
+ expect(toStringOfSamePrecision(1.23456, '1.12')).toBe('1.23');
6
+ expect(toStringOfSamePrecision(1.23456, '1.120')).toBe('1.235');
7
+ expect(toStringOfSamePrecision(1.23456, '1.1')).toBe('1.2');
8
+ expect(toStringOfSamePrecision(1.23456, '1.1', '5.32')).toBe('1.23');
9
+ expect(toStringOfSamePrecision(-1.23456, '1.1', '5.32')).toBe('-1.23');
10
+ expect(toStringOfSamePrecision(-1.99999, '1.1', '5.32')).toBe('-2');
11
+ expect(toStringOfSamePrecision(1.99999, '1.1', '5.32')).toBe('2');
12
+ expect(toStringOfSamePrecision(1.89999, '1.1', '5.32')).toBe('1.9');
13
+ expect(toStringOfSamePrecision(9.99999999, '-1.1234')).toBe('10');
14
+ expect(toStringOfSamePrecision(9.999999998999996, '100')).toBe('10');
15
+ expect(toStringOfSamePrecision(0.000012345, '0.000012')).toBe('.000012');
16
+ expect(toStringOfSamePrecision(0.000012645, '.000012')).toBe('.000013');
17
+ expect(toStringOfSamePrecision(-0.09999999999999432, '291.3')).toBe('-.1');
18
+ expect(toStringOfSamePrecision(-0.9999999999999432, '291.3')).toBe('-1');
19
+ expect(toStringOfSamePrecision(9998.9, '.1', '-11')).toBe('9998.9');
20
+ expect(toStringOfSamePrecision(-14.20000000000002, '.000001', '-11')).toBe('-14.2');
21
+ });
@@ -0,0 +1,63 @@
1
+ import cleanUpNumber from './cleanUpNumber';
2
+ import { numberRegex } from './constants';
3
+ import getLenAfterDecimal from './getLenAfterDecimal';
4
+ import toRoundedString from './toRoundedString';
5
+
6
+ // [reference] should be a string representation of a base-10 number (no exponential (e.g. 10e10))
7
+ export const toStringOfSamePrecision = (num: number, ...references: string[]): string => {
8
+ const text = num.toString(10);
9
+ const textMatch = numberRegex.exec(text);
10
+ if (!textMatch) {
11
+ return text;
12
+ }
13
+
14
+ let decimalPlaces = -1;
15
+ for (const reference of references) {
16
+ decimalPlaces = Math.max(getLenAfterDecimal(reference), decimalPlaces);
17
+ }
18
+
19
+ if (decimalPlaces === -1) {
20
+ return toRoundedString(num);
21
+ }
22
+
23
+ // Make text's after decimal length match [afterDecimalLen].
24
+ let postDecimal = textMatch[3].substring(0, decimalPlaces);
25
+ let preDecimal = textMatch[2];
26
+ const nextDigit = textMatch[3].charAt(decimalPlaces);
27
+
28
+ if (nextDigit !== '') {
29
+ const asNumber = parseInt(nextDigit, 10);
30
+ if (asNumber >= 5) {
31
+ // Don't attempt to parseInt() an empty string.
32
+ if (postDecimal.length > 0) {
33
+ const leadingZeroMatch = /^(0+)(\d*)$/.exec(postDecimal);
34
+
35
+ let leadingZeroes = '';
36
+ let postLeading = postDecimal;
37
+ if (leadingZeroMatch) {
38
+ leadingZeroes = leadingZeroMatch[1];
39
+ postLeading = leadingZeroMatch[2];
40
+ }
41
+
42
+ postDecimal = (parseInt(postDecimal) + 1).toString();
43
+
44
+ // If postDecimal got longer, remove leading zeroes if possible
45
+ if (postDecimal.length > postLeading.length && leadingZeroes.length > 0) {
46
+ leadingZeroes = leadingZeroes.substring(1);
47
+ }
48
+
49
+ postDecimal = leadingZeroes + postDecimal;
50
+ }
51
+
52
+ if (postDecimal.length === 0 || postDecimal.length > decimalPlaces) {
53
+ preDecimal = (parseInt(preDecimal) + 1).toString();
54
+ postDecimal = postDecimal.substring(1);
55
+ }
56
+ }
57
+ }
58
+
59
+ const negativeSign = textMatch[1];
60
+ return cleanUpNumber(`${negativeSign}${preDecimal}.${postDecimal}`);
61
+ };
62
+
63
+ export default toStringOfSamePrecision;
@@ -1,4 +1,3 @@
1
- import { toRoundedString, toStringOfSamePrecision } from '../rounding';
2
1
  import LineSegment2 from './LineSegment2';
3
2
  import Mat33 from '../Mat33';
4
3
  import Rect2 from './Rect2';
@@ -7,6 +6,8 @@ import Abstract2DShape from './Abstract2DShape';
7
6
  import CubicBezier from './CubicBezier';
8
7
  import QuadraticBezier from './QuadraticBezier';
9
8
  import PointShape2D from './PointShape2D';
9
+ import toRoundedString from '../rounding/toRoundedString';
10
+ import toStringOfSamePrecision from '../rounding/toStringOfSamePrecision';
10
11
 
11
12
  export enum PathCommandType {
12
13
  LineTo,
@@ -41,13 +41,13 @@ export default class Triangle extends Abstract2DShape {
41
41
 
42
42
  // Transform, treating this as composed of 2D points.
43
43
  public transformed2DBy(affineTransform: Mat33) {
44
- return this.map(affineTransform.transformVec2);
44
+ return this.map(vertex => affineTransform.transformVec2(vertex));
45
45
  }
46
46
 
47
47
  // Transforms this by a linear transform --- verticies are treated as
48
48
  // 3D points.
49
49
  public transformedBy(linearTransform: Mat33) {
50
- return this.map(linearTransform.transformVec3);
50
+ return this.map(vertex => linearTransform.transformVec3(vertex));
51
51
  }
52
52
 
53
53
  #sides: TriangleBoundary|undefined = undefined;
@@ -1,146 +0,0 @@
1
- "use strict";
2
- // @packageDocumentation @internal
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.toStringOfSamePrecision = exports.getLenAfterDecimal = exports.toRoundedString = exports.cleanUpNumber = void 0;
5
- // Clean up stringified numbers
6
- const cleanUpNumber = (text) => {
7
- // Regular expression substitions can be somewhat expensive. Only do them
8
- // if necessary.
9
- if (text.indexOf('e') > 0) {
10
- // Round to zero.
11
- if (text.match(/[eE][-]\d{2,}$/)) {
12
- return '0';
13
- }
14
- }
15
- const lastChar = text.charAt(text.length - 1);
16
- if (lastChar === '0' || lastChar === '.') {
17
- // Remove trailing zeroes
18
- text = text.replace(/([.]\d*[^0]+)0+$/, '$1');
19
- text = text.replace(/[.]0+$/, '.');
20
- // Remove trailing period
21
- text = text.replace(/[.]$/, '');
22
- }
23
- const firstChar = text.charAt(0);
24
- if (firstChar === '0' || firstChar === '-') {
25
- // Remove unnecessary leading zeroes.
26
- text = text.replace(/^(0+)[.]/, '.');
27
- text = text.replace(/^-(0+)[.]/, '-.');
28
- text = text.replace(/^(-?)0+$/, '$10');
29
- }
30
- if (text === '-0') {
31
- return '0';
32
- }
33
- return text;
34
- };
35
- exports.cleanUpNumber = cleanUpNumber;
36
- /**
37
- * Converts `num` to a string, removing trailing digits that were likely caused by
38
- * precision errors.
39
- *
40
- * @example
41
- * ```ts,runnable,console
42
- * import { toRoundedString } from '@js-draw/math';
43
- *
44
- * console.log('Rounded: ', toRoundedString(1.000000011));
45
- * ```
46
- */
47
- const toRoundedString = (num) => {
48
- // Try to remove rounding errors. If the number ends in at least three/four zeroes
49
- // (or nines) just one or two digits, it's probably a rounding error.
50
- const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d{1,4}$/;
51
- const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,})\d{1,4}$/;
52
- let text = num.toString(10);
53
- if (text.indexOf('.') === -1) {
54
- return text;
55
- }
56
- const roundingDownMatch = hasRoundingDownExp.exec(text);
57
- if (roundingDownMatch) {
58
- const negativeSign = roundingDownMatch[1];
59
- const postDecimalString = roundingDownMatch[3];
60
- const lastDigit = parseInt(postDecimalString.charAt(postDecimalString.length - 1), 10);
61
- const postDecimal = parseInt(postDecimalString, 10);
62
- const preDecimal = parseInt(roundingDownMatch[2], 10);
63
- const origPostDecimalString = roundingDownMatch[3];
64
- let newPostDecimal = (postDecimal + 10 - lastDigit).toString();
65
- let carry = 0;
66
- if (newPostDecimal.length > postDecimal.toString().length) {
67
- // Left-shift
68
- newPostDecimal = newPostDecimal.substring(1);
69
- carry = 1;
70
- }
71
- // parseInt(...).toString() removes leading zeroes. Add them back.
72
- while (newPostDecimal.length < origPostDecimalString.length) {
73
- newPostDecimal = carry.toString(10) + newPostDecimal;
74
- carry = 0;
75
- }
76
- text = `${negativeSign + (preDecimal + carry).toString()}.${newPostDecimal}`;
77
- }
78
- text = text.replace(fixRoundingUpExp, '$1');
79
- return (0, exports.cleanUpNumber)(text);
80
- };
81
- exports.toRoundedString = toRoundedString;
82
- const numberExp = /^([-]?)(\d*)[.](\d+)$/;
83
- const getLenAfterDecimal = (numberAsString) => {
84
- const numberMatch = numberExp.exec(numberAsString);
85
- if (!numberMatch) {
86
- // If not a match, either the number is exponential notation (or is something
87
- // like NaN or Infinity)
88
- if (numberAsString.search(/[eE]/) !== -1 || /^[a-zA-Z]+$/.exec(numberAsString)) {
89
- return -1;
90
- // Or it has no decimal point
91
- }
92
- else {
93
- return 0;
94
- }
95
- }
96
- const afterDecimalLen = numberMatch[3].length;
97
- return afterDecimalLen;
98
- };
99
- exports.getLenAfterDecimal = getLenAfterDecimal;
100
- // [reference] should be a string representation of a base-10 number (no exponential (e.g. 10e10))
101
- const toStringOfSamePrecision = (num, ...references) => {
102
- const text = num.toString(10);
103
- const textMatch = numberExp.exec(text);
104
- if (!textMatch) {
105
- return text;
106
- }
107
- let decimalPlaces = -1;
108
- for (const reference of references) {
109
- decimalPlaces = Math.max((0, exports.getLenAfterDecimal)(reference), decimalPlaces);
110
- }
111
- if (decimalPlaces === -1) {
112
- return (0, exports.toRoundedString)(num);
113
- }
114
- // Make text's after decimal length match [afterDecimalLen].
115
- let postDecimal = textMatch[3].substring(0, decimalPlaces);
116
- let preDecimal = textMatch[2];
117
- const nextDigit = textMatch[3].charAt(decimalPlaces);
118
- if (nextDigit !== '') {
119
- const asNumber = parseInt(nextDigit, 10);
120
- if (asNumber >= 5) {
121
- // Don't attempt to parseInt() an empty string.
122
- if (postDecimal.length > 0) {
123
- const leadingZeroMatch = /^(0+)(\d*)$/.exec(postDecimal);
124
- let leadingZeroes = '';
125
- let postLeading = postDecimal;
126
- if (leadingZeroMatch) {
127
- leadingZeroes = leadingZeroMatch[1];
128
- postLeading = leadingZeroMatch[2];
129
- }
130
- postDecimal = (parseInt(postDecimal) + 1).toString();
131
- // If postDecimal got longer, remove leading zeroes if possible
132
- if (postDecimal.length > postLeading.length && leadingZeroes.length > 0) {
133
- leadingZeroes = leadingZeroes.substring(1);
134
- }
135
- postDecimal = leadingZeroes + postDecimal;
136
- }
137
- if (postDecimal.length === 0 || postDecimal.length > decimalPlaces) {
138
- preDecimal = (parseInt(preDecimal) + 1).toString();
139
- postDecimal = postDecimal.substring(1);
140
- }
141
- }
142
- }
143
- const negativeSign = textMatch[1];
144
- return (0, exports.cleanUpNumber)(`${negativeSign}${preDecimal}.${postDecimal}`);
145
- };
146
- exports.toStringOfSamePrecision = toStringOfSamePrecision;
@@ -1,139 +0,0 @@
1
- // @packageDocumentation @internal
2
- // Clean up stringified numbers
3
- export const cleanUpNumber = (text) => {
4
- // Regular expression substitions can be somewhat expensive. Only do them
5
- // if necessary.
6
- if (text.indexOf('e') > 0) {
7
- // Round to zero.
8
- if (text.match(/[eE][-]\d{2,}$/)) {
9
- return '0';
10
- }
11
- }
12
- const lastChar = text.charAt(text.length - 1);
13
- if (lastChar === '0' || lastChar === '.') {
14
- // Remove trailing zeroes
15
- text = text.replace(/([.]\d*[^0]+)0+$/, '$1');
16
- text = text.replace(/[.]0+$/, '.');
17
- // Remove trailing period
18
- text = text.replace(/[.]$/, '');
19
- }
20
- const firstChar = text.charAt(0);
21
- if (firstChar === '0' || firstChar === '-') {
22
- // Remove unnecessary leading zeroes.
23
- text = text.replace(/^(0+)[.]/, '.');
24
- text = text.replace(/^-(0+)[.]/, '-.');
25
- text = text.replace(/^(-?)0+$/, '$10');
26
- }
27
- if (text === '-0') {
28
- return '0';
29
- }
30
- return text;
31
- };
32
- /**
33
- * Converts `num` to a string, removing trailing digits that were likely caused by
34
- * precision errors.
35
- *
36
- * @example
37
- * ```ts,runnable,console
38
- * import { toRoundedString } from '@js-draw/math';
39
- *
40
- * console.log('Rounded: ', toRoundedString(1.000000011));
41
- * ```
42
- */
43
- export const toRoundedString = (num) => {
44
- // Try to remove rounding errors. If the number ends in at least three/four zeroes
45
- // (or nines) just one or two digits, it's probably a rounding error.
46
- const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d{1,4}$/;
47
- const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,})\d{1,4}$/;
48
- let text = num.toString(10);
49
- if (text.indexOf('.') === -1) {
50
- return text;
51
- }
52
- const roundingDownMatch = hasRoundingDownExp.exec(text);
53
- if (roundingDownMatch) {
54
- const negativeSign = roundingDownMatch[1];
55
- const postDecimalString = roundingDownMatch[3];
56
- const lastDigit = parseInt(postDecimalString.charAt(postDecimalString.length - 1), 10);
57
- const postDecimal = parseInt(postDecimalString, 10);
58
- const preDecimal = parseInt(roundingDownMatch[2], 10);
59
- const origPostDecimalString = roundingDownMatch[3];
60
- let newPostDecimal = (postDecimal + 10 - lastDigit).toString();
61
- let carry = 0;
62
- if (newPostDecimal.length > postDecimal.toString().length) {
63
- // Left-shift
64
- newPostDecimal = newPostDecimal.substring(1);
65
- carry = 1;
66
- }
67
- // parseInt(...).toString() removes leading zeroes. Add them back.
68
- while (newPostDecimal.length < origPostDecimalString.length) {
69
- newPostDecimal = carry.toString(10) + newPostDecimal;
70
- carry = 0;
71
- }
72
- text = `${negativeSign + (preDecimal + carry).toString()}.${newPostDecimal}`;
73
- }
74
- text = text.replace(fixRoundingUpExp, '$1');
75
- return cleanUpNumber(text);
76
- };
77
- const numberExp = /^([-]?)(\d*)[.](\d+)$/;
78
- export const getLenAfterDecimal = (numberAsString) => {
79
- const numberMatch = numberExp.exec(numberAsString);
80
- if (!numberMatch) {
81
- // If not a match, either the number is exponential notation (or is something
82
- // like NaN or Infinity)
83
- if (numberAsString.search(/[eE]/) !== -1 || /^[a-zA-Z]+$/.exec(numberAsString)) {
84
- return -1;
85
- // Or it has no decimal point
86
- }
87
- else {
88
- return 0;
89
- }
90
- }
91
- const afterDecimalLen = numberMatch[3].length;
92
- return afterDecimalLen;
93
- };
94
- // [reference] should be a string representation of a base-10 number (no exponential (e.g. 10e10))
95
- export const toStringOfSamePrecision = (num, ...references) => {
96
- const text = num.toString(10);
97
- const textMatch = numberExp.exec(text);
98
- if (!textMatch) {
99
- return text;
100
- }
101
- let decimalPlaces = -1;
102
- for (const reference of references) {
103
- decimalPlaces = Math.max(getLenAfterDecimal(reference), decimalPlaces);
104
- }
105
- if (decimalPlaces === -1) {
106
- return toRoundedString(num);
107
- }
108
- // Make text's after decimal length match [afterDecimalLen].
109
- let postDecimal = textMatch[3].substring(0, decimalPlaces);
110
- let preDecimal = textMatch[2];
111
- const nextDigit = textMatch[3].charAt(decimalPlaces);
112
- if (nextDigit !== '') {
113
- const asNumber = parseInt(nextDigit, 10);
114
- if (asNumber >= 5) {
115
- // Don't attempt to parseInt() an empty string.
116
- if (postDecimal.length > 0) {
117
- const leadingZeroMatch = /^(0+)(\d*)$/.exec(postDecimal);
118
- let leadingZeroes = '';
119
- let postLeading = postDecimal;
120
- if (leadingZeroMatch) {
121
- leadingZeroes = leadingZeroMatch[1];
122
- postLeading = leadingZeroMatch[2];
123
- }
124
- postDecimal = (parseInt(postDecimal) + 1).toString();
125
- // If postDecimal got longer, remove leading zeroes if possible
126
- if (postDecimal.length > postLeading.length && leadingZeroes.length > 0) {
127
- leadingZeroes = leadingZeroes.substring(1);
128
- }
129
- postDecimal = leadingZeroes + postDecimal;
130
- }
131
- if (postDecimal.length === 0 || postDecimal.length > decimalPlaces) {
132
- preDecimal = (parseInt(preDecimal) + 1).toString();
133
- postDecimal = postDecimal.substring(1);
134
- }
135
- }
136
- }
137
- const negativeSign = textMatch[1];
138
- return cleanUpNumber(`${negativeSign}${preDecimal}.${postDecimal}`);
139
- };
@@ -1,65 +0,0 @@
1
- import { cleanUpNumber, toRoundedString, toStringOfSamePrecision } from './rounding';
2
-
3
- describe('toRoundedString', () => {
4
- it('should round up numbers endings similar to .999999999999999', () => {
5
- expect(toRoundedString(0.999999999)).toBe('1');
6
- expect(toRoundedString(0.899999999)).toBe('.9');
7
- expect(toRoundedString(9.999999999)).toBe('10');
8
- expect(toRoundedString(-10.999999999)).toBe('-11');
9
- });
10
-
11
- it('should round up numbers similar to 10.999999998', () => {
12
- expect(toRoundedString(10.999999998)).toBe('11');
13
- });
14
-
15
- it('should round strings with multiple digits after the ending decimal points', () => {
16
- expect(toRoundedString(292.2 - 292.8)).toBe('-.6');
17
- expect(toRoundedString(4.06425600000023)).toBe('4.064256');
18
- });
19
-
20
- it('should round down strings ending endings similar to .00000001', () => {
21
- expect(toRoundedString(10.00000001)).toBe('10');
22
- expect(toRoundedString(-30.00000001)).toBe('-30');
23
- expect(toRoundedString(-14.20000000000002)).toBe('-14.2');
24
- });
25
-
26
- it('should not round numbers insufficiently close to the next', () => {
27
- expect(toRoundedString(-10.9999)).toBe('-10.9999');
28
- expect(toRoundedString(-10.0001)).toBe('-10.0001');
29
- expect(toRoundedString(-10.123499)).toBe('-10.123499');
30
- expect(toRoundedString(0.00123499)).toBe('.00123499');
31
- });
32
- });
33
-
34
- it('toStringOfSamePrecision', () => {
35
- expect(toStringOfSamePrecision(1.23456, '1.12')).toBe('1.23');
36
- expect(toStringOfSamePrecision(1.23456, '1.120')).toBe('1.235');
37
- expect(toStringOfSamePrecision(1.23456, '1.1')).toBe('1.2');
38
- expect(toStringOfSamePrecision(1.23456, '1.1', '5.32')).toBe('1.23');
39
- expect(toStringOfSamePrecision(-1.23456, '1.1', '5.32')).toBe('-1.23');
40
- expect(toStringOfSamePrecision(-1.99999, '1.1', '5.32')).toBe('-2');
41
- expect(toStringOfSamePrecision(1.99999, '1.1', '5.32')).toBe('2');
42
- expect(toStringOfSamePrecision(1.89999, '1.1', '5.32')).toBe('1.9');
43
- expect(toStringOfSamePrecision(9.99999999, '-1.1234')).toBe('10');
44
- expect(toStringOfSamePrecision(9.999999998999996, '100')).toBe('10');
45
- expect(toStringOfSamePrecision(0.000012345, '0.000012')).toBe('.000012');
46
- expect(toStringOfSamePrecision(0.000012645, '.000012')).toBe('.000013');
47
- expect(toStringOfSamePrecision(-0.09999999999999432, '291.3')).toBe('-.1');
48
- expect(toStringOfSamePrecision(-0.9999999999999432, '291.3')).toBe('-1');
49
- expect(toStringOfSamePrecision(9998.9, '.1', '-11')).toBe('9998.9');
50
- expect(toStringOfSamePrecision(-14.20000000000002, '.000001', '-11')).toBe('-14.2');
51
- });
52
-
53
- it('cleanUpNumber', () => {
54
- expect(cleanUpNumber('000.0000')).toBe('0');
55
- expect(cleanUpNumber('-000.0000')).toBe('0');
56
- expect(cleanUpNumber('0.0000')).toBe('0');
57
- expect(cleanUpNumber('0.001')).toBe('.001');
58
- expect(cleanUpNumber('-0.001')).toBe('-.001');
59
- expect(cleanUpNumber('-0.000000001')).toBe('-.000000001');
60
- expect(cleanUpNumber('-0.00000000100')).toBe('-.000000001');
61
- expect(cleanUpNumber('1234')).toBe('1234');
62
- expect(cleanUpNumber('1234.5')).toBe('1234.5');
63
- expect(cleanUpNumber('1234.500')).toBe('1234.5');
64
- expect(cleanUpNumber('1.1368683772161603e-13')).toBe('0');
65
- });