UNPKG

8.11 kBJavaScriptView Raw
1import naturalSort from 'javascript-natural-sort'
2import { isDenseMatrix, isSparseMatrix, typeOf } from '../../utils/is'
3import { factory } from '../../utils/factory'
4
5const name = 'compareNatural'
6const dependencies = [
7 'typed',
8 'compare'
9]
10
11export const createCompareNatural = /* #__PURE__ */ factory(name, dependencies, ({ typed, compare }) => {
12 const compareBooleans = compare.signatures['boolean,boolean']
13
14 /**
15 * Compare two values of any type in a deterministic, natural way.
16 *
17 * For numeric values, the function works the same as `math.compare`.
18 * For types of values that can't be compared mathematically,
19 * the function compares in a natural way.
20 *
21 * For numeric values, x and y are considered equal when the relative
22 * difference between x and y is smaller than the configured epsilon.
23 * The function cannot be used to compare values smaller than
24 * approximately 2.22e-16.
25 *
26 * For Complex numbers, first the real parts are compared. If equal,
27 * the imaginary parts are compared.
28 *
29 * Strings are compared with a natural sorting algorithm, which
30 * orders strings in a "logic" way following some heuristics.
31 * This differs from the function `compare`, which converts the string
32 * into a numeric value and compares that. The function `compareText`
33 * on the other hand compares text lexically.
34 *
35 * Arrays and Matrices are compared value by value until there is an
36 * unequal pair of values encountered. Objects are compared by sorted
37 * keys until the keys or their values are unequal.
38 *
39 * Syntax:
40 *
41 * math.compareNatural(x, y)
42 *
43 * Examples:
44 *
45 * math.compareNatural(6, 1) // returns 1
46 * math.compareNatural(2, 3) // returns -1
47 * math.compareNatural(7, 7) // returns 0
48 *
49 * math.compareNatural('10', '2') // returns 1
50 * math.compareText('10', '2') // returns -1
51 * math.compare('10', '2') // returns 1
52 *
53 * math.compareNatural('Answer: 10', 'Answer: 2') // returns 1
54 * math.compareText('Answer: 10', 'Answer: 2') // returns -1
55 * math.compare('Answer: 10', 'Answer: 2')
56 * // Error: Cannot convert "Answer: 10" to a number
57 *
58 * const a = math.unit('5 cm')
59 * const b = math.unit('40 mm')
60 * math.compareNatural(a, b) // returns 1
61 *
62 * const c = math.complex('2 + 3i')
63 * const d = math.complex('2 + 4i')
64 * math.compareNatural(c, d) // returns -1
65 *
66 * math.compareNatural([1, 2, 4], [1, 2, 3]) // returns 1
67 * math.compareNatural([1, 2, 3], [1, 2]) // returns 1
68 * math.compareNatural([1, 5], [1, 2, 3]) // returns 1
69 * math.compareNatural([1, 2], [1, 2]) // returns 0
70 *
71 * math.compareNatural({a: 2}, {a: 4}) // returns -1
72 *
73 * See also:
74 *
75 * compare, compareText
76 *
77 * @param {*} x First value to compare
78 * @param {*} y Second value to compare
79 * @return {number} Returns the result of the comparison:
80 * 1 when x > y, -1 when x < y, and 0 when x == y.
81 */
82 const compareNatural = typed(name, {
83 'any, any': function (x, y) {
84 const typeX = typeOf(x)
85 const typeY = typeOf(y)
86 let c
87
88 // numeric types
89 if ((typeX === 'number' || typeX === 'BigNumber' || typeX === 'Fraction') &&
90 (typeY === 'number' || typeY === 'BigNumber' || typeY === 'Fraction')) {
91 c = compare(x, y)
92 if (c.toString() !== '0') {
93 // c can be number, BigNumber, or Fraction
94 return c > 0 ? 1 : -1 // return a number
95 } else {
96 return naturalSort(typeX, typeY)
97 }
98 }
99
100 // matrix types
101 if (typeX === 'Array' || typeX === 'Matrix' ||
102 typeY === 'Array' || typeY === 'Matrix') {
103 c = compareMatricesAndArrays(x, y)
104 if (c !== 0) {
105 return c
106 } else {
107 return naturalSort(typeX, typeY)
108 }
109 }
110
111 // in case of different types, order by name of type, i.e. 'BigNumber' < 'Complex'
112 if (typeX !== typeY) {
113 return naturalSort(typeX, typeY)
114 }
115
116 if (typeX === 'Complex') {
117 return compareComplexNumbers(x, y)
118 }
119
120 if (typeX === 'Unit') {
121 if (x.equalBase(y)) {
122 return compareNatural(x.value, y.value)
123 }
124
125 // compare by units
126 return compareArrays(x.formatUnits(), y.formatUnits())
127 }
128
129 if (typeX === 'boolean') {
130 return compareBooleans(x, y)
131 }
132
133 if (typeX === 'string') {
134 return naturalSort(x, y)
135 }
136
137 if (typeX === 'Object') {
138 return compareObjects(x, y)
139 }
140
141 if (typeX === 'null') {
142 return 0
143 }
144
145 if (typeX === 'undefined') {
146 return 0
147 }
148
149 // this should not occur...
150 throw new TypeError('Unsupported type of value "' + typeX + '"')
151 }
152 })
153
154 /**
155 * Compare mixed matrix/array types, by converting to same-shaped array.
156 * This comparator is non-deterministic regarding input types.
157 * @param {Array | SparseMatrix | DenseMatrix | *} x
158 * @param {Array | SparseMatrix | DenseMatrix | *} y
159 * @returns {number} Returns the comparison result: -1, 0, or 1
160 */
161 function compareMatricesAndArrays (x, y) {
162 if (isSparseMatrix(x) && isSparseMatrix(y)) {
163 return compareArrays(x.toJSON().values, y.toJSON().values)
164 }
165 if (isSparseMatrix(x)) {
166 // note: convert to array is expensive
167 return compareMatricesAndArrays(x.toArray(), y)
168 }
169 if (isSparseMatrix(y)) {
170 // note: convert to array is expensive
171 return compareMatricesAndArrays(x, y.toArray())
172 }
173
174 // convert DenseArray into Array
175 if (isDenseMatrix(x)) {
176 return compareMatricesAndArrays(x.toJSON().data, y)
177 }
178 if (isDenseMatrix(y)) {
179 return compareMatricesAndArrays(x, y.toJSON().data)
180 }
181
182 // convert scalars to array
183 if (!Array.isArray(x)) {
184 return compareMatricesAndArrays([x], y)
185 }
186 if (!Array.isArray(y)) {
187 return compareMatricesAndArrays(x, [y])
188 }
189
190 return compareArrays(x, y)
191 }
192
193 /**
194 * Compare two Arrays
195 *
196 * - First, compares value by value
197 * - Next, if all corresponding values are equal,
198 * look at the length: longest array will be considered largest
199 *
200 * @param {Array} x
201 * @param {Array} y
202 * @returns {number} Returns the comparison result: -1, 0, or 1
203 */
204 function compareArrays (x, y) {
205 // compare each value
206 for (let i = 0, ii = Math.min(x.length, y.length); i < ii; i++) {
207 const v = compareNatural(x[i], y[i])
208 if (v !== 0) {
209 return v
210 }
211 }
212
213 // compare the size of the arrays
214 if (x.length > y.length) { return 1 }
215 if (x.length < y.length) { return -1 }
216
217 // both Arrays have equal size and content
218 return 0
219 }
220
221 /**
222 * Compare two objects
223 *
224 * - First, compare sorted property names
225 * - Next, compare the property values
226 *
227 * @param {Object} x
228 * @param {Object} y
229 * @returns {number} Returns the comparison result: -1, 0, or 1
230 */
231 function compareObjects (x, y) {
232 const keysX = Object.keys(x)
233 const keysY = Object.keys(y)
234
235 // compare keys
236 keysX.sort(naturalSort)
237 keysY.sort(naturalSort)
238 const c = compareArrays(keysX, keysY)
239 if (c !== 0) {
240 return c
241 }
242
243 // compare values
244 for (let i = 0; i < keysX.length; i++) {
245 const v = compareNatural(x[keysX[i]], y[keysY[i]])
246 if (v !== 0) {
247 return v
248 }
249 }
250
251 return 0
252 }
253
254 return compareNatural
255})
256
257/**
258 * Compare two complex numbers, `x` and `y`:
259 *
260 * - First, compare the real values of `x` and `y`
261 * - If equal, compare the imaginary values of `x` and `y`
262 *
263 * @params {Complex} x
264 * @params {Complex} y
265 * @returns {number} Returns the comparison result: -1, 0, or 1
266 */
267function compareComplexNumbers (x, y) {
268 if (x.re > y.re) { return 1 }
269 if (x.re < y.re) { return -1 }
270
271 if (x.im > y.im) { return 1 }
272 if (x.im < y.im) { return -1 }
273
274 return 0
275}