@c9up/atom 0.1.10 → 0.1.12

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.
package/src/index.ts CHANGED
@@ -26,7 +26,11 @@ export type {
26
26
  ToMinorUnitsOptions,
27
27
  } from "./Decimal.js";
28
28
  export { Decimal } from "./Decimal.js";
29
- export type { MoneyFormatOptions, MoneyOptions } from "./Money.js";
29
+ export type {
30
+ MoneyFormatOptions,
31
+ MoneyOptions,
32
+ MoneyRoundingOptions,
33
+ } from "./Money.js";
30
34
  export { Money, money } from "./Money.js";
31
35
  export { isNativeAvailable } from "./native.js";
32
36
 
@@ -46,13 +50,36 @@ function isDecimalIterable(value: unknown): value is Iterable<DecimalInput> {
46
50
  );
47
51
  }
48
52
 
49
- function resolveValues(
50
- args: [Iterable<DecimalInput>] | DecimalInput[],
51
- ): Iterable<DecimalInput> {
52
- if (args.length === 1 && isDecimalIterable(args[0])) {
53
- return args[0] as Iterable<DecimalInput>;
54
- }
55
- return args as DecimalInput[];
53
+ /**
54
+ * What every aggregate accepts as one value.
55
+ *
56
+ * The variadic form used to be asserted back into shape — a claim about the
57
+ * caller's arguments made in the one place that can check them. Anything else
58
+ * reached `new Decimal(...)`, where an object failed as
59
+ * `input.trim is not a function`, naming neither the argument nor the call.
60
+ */
61
+ function isDecimalValue(value: unknown): value is DecimalInput {
62
+ return (
63
+ Decimal.isDecimal(value) ||
64
+ typeof value === "string" ||
65
+ typeof value === "number" ||
66
+ typeof value === "bigint"
67
+ );
68
+ }
69
+
70
+ function toDecimalValue(value: unknown): DecimalInput {
71
+ if (isDecimalValue(value)) return value;
72
+ throw new Error(
73
+ `Atom aggregates take a string, number, bigint or Decimal, got ${
74
+ value === null ? "null" : typeof value
75
+ }`,
76
+ );
77
+ }
78
+
79
+ function resolveValues(args: readonly unknown[]): Iterable<DecimalInput> {
80
+ const head = args[0];
81
+ if (args.length === 1 && isDecimalIterable(head)) return head;
82
+ return args.map(toDecimalValue);
56
83
  }
57
84
 
58
85
  function sumImpl(values: Iterable<DecimalInput>): Decimal {
@@ -77,25 +104,25 @@ function avgImpl(values: Iterable<DecimalInput>): Decimal {
77
104
  }
78
105
 
79
106
  function minImpl(values: Iterable<DecimalInput>): Decimal {
80
- const list = [...values].map((value) => new Decimal(value));
81
- if (list.length === 0) {
107
+ const [first, ...rest] = [...values].map((value) => new Decimal(value));
108
+ if (first === undefined) {
82
109
  throw new Error("Atom.min requires at least one value");
83
110
  }
84
- let best = list[0];
85
- for (let i = 1; i < list.length; i++) {
86
- if (list[i].lt(best)) best = list[i];
111
+ let best = first;
112
+ for (const candidate of rest) {
113
+ if (candidate.lt(best)) best = candidate;
87
114
  }
88
115
  return best;
89
116
  }
90
117
 
91
118
  function maxImpl(values: Iterable<DecimalInput>): Decimal {
92
- const list = [...values].map((value) => new Decimal(value));
93
- if (list.length === 0) {
119
+ const [first, ...rest] = [...values].map((value) => new Decimal(value));
120
+ if (first === undefined) {
94
121
  throw new Error("Atom.max requires at least one value");
95
122
  }
96
- let best = list[0];
97
- for (let i = 1; i < list.length; i++) {
98
- if (list[i].gt(best)) best = list[i];
123
+ let best = first;
124
+ for (const candidate of rest) {
125
+ if (candidate.gt(best)) best = candidate;
99
126
  }
100
127
  return best;
101
128
  }
@@ -110,9 +137,14 @@ function medianImpl(
110
137
  }
111
138
  list.sort((a, b) => a.cmp(b));
112
139
  const mid = Math.floor(list.length / 2);
113
- if (list.length % 2 === 1) return list[mid];
140
+ // The empty case threw above, so both halves of the middle are present.
141
+ const upper = list[mid];
142
+ if (upper === undefined) throw new Error("Atom.median lost its middle value");
143
+ if (list.length % 2 === 1) return upper;
144
+ const lower = list[mid - 1];
145
+ if (lower === undefined) throw new Error("Atom.median lost its middle value");
114
146
  const precision = options.precision ?? defaultPrecision();
115
- return list[mid - 1].plus(list[mid]).div("2", { precision });
147
+ return lower.plus(upper).div("2", { precision });
116
148
  }
117
149
 
118
150
  function modeImpl(values: Iterable<DecimalInput>): Decimal[] {
@@ -162,32 +194,39 @@ function stddevImpl(
162
194
  return variance.sqrt({ precision, mode });
163
195
  }
164
196
 
197
+ /**
198
+ * The trailing options object of `median(values, options)` / `stddev(values,
199
+ * options)`, or `{}`.
200
+ *
201
+ * A `Decimal` is an object too, so excluding it is what lets the compiler
202
+ * check the narrowing instead of being told the answer.
203
+ */
204
+ function optionsFrom<T extends MedianOptions | StddevOptions>(
205
+ value: DecimalInput | T | undefined,
206
+ ): T | Record<string, never> {
207
+ if (typeof value !== "object" || value === null) return {};
208
+ if (Decimal.isDecimal(value)) return {};
209
+ return value;
210
+ }
211
+
165
212
  function parseMedianArgs(
166
213
  args: [Iterable<DecimalInput>, MedianOptions?] | DecimalInput[],
167
214
  ): { values: Iterable<DecimalInput>; options: MedianOptions } {
168
- if (args.length >= 1 && isDecimalIterable(args[0])) {
169
- const values = args[0] as Iterable<DecimalInput>;
170
- const options =
171
- args.length > 1 && typeof args[1] === "object" && args[1] !== null
172
- ? (args[1] as MedianOptions)
173
- : {};
174
- return { values, options };
215
+ const head = args[0];
216
+ if (isDecimalIterable(head)) {
217
+ return { values: head, options: optionsFrom<MedianOptions>(args[1]) };
175
218
  }
176
- return { values: args as DecimalInput[], options: {} };
219
+ return { values: resolveValues(args), options: {} };
177
220
  }
178
221
 
179
222
  function parseStddevArgs(
180
223
  args: [Iterable<DecimalInput>, StddevOptions?] | DecimalInput[],
181
224
  ): { values: Iterable<DecimalInput>; options: StddevOptions } {
182
- if (args.length >= 1 && isDecimalIterable(args[0])) {
183
- const values = args[0] as Iterable<DecimalInput>;
184
- const options =
185
- args.length > 1 && typeof args[1] === "object" && args[1] !== null
186
- ? (args[1] as StddevOptions)
187
- : {};
188
- return { values, options };
225
+ const head = args[0];
226
+ if (isDecimalIterable(head)) {
227
+ return { values: head, options: optionsFrom<StddevOptions>(args[1]) };
189
228
  }
190
- return { values: args as DecimalInput[], options: {} };
229
+ return { values: resolveValues(args), options: {} };
191
230
  }
192
231
 
193
232
  function sumFn(...args: [Iterable<DecimalInput>] | DecimalInput[]): Decimal {