@cavelang/solver 0.35.0 → 0.36.1

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 (119) hide show
  1. package/README.md +2155 -3
  2. package/dist/src/adapter.d.ts +6 -0
  3. package/dist/src/adapter.d.ts.map +1 -1
  4. package/dist/src/adapter.js +3 -0
  5. package/dist/src/adapter.js.map +1 -1
  6. package/dist/src/balanced-reduce.d.ts +3 -0
  7. package/dist/src/balanced-reduce.d.ts.map +1 -0
  8. package/dist/src/balanced-reduce.js +10 -0
  9. package/dist/src/balanced-reduce.js.map +1 -0
  10. package/dist/src/canonical-owned.d.ts +5 -0
  11. package/dist/src/canonical-owned.d.ts.map +1 -0
  12. package/dist/src/canonical-owned.js +206 -0
  13. package/dist/src/canonical-owned.js.map +1 -0
  14. package/dist/src/canonical.d.ts +3 -2
  15. package/dist/src/canonical.d.ts.map +1 -1
  16. package/dist/src/canonical.js +12 -85
  17. package/dist/src/canonical.js.map +1 -1
  18. package/dist/src/capability.d.ts.map +1 -1
  19. package/dist/src/capability.js +43 -33
  20. package/dist/src/capability.js.map +1 -1
  21. package/dist/src/clone.d.ts +3 -0
  22. package/dist/src/clone.d.ts.map +1 -0
  23. package/dist/src/clone.js +54 -0
  24. package/dist/src/clone.js.map +1 -0
  25. package/dist/src/constant-sign.d.ts +5 -0
  26. package/dist/src/constant-sign.d.ts.map +1 -0
  27. package/dist/src/constant-sign.js +54 -0
  28. package/dist/src/constant-sign.js.map +1 -0
  29. package/dist/src/context.d.ts +6 -0
  30. package/dist/src/context.d.ts.map +1 -0
  31. package/dist/src/context.js +151 -0
  32. package/dist/src/context.js.map +1 -0
  33. package/dist/src/diagnostic-text.d.ts +3 -0
  34. package/dist/src/diagnostic-text.d.ts.map +1 -0
  35. package/dist/src/diagnostic-text.js +5 -0
  36. package/dist/src/diagnostic-text.js.map +1 -0
  37. package/dist/src/evaluate-expression.d.ts +4 -0
  38. package/dist/src/evaluate-expression.d.ts.map +1 -0
  39. package/dist/src/evaluate-expression.js +24 -0
  40. package/dist/src/evaluate-expression.js.map +1 -0
  41. package/dist/src/exact.d.ts +2 -0
  42. package/dist/src/exact.d.ts.map +1 -1
  43. package/dist/src/exact.js +125 -28
  44. package/dist/src/exact.js.map +1 -1
  45. package/dist/src/explain.d.ts +4 -0
  46. package/dist/src/explain.d.ts.map +1 -1
  47. package/dist/src/explain.js +330 -91
  48. package/dist/src/explain.js.map +1 -1
  49. package/dist/src/explanation-budget.d.ts +14 -0
  50. package/dist/src/explanation-budget.d.ts.map +1 -0
  51. package/dist/src/explanation-budget.js +46 -0
  52. package/dist/src/explanation-budget.js.map +1 -0
  53. package/dist/src/expression-key.d.ts +8 -0
  54. package/dist/src/expression-key.d.ts.map +1 -0
  55. package/dist/src/expression-key.js +54 -0
  56. package/dist/src/expression-key.js.map +1 -0
  57. package/dist/src/fraction-product.d.ts +3 -0
  58. package/dist/src/fraction-product.d.ts.map +1 -0
  59. package/dist/src/fraction-product.js +13 -0
  60. package/dist/src/fraction-product.js.map +1 -0
  61. package/dist/src/fraction-sum.d.ts +3 -0
  62. package/dist/src/fraction-sum.d.ts.map +1 -0
  63. package/dist/src/fraction-sum.js +23 -0
  64. package/dist/src/fraction-sum.js.map +1 -0
  65. package/dist/src/integer-gcd.d.ts +3 -0
  66. package/dist/src/integer-gcd.d.ts.map +1 -0
  67. package/dist/src/integer-gcd.js +50 -0
  68. package/dist/src/integer-gcd.js.map +1 -0
  69. package/dist/src/linear.d.ts +2 -1
  70. package/dist/src/linear.d.ts.map +1 -1
  71. package/dist/src/linear.js +85 -17
  72. package/dist/src/linear.js.map +1 -1
  73. package/dist/src/numeric-size.d.ts +2 -0
  74. package/dist/src/numeric-size.d.ts.map +1 -0
  75. package/dist/src/numeric-size.js +33 -0
  76. package/dist/src/numeric-size.js.map +1 -0
  77. package/dist/src/options.d.ts +4 -0
  78. package/dist/src/options.d.ts.map +1 -0
  79. package/dist/src/options.js +18 -0
  80. package/dist/src/options.js.map +1 -0
  81. package/dist/src/result.d.ts +4 -0
  82. package/dist/src/result.d.ts.map +1 -0
  83. package/dist/src/result.js +29 -0
  84. package/dist/src/result.js.map +1 -0
  85. package/dist/src/solve.d.ts.map +1 -1
  86. package/dist/src/solve.js +34 -3
  87. package/dist/src/solve.js.map +1 -1
  88. package/dist/src/validate.d.ts +5 -0
  89. package/dist/src/validate.d.ts.map +1 -1
  90. package/dist/src/validate.js +323 -65
  91. package/dist/src/validate.js.map +1 -1
  92. package/dist/src/workflow.d.ts.map +1 -1
  93. package/dist/src/workflow.js +98 -26
  94. package/dist/src/workflow.js.map +1 -1
  95. package/package.json +2 -2
  96. package/src/adapter.ts +9 -0
  97. package/src/balanced-reduce.ts +9 -0
  98. package/src/canonical-owned.ts +186 -0
  99. package/src/canonical.ts +14 -90
  100. package/src/capability.ts +41 -30
  101. package/src/clone.ts +47 -0
  102. package/src/constant-sign.ts +59 -0
  103. package/src/context.ts +114 -0
  104. package/src/diagnostic-text.ts +4 -0
  105. package/src/evaluate-expression.ts +21 -0
  106. package/src/exact.ts +122 -27
  107. package/src/explain.ts +342 -94
  108. package/src/explanation-budget.ts +47 -0
  109. package/src/expression-key.ts +45 -0
  110. package/src/fraction-product.ts +10 -0
  111. package/src/fraction-sum.ts +20 -0
  112. package/src/integer-gcd.ts +47 -0
  113. package/src/linear.ts +82 -16
  114. package/src/numeric-size.ts +26 -0
  115. package/src/options.ts +18 -0
  116. package/src/result.ts +28 -0
  117. package/src/solve.ts +34 -3
  118. package/src/validate.ts +312 -66
  119. package/src/workflow.ts +92 -26
