@js-draw/math 1.10.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
- });