@vuu-ui/vuu-utils 3.5.17 → 3.5.18-beta.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.
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.5.17",
2
+ "version": "3.5.18-beta.1",
3
3
  "author": "heswell",
4
4
  "main": "./src/index.js",
5
5
  "license": "Apache-2.0",
@@ -15,14 +15,14 @@
15
15
  },
16
16
  "devDependencies": {
17
17
  "@dnd-kit/react": "^0.1.21",
18
- "@vuu-ui/vuu-data-types": "3.5.17",
19
- "@vuu-ui/vuu-table-types": "3.5.17",
20
- "@vuu-ui/vuu-filter-types": "3.5.17",
21
- "@vuu-ui/vuu-protocol-types": "3.5.17"
18
+ "@vuu-ui/vuu-data-types": "3.5.18-beta.1",
19
+ "@vuu-ui/vuu-table-types": "3.5.18-beta.1",
20
+ "@vuu-ui/vuu-filter-types": "3.5.18-beta.1",
21
+ "@vuu-ui/vuu-protocol-types": "3.5.18-beta.1"
22
22
  },
23
23
  "peerDependencies": {
24
24
  "@internationalized/date": "^3.0.0",
25
- "@vuu-ui/vuu-filter-parser": "3.5.17",
25
+ "@vuu-ui/vuu-filter-parser": "3.5.18-beta.1",
26
26
  "clsx": "^2.0.0",
27
27
  "react": "^19.2.3",
28
28
  "react-dom": "^19.2.3"
@@ -9,7 +9,6 @@ class ScaledDecimal {
9
9
  constructor(value, decimals = 6){
10
10
  this.value = value;
11
11
  this.decimals = decimals;
12
- console.log(`create a scaled decimal ${value} (${decimals} decimals)`);
13
12
  }
14
13
  get asLong() {
15
14
  if ("" === this.value) return "";
@@ -4,6 +4,7 @@ import { isStartOfDay, startOfDay, startOfNextDay, timeOfDayToEpochMillis } from
4
4
  import { stringIsValidLong } from "../data-utils.js";
5
5
  import { isMultiClauseFilter, isMultiValueFilter, isSerializableFilter } from "./filter-utils.js";
6
6
  import { ScaledDecimal } from "../ScaledDecimal.js";
7
+ import { getScaledDecimalScale, isScaledDecimalFilterOptions, scaledDecimalFilterAsQuery } from "./scaled-decimal-filter.js";
7
8
  const filterValue = (value, columnType)=>{
8
9
  if ("string" == typeof value) {
9
10
  if (void 0 !== columnType) {
@@ -12,7 +13,11 @@ const filterValue = (value, columnType)=>{
12
13
  "long",
13
14
  "double",
14
15
  "epochtimestamp",
15
- "epochtimestampnano"
16
+ "epochtimestampnano",
17
+ "scaleddecimal2",
18
+ "scaleddecimal4",
19
+ "scaleddecimal6",
20
+ "scaleddecimal8"
16
21
  ].includes(columnType);
17
22
  return isNumericType ? value : `"${value}"`;
18
23
  }
@@ -43,6 +48,7 @@ const filterAsQueryCore = (f, opts)=>{
43
48
  function singleValueFilterAsQuery(f, opts) {
44
49
  if (isSerializableFilter(f)) return f.asQuery();
45
50
  {
51
+ if (isScaledDecimalFilterOptions(f.extendedOptions)) return scaledDecimalFilterAsQuery(f, f.extendedOptions);
46
52
  const column = opts?.columnsByName?.[f.column];
47
53
  const temporalInfo = getTemporalInfo(column);
48
54
  if (temporalInfo) return temporalFilterAsQuery(f, temporalInfo);
@@ -143,7 +149,10 @@ const getColumnsByNameForFilter = (tableSchema, columnsByName)=>{
143
149
  };
144
150
  const filterRequiresResolution = (filter, columnsByName)=>{
145
151
  if (isMultiClauseFilter(filter)) return filter.filters.some((f)=>filterRequiresResolution(f, columnsByName));
146
- if (isSerializableFilter(filter)) return true;
147
- return void 0 !== getTemporalInfo(columnsByName?.[filter.column]);
152
+ {
153
+ if (isSerializableFilter(filter) || isScaledDecimalFilterOptions(filter.extendedOptions)) return true;
154
+ const column = columnsByName?.[filter.column];
155
+ return void 0 !== getTemporalInfo(column) || void 0 !== getScaledDecimalScale(column?.serverDataType);
156
+ }
148
157
  };
149
158
  export { ONE_DAY_IN_MILLIS, dateFilterAsQuery, filterAsQuery, filterRequiresResolution, getColumnsByName, getColumnsByNameForFilter, temporalFilterAsQuery };
@@ -1,3 +1,4 @@
1
1
  export * from "./filter-utils.js";
2
2
  export * from "./filterAsQuery.js";
3
3
  export * from "./temporal-cell-filter.js";
4
+ export * from "./scaled-decimal-filter.js";
@@ -0,0 +1,130 @@
1
+ import { isTypeDescriptor } from "../column-utils.js";
2
+ import { roundScaledDecimal } from "../round-decimal.js";
3
+ const DEFAULT_DISPLAY_DECIMALS = 4;
4
+ const scales = {
5
+ scaleddecimal2: 2,
6
+ scaleddecimal4: 4,
7
+ scaleddecimal6: 6,
8
+ scaleddecimal8: 8
9
+ };
10
+ const getScaledDecimalScale = (serverDataType)=>void 0 === serverDataType ? void 0 : scales[serverDataType];
11
+ const decimalPattern = /^-?(\d+(\.\d*)?|\.\d+)$/;
12
+ const isValidDecimalString = (value)=>decimalPattern.test(value);
13
+ const isScaledDecimalFilterOptions = (options)=>options?.type === "ScaledDecimal";
14
+ const toDecimalString = (value, scale)=>{
15
+ if ("number" == typeof value) {
16
+ const text = `${value}`;
17
+ return /e/i.test(text) ? value.toFixed(scale) : text;
18
+ }
19
+ return `${value}`;
20
+ };
21
+ const toScaledInteger = (value, scale)=>{
22
+ if (!isValidDecimalString(value)) throw Error(`[scaled-decimal-filter] invalid decimal value '${value}'`);
23
+ const negative = value.startsWith("-");
24
+ const [integral, fraction = ""] = (negative ? value.slice(1) : value).split(".");
25
+ const magnitude = BigInt((integral || "0") + fraction.slice(0, scale).padEnd(scale, "0"));
26
+ return negative ? -magnitude : magnitude;
27
+ };
28
+ const fromScaledInteger = (value, scale)=>{
29
+ const negative = value < 0n;
30
+ const digits = (negative ? -value : value).toString().padStart(scale + 1, "0");
31
+ const literal = `${digits.slice(0, -scale)}.${digits.slice(-scale)}`;
32
+ return negative ? `-${literal}` : literal;
33
+ };
34
+ const getScaledDecimalFilterOptions = (column)=>{
35
+ const scale = getScaledDecimalScale(column.serverDataType);
36
+ if (void 0 === scale) return;
37
+ if (isTypeDescriptor(column.type)) {
38
+ const { decimals = DEFAULT_DISPLAY_DECIMALS, roundingRule = "round" } = column.type.formatting ?? {};
39
+ return decimals < scale ? {
40
+ type: "ScaledDecimal",
41
+ scale,
42
+ decimals,
43
+ roundingRule
44
+ } : {
45
+ type: "ScaledDecimal",
46
+ scale
47
+ };
48
+ }
49
+ return {
50
+ type: "ScaledDecimal",
51
+ scale
52
+ };
53
+ };
54
+ const countDecimals = (value)=>value.split(".")[1]?.length ?? 0;
55
+ const getScaledDecimalMatchRange = (value, { scale, decimals, roundingRule = "round" })=>{
56
+ const scaledValue = toScaledInteger(value, scale);
57
+ const precision = void 0 === decimals ? scale : Math.max(decimals, countDecimals(value));
58
+ if (precision >= scale) return [
59
+ scaledValue,
60
+ scaledValue
61
+ ];
62
+ const step = 10n ** BigInt(scale - precision);
63
+ if ("truncate" === roundingRule) if (scaledValue > 0n) return [
64
+ scaledValue,
65
+ scaledValue + step - 1n
66
+ ];
67
+ else if (scaledValue < 0n) return [
68
+ scaledValue - step + 1n,
69
+ scaledValue
70
+ ];
71
+ else return [
72
+ -step + 1n,
73
+ step - 1n
74
+ ];
75
+ {
76
+ const half = step / 2n;
77
+ if (scaledValue > 0n) return [
78
+ scaledValue - half,
79
+ scaledValue + half - 1n
80
+ ];
81
+ if (scaledValue < 0n) return [
82
+ scaledValue - half + 1n,
83
+ scaledValue + half
84
+ ];
85
+ return [
86
+ -half + 1n,
87
+ half - 1n
88
+ ];
89
+ }
90
+ };
91
+ const scaledDecimalFilterAsQuery = ({ column, op, value }, options)=>{
92
+ const { scale } = options;
93
+ const [from, to] = getScaledDecimalMatchRange(toDecimalString(value, scale), options);
94
+ const lo = fromScaledInteger(from, scale);
95
+ const hi = fromScaledInteger(to, scale);
96
+ if (from === to) return `${column} ${op} ${lo}`;
97
+ switch(op){
98
+ case "=":
99
+ return `(${column} >= ${lo} and ${column} <= ${hi})`;
100
+ case "!=":
101
+ return `(${column} < ${lo} or ${column} > ${hi})`;
102
+ case ">":
103
+ return `${column} > ${hi}`;
104
+ case ">=":
105
+ return `${column} >= ${lo}`;
106
+ case "<":
107
+ return `${column} < ${lo}`;
108
+ case "<=":
109
+ return `${column} <= ${hi}`;
110
+ default:
111
+ throw Error(`[scaled-decimal-filter] operator ${op} is not supported for scaled decimals`);
112
+ }
113
+ };
114
+ const getScaledDecimalCellFilter = (column, value)=>{
115
+ const options = getScaledDecimalFilterOptions(column);
116
+ if (void 0 === options || "string" != typeof value || "" === value) return;
117
+ const { decimals, roundingRule } = options;
118
+ if (void 0 === decimals) return {
119
+ label: value,
120
+ op: "=",
121
+ value
122
+ };
123
+ const display = (useLocaleString)=>roundScaledDecimal(value, void 0, decimals, true, false, useLocaleString, roundingRule);
124
+ return {
125
+ label: display(true),
126
+ op: "=",
127
+ value: display(false)
128
+ };
129
+ };
130
+ export { fromScaledInteger, getScaledDecimalCellFilter, getScaledDecimalFilterOptions, getScaledDecimalMatchRange, getScaledDecimalScale, isScaledDecimalFilterOptions, isValidDecimalString, scaledDecimalFilterAsQuery, toDecimalString, toScaledInteger };
package/src/form-utils.js CHANGED
@@ -4,6 +4,7 @@ import { getTemporalInfo, parseTemporalInput } from "./date/temporal.js";
4
4
  import { getDefaultTimeZone } from "./date/time-zone.js";
5
5
  import { isTypeDescriptor } from "./column-utils.js";
6
6
  import { queryClosest } from "./html-utils.js";
7
+ import { isValidDecimalString } from "./filters/scaled-decimal-filter.js";
7
8
  import { ScaledDecimal2, ScaledDecimal4, ScaledDecimal6, ScaledDecimal8 } from "./ScaledDecimal.js";
8
9
  class DataValidationError extends Error {
9
10
  actualType;
@@ -33,6 +34,12 @@ function getTypedRange([value1, value2], dataType, options) {
33
34
  getTypedValue(value2, dataType, false, options)
34
35
  ];
35
36
  }
37
+ const ScaledDecimalFactory = {
38
+ scaleddecimal2: ScaledDecimal2,
39
+ scaleddecimal4: ScaledDecimal4,
40
+ scaleddecimal6: ScaledDecimal6,
41
+ scaleddecimal8: ScaledDecimal8
42
+ };
36
43
  const getActualType = (value)=>{
37
44
  if ("boolean" == typeof value) return "boolean";
38
45
  if ("number" == typeof value) return "number";
@@ -53,13 +60,12 @@ function getTypedValue(value, type, throwIfInvalid = false, options) {
53
60
  if (!throwIfInvalid) return;
54
61
  throw new DataValidationError(`value ${value} is not a valid ${type}`, "single character", getActualType(value));
55
62
  case "scaleddecimal2":
56
- return ScaledDecimal2(value);
57
63
  case "scaleddecimal4":
58
- return ScaledDecimal4(value);
59
64
  case "scaleddecimal6":
60
- return ScaledDecimal6(value);
61
65
  case "scaleddecimal8":
62
- return ScaledDecimal8(value);
66
+ if ("" === value || isValidDecimalString(value)) return ScaledDecimalFactory[type](value);
67
+ if (!throwIfInvalid) return;
68
+ throw new DataValidationError(`value ${value} is not a valid ${type}`, "decimal", getActualType(value));
63
69
  case "double":
64
70
  case "number":
65
71
  if (stringIsValidDecimal(value)) return parseFloat(value);
@@ -5,7 +5,26 @@ const integerFormat = new Intl.NumberFormat();
5
5
  const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER.toString();
6
6
  const MAX_INTEGER_DIGITS = MAX_SAFE_INTEGER.length;
7
7
  const exceedsMaxSafeInteger = (value)=>value.length > MAX_INTEGER_DIGITS || value.length === MAX_INTEGER_DIGITS && value > MAX_SAFE_INTEGER;
8
- const carriesIntoIntegral = (fraction, decimals, roundingRule)=>"round" === roundingRule && decimals !== DECIMALS_AUTO && fraction.length > decimals && parseFloat(`0.${fraction}`).toFixed(decimals).startsWith("1");
8
+ const roundFraction = (fraction, decimals)=>{
9
+ const kept = fraction.slice(0, decimals);
10
+ if (fraction.length <= decimals || fraction[decimals] < "5") return [
11
+ kept,
12
+ false
13
+ ];
14
+ if (0 === decimals) return [
15
+ "",
16
+ true
17
+ ];
18
+ const rounded = (BigInt(kept) + 1n).toString().padStart(decimals, "0");
19
+ return rounded.length > decimals ? [
20
+ rounded.slice(1),
21
+ true
22
+ ] : [
23
+ rounded,
24
+ false
25
+ ];
26
+ };
27
+ const carriesIntoIntegral = (fraction, decimals, roundingRule)=>"round" === roundingRule && decimals !== DECIMALS_AUTO && fraction.length > decimals && roundFraction(fraction, decimals)[1];
9
28
  const incrementIntegral = (integral)=>{
10
29
  const negative = integral.startsWith("-");
11
30
  const magnitude = BigInt(negative ? integral.slice(1) : integral) + 1n;
@@ -73,7 +92,10 @@ function roundDecimal(value, align = Align.Right, decimals = 4, zeroPad, alignOn
73
92
  const actualDecimals = part2.length;
74
93
  integral = "-0" === part1 ? "-0" : useLocaleString ? integerFormat.format(parseFloat(part1)) : parseFloat(part1).toString();
75
94
  if (align === Align.Left && alignOnDecimals) integral = padLeft(integral);
76
- fraction = decimals === DECIMALS_AUTO || actualDecimals === decimals ? part2 : actualDecimals > decimals ? "round" === roundingRule ? parseFloat("0." + part2).toFixed(decimals).slice(2) : part2.slice(0, decimals) : (Pad = zeroPad ? Zero : alignOnDecimals && align !== Align.Left ? Space : null) ? 0 === actualDecimals ? Pad.FULL_PADDING[decimals] ?? "" : pad(part2, decimals, Pad) : part2;
95
+ if (decimals === DECIMALS_AUTO || actualDecimals === decimals) fraction = part2;
96
+ else if (actualDecimals > decimals) if ("round" === roundingRule) [fraction] = roundFraction(part2, decimals);
97
+ else fraction = part2.slice(0, decimals);
98
+ else fraction = (Pad = zeroPad ? Zero : alignOnDecimals && align !== Align.Left ? Space : null) ? 0 === actualDecimals ? Pad.FULL_PADDING[decimals] ?? "" : pad(part2, decimals, Pad) : part2;
77
99
  return integral + (fraction ? "." + fraction : "");
78
100
  }
79
101
  function roundScaledDecimal(value, align = Align.Right, decimals = 4, zeroPad, alignOnDecimals, useLocaleString = true, roundingRule = "round") {
@@ -86,7 +108,10 @@ function roundScaledDecimal(value, align = Align.Right, decimals = 4, zeroPad, a
86
108
  const actualDecimals = part2.length;
87
109
  integral = "" === part1 ? "0" : "-0" === part1 ? "-0" : useLocaleString ? exceedsMaxSafeInteger(value) ? integerFormat.format(BigInt(part1)) : integerFormat.format(parseFloat(part1)) : part1;
88
110
  if (align === Align.Left && alignOnDecimals) integral = padLeft(integral);
89
- fraction = decimals === DECIMALS_AUTO || actualDecimals === decimals ? part2 : actualDecimals > decimals ? "round" === roundingRule ? parseFloat("0." + part2).toFixed(decimals).slice(2) : part2.slice(0, decimals) : (Pad = zeroPad ? Zero : alignOnDecimals && align !== Align.Left ? Space : null) ? 0 === actualDecimals ? Pad.FULL_PADDING[decimals] ?? "" : pad(part2, decimals, Pad) : part2;
111
+ if (decimals === DECIMALS_AUTO || actualDecimals === decimals) fraction = part2;
112
+ else if (actualDecimals > decimals) if ("round" === roundingRule) [fraction] = roundFraction(part2, decimals);
113
+ else fraction = part2.slice(0, decimals);
114
+ else fraction = (Pad = zeroPad ? Zero : alignOnDecimals && align !== Align.Left ? Space : null) ? 0 === actualDecimals ? Pad.FULL_PADDING[decimals] ?? "" : pad(part2, decimals, Pad) : part2;
90
115
  return integral + (fraction ? "." + fraction : "");
91
116
  }
92
117
  export { exceedsMaxSafeInteger, roundDecimal, roundScaledDecimal };
@@ -43,7 +43,8 @@ export declare const getColumnsByName: (columns: readonly ColumnDescriptor[]) =>
43
43
  export declare const getColumnsByNameForFilter: (tableSchema?: Pick<TableSchema, "columns">, columnsByName?: ColumnDescriptorsByName) => ColumnDescriptorsByName | undefined;
44
44
  /**
45
45
  * Returns true if the filter includes clauses whose meaning depends on the
46
- * column type (temporal columns) or which serialize themselves. Such filters
47
- * must be resolved (see resolveFilterQuery) before they can be evaluated locally.
46
+ * column type (temporal and scaled decimal columns) or which serialize
47
+ * themselves. Such filters must be resolved (see resolveFilterQuery) before
48
+ * they can be evaluated locally.
48
49
  */
49
50
  export declare const filterRequiresResolution: (filter: Filter, columnsByName?: ColumnDescriptorsByName) => boolean;
@@ -1,3 +1,4 @@
1
1
  export * from "./filter-utils";
2
2
  export * from "./filterAsQuery";
3
3
  export * from "./temporal-cell-filter";
4
+ export * from "./scaled-decimal-filter";
@@ -0,0 +1,58 @@
1
+ import type { ExtendedFilterOptions, ScaledDecimalFilterOptions, SingleValueFilterClause } from "@vuu-ui/vuu-filter-types";
2
+ import type { DataValueDescriptor } from "@vuu-ui/vuu-data-types";
3
+ export type ScaledDecimalScale = ScaledDecimalFilterOptions["scale"];
4
+ export declare const getScaledDecimalScale: (serverDataType?: string) => ScaledDecimalScale | undefined;
5
+ export declare const isValidDecimalString: (value: string) => boolean;
6
+ export declare const isScaledDecimalFilterOptions: (options?: ExtendedFilterOptions) => options is ScaledDecimalFilterOptions;
7
+ /**
8
+ * A decimal (not exponential) string representation of a filter value.
9
+ */
10
+ export declare const toDecimalString: (value: unknown, scale: ScaledDecimalScale) => string;
11
+ /**
12
+ * Convert a decimal string to its scaled integer representation. Digits beyond
13
+ * the scale are truncated, as the Vuu server does.
14
+ */
15
+ export declare const toScaledInteger: (value: string, scale: ScaledDecimalScale) => bigint;
16
+ /**
17
+ * Convert a scaled integer to a decimal string with 'scale' decimal places,
18
+ * the literal form in which scaled decimals are expressed in a filter query.
19
+ */
20
+ export declare const fromScaledInteger: (value: bigint, scale: ScaledDecimalScale) => string;
21
+ /**
22
+ * The filter options for a clause on a scaled decimal column, describing the
23
+ * precision with which the column displays values. These mirror the formatting
24
+ * rules of numericFormatter: a column with no type descriptor displays the full
25
+ * value, a type descriptor with no decimals displays 4 decimal places.
26
+ */
27
+ export declare const getScaledDecimalFilterOptions: (column: Pick<DataValueDescriptor, "serverDataType" | "type">) => ScaledDecimalFilterOptions | undefined;
28
+ /**
29
+ * The range of scaled integer values, inclusive at both ends, matched by a
30
+ * filter value. A value with no more decimal places than are displayed matches
31
+ * every value that displays as that value, e.g 1.23, displayed with 2 decimals,
32
+ * rounded, matches [1.225, 1.235). Display rounding applies to the magnitude,
33
+ * so -1.23 matches (-1.235, -1.225]. A value with more decimal places than are
34
+ * displayed is matched at its own precision, by the same rounding rule, e.g
35
+ * 1.234 matches [1.2335, 1.2345). A value with as many decimal places as the
36
+ * column holds, or any value where every decimal is displayed, is matched exactly.
37
+ */
38
+ export declare const getScaledDecimalMatchRange: (value: string, { scale, decimals, roundingRule }: ScaledDecimalFilterOptions) => [bigint, bigint];
39
+ /**
40
+ * Serialize a filter clause on a scaled decimal column. Where the value matches
41
+ * a range of values (see getScaledDecimalMatchRange), each operator applies to
42
+ * that range, e.g '>' matches values greater than every value in the range.
43
+ * Note: '!=' on a range is expressed as '< from or > to', which, unlike an
44
+ * exact '!=', does not match a null value.
45
+ */
46
+ export declare const scaledDecimalFilterAsQuery: ({ column, op, value, }: Pick<SingleValueFilterClause, "column" | "op" | "value">, options: ScaledDecimalFilterOptions) => string;
47
+ export interface ScaledDecimalCellFilter {
48
+ /** The value as displayed */
49
+ label: string;
50
+ op: "=";
51
+ value: string;
52
+ }
53
+ /**
54
+ * The filter for a cell value on a scaled decimal column, matching the value as
55
+ * displayed, e.g a cell displaying 1.23 (underlying value 1.234567) creates a
56
+ * filter matching every value that displays as 1.23.
57
+ */
58
+ export declare const getScaledDecimalCellFilter: (column: Pick<DataValueDescriptor, "serverDataType" | "type">, value: unknown) => ScaledDecimalCellFilter | undefined;