package/src/exact.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { diagnosticText } from './diagnostic-text.ts'
2
+ import { gcd } from './integer-gcd.ts'
1
3
  import type { Integer, Rational } from './model.ts'
2
4
 
3
5
  export type Normalized = {
@@ -8,15 +10,10 @@ export type Normalized = {
8
10
  const integerPattern = /^[+-]?\d+$/
9
11
  const decimalPattern = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/
10
12
 
11
- const gcd = (left: bigint, right: bigint): bigint => {
12
- let a = left < 0n ? -left : left
13
- let b = right < 0n ? -right : right
14
- while (b !== 0n) {
15
- const remainder = a % b
16
- a = b
17
- b = remainder
18
- }
19
- return a
13
+ const assertIntegerText = (input: string): void => {
14
+ if (input.match(integerPattern)?.[0] !== input) {
15
+ throw new TypeError(`expected an integer string, received ${diagnosticText(input)}`)
16
+ }
20
17
  }
21
18
 
22
19
  export const integer = (input: Integer): bigint => {
@@ -26,40 +23,95 @@ export const integer = (input: Integer): bigint => {
26
23
  }
27
24
  return BigInt(input)
28
25
  }
29
- if (!integerPattern.test(input)) {
30
- throw new TypeError(`expected an integer string, received ${JSON.stringify(input)}`)
26
+ if (typeof input !== 'string') {
27
+ throw new TypeError('expected a safe integer number or an integer string')
31
28
  }
29
+ assertIntegerText(input)
32
30
  return BigInt(input)
33
31
  }
34
32
 
35
- const decimal = (input: string): readonly [bigint, bigint] => {
36
- if (!decimalPattern.test(input)) {
37
- throw new TypeError(`expected an exact decimal string, received ${JSON.stringify(input)}`)
33
+ const decimalParts = (input: string): { coefficient: string, exponent: number } => {
34
+ if (input.match(decimalPattern)?.[0] !== input) {
35
+ throw new TypeError(`expected an exact decimal string, received ${diagnosticText(input)}`)
38
36
  }
39
37
  const [coefficient = '', exponentText] = input.toLowerCase().split('e')
40
38
  const exponent = exponentText === undefined ? 0 : Number(exponentText)
41
39
  if (!Number.isSafeInteger(exponent)) {
42
- throw new TypeError(`decimal exponent is outside the supported range: ${JSON.stringify(input)}`)
40
+ throw new TypeError(`decimal exponent is outside the supported range: ${diagnosticText(input)}`)
43
41
  }
42
+ return { coefficient, exponent }
43
+ }
44
+
45
+ const decimal = (input: string): Normalized => {
46
+ const { coefficient, exponent } = decimalParts(input)
44
47
  const negative = coefficient.startsWith('-')
45
48
  const unsigned = coefficient.replace(/^[+-]/, '')
46
49
  const [whole = '', fraction = ''] = unsigned.split('.')
47
50
  const digits = `${whole === '' ? '0' : whole}${fraction}`
48
- const scale = fraction.length - exponent
49
- const signed = BigInt(digits === '' ? '0' : digits) * (negative ? -1n : 1n)
50
- return scale <= 0
51
- ? [signed * (10n ** BigInt(-scale)), 1n]
52
- : [signed, 10n ** BigInt(scale)]
51
+ let scale = fraction.length - exponent
52
+ let magnitude = digits.replace(/^0+/, '')
53
+ if (magnitude === '') return { numerator: '0', denominator: '1' }
54
+ // Cancel powers of ten in text before allocating either bigint operand.
55
+ let cancelled = 0
56
+ while (cancelled < scale && magnitude[magnitude.length - cancelled - 1] === '0') cancelled++
57
+ if (cancelled > 0) {
58
+ magnitude = magnitude.slice(0, -cancelled)
59
+ scale -= cancelled
60
+ }
61
+ if (scale <= 0) {
62
+ return { numerator: `${negative ? '-' : ''}${magnitude}${'0'.repeat(-scale)}`, denominator: '1' }
63
+ }
64
+ return fromBigInts(BigInt(magnitude) * (negative ? -1n : 1n), 10n ** BigInt(scale))
65
+ }
66
+
67
+ const assertRationalObject = (input: unknown): void => {
68
+ if (input === null || typeof input !== 'object') {
69
+ throw new TypeError('expected an exact decimal string or a numerator/denominator object')
70
+ }
53
71
  }
54
72
 
55
73
  export const rational = (input: Rational): Normalized => {
56
- let numerator: bigint
74
+ let numerator = 0n
57
75
  let denominator: bigint
58
76
  if (typeof input === 'string') {
59
- ;[numerator, denominator] = decimal(input)
77
+ return decimal(input)
60
78
  } else {
61
- numerator = integer(input.numerator)
62
- denominator = integer(input.denominator)
79
+ assertRationalObject(input)
80
+ const suppliedNumerator = input.numerator
81
+ const numeratorText = typeof suppliedNumerator === 'string' ? suppliedNumerator : undefined
82
+ let zero: boolean
83
+ if (numeratorText !== undefined) {
84
+ assertIntegerText(numeratorText)
85
+ zero = !/[1-9]/.test(numeratorText)
86
+ } else {
87
+ numerator = integer(suppliedNumerator)
88
+ zero = numerator === 0n
89
+ }
90
+ const suppliedDenominator = input.denominator
91
+ if (zero && typeof suppliedDenominator === 'string') {
92
+ assertIntegerText(suppliedDenominator)
93
+ if (!/[1-9]/.test(suppliedDenominator)) {
94
+ throw new TypeError('rational denominator must not be zero')
95
+ }
96
+ return { numerator: '0', denominator: '1' }
97
+ }
98
+ denominator = integer(suppliedDenominator)
99
+ if (numeratorText !== undefined) {
100
+ if (denominator === 1n || denominator === -1n) {
101
+ const magnitude = numeratorText.replace(/^[+-]?0*/, '')
102
+ const negative = numeratorText.startsWith('-') !== (denominator === -1n)
103
+ return { numerator: magnitude === '' ? '0' : `${negative ? '-' : ''}${magnitude}`, denominator: '1' }
104
+ }
105
+ numerator = BigInt(numeratorText)
106
+ }
107
+ }
108
+ return fromBigInts(numerator, denominator)
109
+ }
110
+
111
+ /** Normalize integer intermediates without serializing and parsing them again. */
112
+ export const fromBigInts = (numerator: bigint, denominator: bigint): Normalized => {
113
+ if (typeof numerator !== 'bigint' || typeof denominator !== 'bigint') {
114
+ throw new TypeError('expected bigint numerator and denominator')
63
115
  }
64
116
  if (denominator === 0n) {
65
117
  throw new TypeError('rational denominator must not be zero')
@@ -75,11 +127,54 @@ export const rational = (input: Rational): Normalized => {
75
127
  }
76
128
  }
77
129
 
130
+ /** Compare canonical integer text without allocating bigint magnitudes. */
131
+ const integerOrder = (left: string, right: string): -1 | 0 | 1 => {
132
+ if (left === right) return 0
133
+ const negative = left.startsWith('-')
134
+ if (negative !== right.startsWith('-')) return negative ? -1 : 1
135
+ const a = negative ? left.slice(1) : left
136
+ const b = negative ? right.slice(1) : right
137
+ const order = a.length === b.length ? (a < b ? -1 : 1) : a.length < b.length ? -1 : 1
138
+ return negative ? (order === -1 ? 1 : -1) : order
139
+ }
140
+
78
141
  export const compare = (left: Rational, right: Rational): -1 | 0 | 1 => {
79
142
  const a = rational(left)
80
143
  const b = rational(right)
81
- const difference = BigInt(a.numerator) * BigInt(b.denominator) - BigInt(b.numerator) * BigInt(a.denominator)
82
- return difference < 0n ? -1 : difference > 0n ? 1 : 0
144
+ const numerators = integerOrder(a.numerator, b.numerator)
145
+ const negative = a.numerator.startsWith('-')
146
+ if (a.denominator === b.denominator || a.numerator === '0' || b.numerator === '0' ||
147
+ negative !== b.numerator.startsWith('-')) {
148
+ return numerators
149
+ }
150
+ const denominators = integerOrder(a.denominator, b.denominator)
151
+ if (numerators === 0) {
152
+ // Equal denominators were handled above. A positive common numerator
153
+ // reverses denominator order; a negative one preserves it.
154
+ return negative ? denominators : denominators === -1 ? 1 : -1
155
+ }
156
+ // With equal signs, a smaller magnitude numerator and larger denominator
157
+ // reinforce each other. Their ordering needs no cross-product allocation.
158
+ if ((numerators < 0) === (negative ? denominators < 0 : denominators > 0)) {
159
+ return numerators
160
+ }
161
+ const numeratorA = BigInt(a.numerator), numeratorB = BigInt(b.numerator)
162
+ const denominatorA = BigInt(a.denominator), denominatorB = BigInt(b.denominator)
163
+ const productA = numeratorA * denominatorB
164
+ const productB = numeratorB * denominatorA
165
+ return productA < productB ? -1 : productA > productB ? 1 : 0
83
166
  }
84
167
 
85
- export const isZero = (input: Rational): boolean => rational(input).numerator === '0'
168
+ const integerIsZero = (input: Integer): boolean => {
169
+ if (typeof input !== 'string') return integer(input) === 0n
170
+ assertIntegerText(input)
171
+ return !/[1-9]/.test(input)
172
+ }
173
+
174
+ export const isZero = (input: Rational): boolean => {
175
+ if (typeof input === 'string') return !/[1-9]/.test(decimalParts(input).coefficient)
176
+ assertRationalObject(input)
177
+ const zero = integerIsZero(input.numerator)
178
+ if (integerIsZero(input.denominator)) throw new TypeError('rational denominator must not be zero')
179
+ return zero
180
+ }