@vuu-ui/vuu-utils 4.0.0-alpha.18 → 4.0.0-alpha.19
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 +6 -6
- package/src/column-utils.js +21 -6
- package/src/component-registry.js +2 -1
- package/src/data-utils.js +1 -0
- package/src/datasource/BaseDataSource.js +5 -3
- package/src/date/EpochTimestamp.js +85 -0
- package/src/date/date-utils.js +50 -18
- package/src/date/dateTimePattern.js +33 -5
- package/src/date/formatter.js +54 -16
- package/src/date/index.js +5 -2
- package/src/date/temporal.js +131 -0
- package/src/date/time-zone.js +56 -0
- package/src/filters/filter-utils.js +12 -6
- package/src/filters/filterAsQuery.js +107 -45
- package/src/filters/index.js +1 -0
- package/src/filters/temporal-cell-filter.js +63 -0
- package/src/form-utils.js +38 -4
- package/src/formatting-utils.js +46 -25
- package/src/html-utils.js +35 -1
- package/src/keyset.js +15 -9
- package/types/column-utils.d.ts +42 -7
- package/types/component-registry.d.ts +4 -4
- package/types/datasource/BaseDataSource.d.ts +2 -2
- package/types/date/EpochTimestamp.d.ts +66 -0
- package/types/date/date-utils.d.ts +34 -9
- package/types/date/dateTimePattern.d.ts +20 -2
- package/types/date/formatter.d.ts +26 -1
- package/types/date/index.d.ts +6 -2
- package/types/date/temporal.d.ts +68 -0
- package/types/date/time-zone.d.ts +52 -0
- package/types/filters/filter-utils.d.ts +6 -1
- package/types/filters/filterAsQuery.d.ts +41 -0
- package/types/filters/index.d.ts +1 -0
- package/types/filters/temporal-cell-filter.d.ts +27 -0
- package/types/form-utils.d.ts +19 -0
- package/types/formatting-utils.d.ts +23 -1
- package/types/html-utils.d.ts +16 -0
- package/types/keyset.d.ts +11 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { CalendarDate, CalendarDateTime, fromAbsolute, getLocalTimeZone, toCalendarDate, toZoned } from "@internationalized/date";
|
|
2
|
+
let defaultTimeZone = "local";
|
|
3
|
+
const setDefaultTimeZone = (timeZone)=>{
|
|
4
|
+
defaultTimeZone = timeZone;
|
|
5
|
+
};
|
|
6
|
+
const getDefaultTimeZone = ()=>defaultTimeZone;
|
|
7
|
+
const resolveTimeZone = (timeZone = defaultTimeZone)=>"local" === timeZone ? getLocalTimeZone() : timeZone;
|
|
8
|
+
const getDateTimeFields = (epochMillis, timeZone)=>{
|
|
9
|
+
const { year, month, day, hour, minute, second, millisecond } = fromAbsolute(epochMillis, resolveTimeZone(timeZone));
|
|
10
|
+
return {
|
|
11
|
+
year,
|
|
12
|
+
month,
|
|
13
|
+
day,
|
|
14
|
+
hour,
|
|
15
|
+
minute,
|
|
16
|
+
second,
|
|
17
|
+
millisecond
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
const fromDateTimeFields = ({ year, month, day, hour = 0, minute = 0, second = 0, millisecond = 0 }, timeZone)=>toZoned(new CalendarDateTime(year, month, day, hour, minute, second, millisecond), resolveTimeZone(timeZone), "compatible").toDate().getTime();
|
|
21
|
+
const calendarDateOf = (epochMillis, timeZone)=>toCalendarDate(fromAbsolute(epochMillis, resolveTimeZone(timeZone)));
|
|
22
|
+
const startOfCalendarDay = (date, timeZone)=>toZoned(date, resolveTimeZone(timeZone)).toDate().getTime();
|
|
23
|
+
const startOfDay = (epochMillis, timeZone)=>startOfCalendarDay(calendarDateOf(epochMillis, timeZone), timeZone);
|
|
24
|
+
const startOfNextDay = (epochMillis, timeZone)=>startOfCalendarDay(calendarDateOf(epochMillis, timeZone).add({
|
|
25
|
+
days: 1
|
|
26
|
+
}), timeZone);
|
|
27
|
+
const isStartOfDay = (epochMillis, timeZone)=>startOfDay(epochMillis, timeZone) === epochMillis;
|
|
28
|
+
const isoDatePattern = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
29
|
+
const resolveRelativeDate = (date = "today", timeZone)=>{
|
|
30
|
+
if (date instanceof Date) return calendarDateOf(date.getTime(), timeZone);
|
|
31
|
+
{
|
|
32
|
+
if ("today" === date) return calendarDateOf(Date.now(), timeZone);
|
|
33
|
+
if ("yesterday" === date) return calendarDateOf(Date.now(), timeZone).subtract({
|
|
34
|
+
days: 1
|
|
35
|
+
});
|
|
36
|
+
const match = isoDatePattern.exec(date);
|
|
37
|
+
if (match) return new CalendarDate(parseInt(match[1]), parseInt(match[2]), parseInt(match[3]));
|
|
38
|
+
throw Error(`[time-zone] invalid date ${date}`);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const timeOfDayToEpochMillis = (timeString, date = "today", timeZone)=>{
|
|
42
|
+
const { year, month, day } = resolveRelativeDate(date, timeZone);
|
|
43
|
+
const [hms, fraction = ""] = timeString.split(".");
|
|
44
|
+
const [hour, minute, second] = hms.split(":").map((v)=>parseInt(v, 10));
|
|
45
|
+
const millisecond = parseInt(fraction.padEnd(3, "0").slice(0, 3), 10);
|
|
46
|
+
return fromDateTimeFields({
|
|
47
|
+
year,
|
|
48
|
+
month,
|
|
49
|
+
day,
|
|
50
|
+
hour,
|
|
51
|
+
minute,
|
|
52
|
+
second,
|
|
53
|
+
millisecond
|
|
54
|
+
}, timeZone);
|
|
55
|
+
};
|
|
56
|
+
export { fromDateTimeFields, getDateTimeFields, getDefaultTimeZone, isStartOfDay, resolveRelativeDate, resolveTimeZone, setDefaultTimeZone, startOfDay, startOfNextDay, timeOfDayToEpochMillis };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { ScaledDecimal } from "../ScaledDecimal.js";
|
|
2
|
+
import { isTimeDataValueWithMilliseconds } from "../column-utils.js";
|
|
3
|
+
import { getTemporalInfo } from "../date/temporal.js";
|
|
2
4
|
const singleValueFilterOps = new Set([
|
|
3
5
|
"=",
|
|
4
6
|
"!=",
|
|
@@ -64,6 +66,13 @@ const collectFiltersForColumn = (filter, columnName)=>{
|
|
|
64
66
|
};
|
|
65
67
|
};
|
|
66
68
|
const stringifyBoolean = (value)=>"boolean" == typeof value ? "${filter.value}" : value;
|
|
69
|
+
const getDefaultTimeRange = (column)=>isTimeDataValueWithMilliseconds(column) ? [
|
|
70
|
+
"00:00:00.000",
|
|
71
|
+
"23:59:59.999"
|
|
72
|
+
] : [
|
|
73
|
+
"00:00:00",
|
|
74
|
+
"23:59:59"
|
|
75
|
+
];
|
|
67
76
|
const getColumnValueFromFilter = (column, operator, filter)=>{
|
|
68
77
|
if (isSingleValueFilter(filter)) {
|
|
69
78
|
if (filter.column === column.name) {
|
|
@@ -72,7 +81,7 @@ const getColumnValueFromFilter = (column, operator, filter)=>{
|
|
|
72
81
|
`${filter.value}`,
|
|
73
82
|
""
|
|
74
83
|
];
|
|
75
|
-
else if ("<" === filter.op) return [
|
|
84
|
+
else if ("<" === filter.op || "<=" === filter.op) return [
|
|
76
85
|
"",
|
|
77
86
|
`${filter.value}`
|
|
78
87
|
];
|
|
@@ -97,10 +106,7 @@ const getColumnValueFromFilter = (column, operator, filter)=>{
|
|
|
97
106
|
}
|
|
98
107
|
}
|
|
99
108
|
if (!operator.startsWith("between")) return "";
|
|
100
|
-
if ("time"
|
|
101
|
-
"00:00:00",
|
|
102
|
-
"23:59:59"
|
|
103
|
-
];
|
|
109
|
+
if (getTemporalInfo(column)?.kind === "time") return getDefaultTimeRange(column);
|
|
104
110
|
return [
|
|
105
111
|
"",
|
|
106
112
|
""
|
|
@@ -133,4 +139,4 @@ const filtersAreEqual = (f1, f2)=>{
|
|
|
133
139
|
}
|
|
134
140
|
return false;
|
|
135
141
|
};
|
|
136
|
-
export { applyFilterToColumns, extractFilterForColumn, filtersAreEqual, getColumnValueFromFilter, isAndFilter, isBetweenFilter, isBetweenOperator, isCompleteFilter, isExtendedFilter, isFilter, isFilterClause, isFilteredColumn, isInFilter, isMultiClauseFilter, isMultiValueFilter, isNamedFilter, isOrFilter, isScaledDecimalFilterClause, isSerializableFilter, isSingleValueFilter, isValidFilterClauseOp, stripFilterFromColumns };
|
|
142
|
+
export { applyFilterToColumns, extractFilterForColumn, filtersAreEqual, getColumnValueFromFilter, getDefaultTimeRange, isAndFilter, isBetweenFilter, isBetweenOperator, isCompleteFilter, isExtendedFilter, isFilter, isFilterClause, isFilteredColumn, isInFilter, isMultiClauseFilter, isMultiValueFilter, isNamedFilter, isOrFilter, isScaledDecimalFilterClause, isSerializableFilter, isSingleValueFilter, isValidFilterClauseOp, stripFilterFromColumns };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { EpochTimestamp } from "../date/EpochTimestamp.js";
|
|
2
|
+
import { getTemporalInfo } from "../date/temporal.js";
|
|
3
|
+
import { isStartOfDay, startOfDay, startOfNextDay, timeOfDayToEpochMillis } from "../date/time-zone.js";
|
|
2
4
|
import { stringIsValidLong } from "../data-utils.js";
|
|
3
5
|
import { isMultiClauseFilter, isMultiValueFilter, isSerializableFilter } from "./filter-utils.js";
|
|
4
6
|
import { ScaledDecimal } from "../ScaledDecimal.js";
|
|
@@ -8,7 +10,9 @@ const filterValue = (value, columnType)=>{
|
|
|
8
10
|
const isNumericType = [
|
|
9
11
|
"int",
|
|
10
12
|
"long",
|
|
11
|
-
"double"
|
|
13
|
+
"double",
|
|
14
|
+
"epochtimestamp",
|
|
15
|
+
"epochtimestampnano"
|
|
12
16
|
].includes(columnType);
|
|
13
17
|
return isNumericType ? value : `"${value}"`;
|
|
14
18
|
}
|
|
@@ -24,64 +28,122 @@ const filterAsQueryCore = (f, opts)=>{
|
|
|
24
28
|
const multiClauseFilter = f.filters.map((filter)=>filterAsQueryCore(filter, opts)).join(` ${f.op} `);
|
|
25
29
|
return `(${multiClauseFilter})`;
|
|
26
30
|
}
|
|
27
|
-
if (isMultiValueFilter(f)) return
|
|
28
|
-
|
|
31
|
+
if (!isMultiValueFilter(f)) return singleValueFilterAsQuery(f, opts);
|
|
32
|
+
{
|
|
33
|
+
const column = opts?.columnsByName?.[f.column];
|
|
34
|
+
const temporalInfo = getTemporalInfo(column);
|
|
35
|
+
const values = f.values;
|
|
36
|
+
if (temporalInfo) {
|
|
37
|
+
const literals = values.map((v)=>EpochTimestamp.fromWire(v, temporalInfo.encoding)?.toLiteral(temporalInfo.encoding) ?? quotedStrings(v));
|
|
38
|
+
return `${f.column} ${f.op} [${literals.join(",")}]`;
|
|
39
|
+
}
|
|
40
|
+
return `${f.column} ${f.op} [${values.map(quotedStrings).join(",")}]`;
|
|
41
|
+
}
|
|
29
42
|
};
|
|
30
43
|
function singleValueFilterAsQuery(f, opts) {
|
|
31
44
|
if (isSerializableFilter(f)) return f.asQuery();
|
|
32
45
|
{
|
|
33
46
|
const column = opts?.columnsByName?.[f.column];
|
|
34
|
-
|
|
47
|
+
const temporalInfo = getTemporalInfo(column);
|
|
48
|
+
if (temporalInfo) return temporalFilterAsQuery(f, temporalInfo);
|
|
35
49
|
return defaultSingleValueFilterAsQuery(f, opts);
|
|
36
50
|
}
|
|
37
51
|
}
|
|
38
52
|
const ONE_DAY_IN_MILLIS = 86400000;
|
|
39
|
-
|
|
40
|
-
|
|
53
|
+
const timeOfDayPattern = /^\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?$/;
|
|
54
|
+
const NANOS_PER_SECOND = 1000000000;
|
|
55
|
+
const NANOS_PER_MILLI = 1000000;
|
|
56
|
+
const resolveTemporalFilterValue = (value, { encoding, kind, timeZone }, date = "today")=>{
|
|
57
|
+
if ("string" == typeof value && timeOfDayPattern.test(value)) {
|
|
58
|
+
const [hms, fraction = ""] = value.split(".");
|
|
59
|
+
const millis = timeOfDayToEpochMillis(hms, date, timeZone);
|
|
60
|
+
const nanos = parseInt(fraction.padEnd(9, "0"), 10);
|
|
61
|
+
const ts = EpochTimestamp.fromMillis(millis + Math.floor(nanos / NANOS_PER_MILLI), "epochNanos" === encoding ? nanos % NANOS_PER_MILLI : 0);
|
|
62
|
+
const period = 10 ** (9 - Math.min(fraction.length, 9));
|
|
63
|
+
const unit = "epochNanos" === encoding ? 1 : NANOS_PER_MILLI;
|
|
64
|
+
return [
|
|
65
|
+
ts,
|
|
66
|
+
period > unit ? period : "exact"
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
const ts = EpochTimestamp.fromWire(value instanceof ScaledDecimal ? value.asLong : value, encoding);
|
|
70
|
+
if (void 0 === ts) return [
|
|
71
|
+
void 0,
|
|
72
|
+
"exact"
|
|
73
|
+
];
|
|
74
|
+
const isWholeDay = 0 === ts.subMilliNanos && isStartOfDay(ts.epochMillis, timeZone);
|
|
75
|
+
if ("date" === kind || "datetime" === kind && isWholeDay) return [
|
|
76
|
+
ts,
|
|
77
|
+
"day"
|
|
78
|
+
];
|
|
79
|
+
if ("time" === kind && 0 === ts.subMilliNanos && ts.epochMillis % 1000 === 0) return [
|
|
80
|
+
ts,
|
|
81
|
+
NANOS_PER_SECOND
|
|
82
|
+
];
|
|
83
|
+
return [
|
|
84
|
+
ts,
|
|
85
|
+
"exact"
|
|
86
|
+
];
|
|
87
|
+
};
|
|
88
|
+
function temporalFilterAsQuery(f, temporalInfo, options) {
|
|
89
|
+
const { column, op } = f;
|
|
90
|
+
const { encoding, timeZone } = temporalInfo;
|
|
91
|
+
const [ts, granularity] = resolveTemporalFilterValue(f.value, temporalInfo, options?.date);
|
|
92
|
+
if (void 0 === ts) return `${column} ${op} ${filterValue(f.value, "long")}`;
|
|
93
|
+
if ("exact" === granularity) return `${column} ${op} ${ts.toLiteral(encoding)}`;
|
|
94
|
+
const [start, end] = "day" === granularity ? [
|
|
95
|
+
EpochTimestamp.fromMillis(startOfDay(ts.epochMillis, timeZone)),
|
|
96
|
+
EpochTimestamp.fromMillis(startOfNextDay(ts.epochMillis, timeZone))
|
|
97
|
+
] : [
|
|
98
|
+
ts,
|
|
99
|
+
EpochTimestamp.fromNanos(ts.epochNanos + BigInt(granularity))
|
|
100
|
+
];
|
|
101
|
+
const from = start.toLiteral(encoding);
|
|
102
|
+
const to = end.toLiteral(encoding);
|
|
103
|
+
switch(op){
|
|
41
104
|
case "=":
|
|
42
|
-
{
|
|
43
|
-
const filters = [
|
|
44
|
-
{
|
|
45
|
-
op: ">=",
|
|
46
|
-
column: f.column,
|
|
47
|
-
value: f.value
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
op: "<",
|
|
51
|
-
column: f.column,
|
|
52
|
-
value: f.value + ONE_DAY_IN_MILLIS
|
|
53
|
-
}
|
|
54
|
-
];
|
|
55
|
-
return filterAsQueryCore({
|
|
56
|
-
op: "and",
|
|
57
|
-
filters
|
|
58
|
-
}, opts);
|
|
59
|
-
}
|
|
105
|
+
return `(${column} >= ${from} and ${column} < ${to})`;
|
|
60
106
|
case "!=":
|
|
61
|
-
{
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
column: f.column,
|
|
71
|
-
value: f.value + ONE_DAY_IN_MILLIS
|
|
72
|
-
}
|
|
73
|
-
];
|
|
74
|
-
return filterAsQueryCore({
|
|
75
|
-
op: "or",
|
|
76
|
-
filters
|
|
77
|
-
}, opts);
|
|
78
|
-
}
|
|
107
|
+
return `(${column} < ${from} or ${column} >= ${to})`;
|
|
108
|
+
case ">":
|
|
109
|
+
return `${column} >= ${to}`;
|
|
110
|
+
case ">=":
|
|
111
|
+
return `${column} >= ${from}`;
|
|
112
|
+
case "<":
|
|
113
|
+
return `${column} < ${from}`;
|
|
114
|
+
case "<=":
|
|
115
|
+
return `${column} < ${to}`;
|
|
79
116
|
default:
|
|
80
|
-
return
|
|
117
|
+
return `${column} ${op} ${ts.toLiteral(encoding)}`;
|
|
81
118
|
}
|
|
82
119
|
}
|
|
120
|
+
function dateFilterAsQuery(f, opts) {
|
|
121
|
+
const temporalInfo = getTemporalInfo(opts?.columnsByName?.[f.column] ?? {
|
|
122
|
+
type: "date/time"
|
|
123
|
+
});
|
|
124
|
+
return temporalFilterAsQuery(f, temporalInfo);
|
|
125
|
+
}
|
|
83
126
|
const defaultSingleValueFilterAsQuery = (f, opts)=>{
|
|
84
127
|
const columnType = opts?.columnsByName?.[f.column]?.serverDataType;
|
|
85
128
|
return `${f.column} ${f.op} ${filterValue(f.value, columnType)}`;
|
|
86
129
|
};
|
|
87
|
-
|
|
130
|
+
const getColumnsByName = (columns)=>columns.reduce((map, column)=>{
|
|
131
|
+
map[column.name] = column;
|
|
132
|
+
return map;
|
|
133
|
+
}, {});
|
|
134
|
+
const getColumnsByNameForFilter = (tableSchema, columnsByName)=>{
|
|
135
|
+
if (void 0 === tableSchema) return columnsByName;
|
|
136
|
+
const result = {};
|
|
137
|
+
for (const column of tableSchema.columns)result[column.name] = {
|
|
138
|
+
...column,
|
|
139
|
+
...columnsByName?.[column.name]
|
|
140
|
+
};
|
|
141
|
+
if (columnsByName) for (const [name, column] of Object.entries(columnsByName))result[name] ??= column;
|
|
142
|
+
return result;
|
|
143
|
+
};
|
|
144
|
+
const filterRequiresResolution = (filter, columnsByName)=>{
|
|
145
|
+
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]);
|
|
148
|
+
};
|
|
149
|
+
export { ONE_DAY_IN_MILLIS, dateFilterAsQuery, filterAsQuery, filterRequiresResolution, getColumnsByName, getColumnsByNameForFilter, temporalFilterAsQuery };
|
package/src/filters/index.js
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { dateTimePattern, defaultPatternsByType } from "../date/dateTimePattern.js";
|
|
2
|
+
import { EpochTimestamp } from "../date/EpochTimestamp.js";
|
|
3
|
+
import { formatTimestamp } from "../date/formatter.js";
|
|
4
|
+
import { getTemporalInfo } from "../date/temporal.js";
|
|
5
|
+
import { startOfDay } from "../date/time-zone.js";
|
|
6
|
+
import { getFractionalSecondDigits, temporalFormatter } from "../formatting-utils.js";
|
|
7
|
+
const pad2 = (n)=>`${n}`.padStart(2, "0");
|
|
8
|
+
const getTemporalCellFilter = (column, value, columnFilter = false)=>{
|
|
9
|
+
const temporalInfo = getTemporalInfo(column);
|
|
10
|
+
if (void 0 === temporalInfo) return;
|
|
11
|
+
const { encoding, kind, timeZone } = temporalInfo;
|
|
12
|
+
const ts = EpochTimestamp.fromWire(value, encoding);
|
|
13
|
+
if (void 0 === ts) return;
|
|
14
|
+
const pattern = dateTimePattern(column.type, kind);
|
|
15
|
+
if ("date" === kind || columnFilter && "datetime" === kind || void 0 === pattern.time) {
|
|
16
|
+
const { date = defaultPatternsByType.date } = pattern;
|
|
17
|
+
const start = EpochTimestamp.fromMillis(startOfDay(ts.epochMillis, timeZone));
|
|
18
|
+
return {
|
|
19
|
+
label: formatTimestamp({
|
|
20
|
+
date
|
|
21
|
+
}, {
|
|
22
|
+
timeZone
|
|
23
|
+
})(ts),
|
|
24
|
+
op: "=",
|
|
25
|
+
value: `${start.toWire(encoding)}`
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const digits = columnFilter ? "hh:mm:ss.ms" === pattern.time ? 3 : 0 : Math.max(0, Math.min(9, getFractionalSecondDigits(column, temporalInfo)));
|
|
29
|
+
const unit = "epochNanos" === encoding ? 1n : 1000000n;
|
|
30
|
+
const period = 10n ** BigInt(9 - digits);
|
|
31
|
+
const startNanos = ts.epochNanos - ts.epochNanos % period;
|
|
32
|
+
const start = EpochTimestamp.fromNanos(startNanos);
|
|
33
|
+
const label = columnFilter ? formatTimestamp(pattern, {
|
|
34
|
+
fractionalSecondDigits: digits,
|
|
35
|
+
timeZone
|
|
36
|
+
})(start) : temporalFormatter(column, temporalInfo)(start.toWire(encoding));
|
|
37
|
+
if ("time" === kind) {
|
|
38
|
+
const { hour, minute, second } = ts.getFields(timeZone);
|
|
39
|
+
const fraction = ts.fractionalSecond(digits);
|
|
40
|
+
return {
|
|
41
|
+
label,
|
|
42
|
+
op: "=",
|
|
43
|
+
value: `${pad2(hour)}:${pad2(minute)}:${pad2(second)}${fraction ? `.${fraction}` : ""}`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
{
|
|
47
|
+
if (period <= unit) return {
|
|
48
|
+
label,
|
|
49
|
+
op: "=",
|
|
50
|
+
value: `${ts.toWire(encoding)}`
|
|
51
|
+
};
|
|
52
|
+
const end = EpochTimestamp.fromNanos(startNanos + period - unit);
|
|
53
|
+
return {
|
|
54
|
+
label,
|
|
55
|
+
op: "between-inclusive",
|
|
56
|
+
value: [
|
|
57
|
+
`${start.toWire(encoding)}`,
|
|
58
|
+
`${end.toWire(encoding)}`
|
|
59
|
+
]
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
export { getTemporalCellFilter };
|
package/src/form-utils.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { stringIsValidDecimal, stringIsValidInt, stringIsValidLong } from "./data-utils.js";
|
|
2
|
-
import { Time, isValidTimeString } from "./date/
|
|
2
|
+
import { Time, isValidTimeString, isValidTimeStringMillis, normaliseTimeString } from "./date/date-utils.js";
|
|
3
|
+
import { getTemporalInfo, parseTemporalInput } from "./date/temporal.js";
|
|
4
|
+
import { getDefaultTimeZone } from "./date/time-zone.js";
|
|
5
|
+
import { isTypeDescriptor } from "./column-utils.js";
|
|
3
6
|
import { queryClosest } from "./html-utils.js";
|
|
4
7
|
import { ScaledDecimal2, ScaledDecimal4, ScaledDecimal6, ScaledDecimal8 } from "./ScaledDecimal.js";
|
|
5
8
|
class DataValidationError extends Error {
|
|
@@ -19,7 +22,9 @@ const getFieldName = (target)=>{
|
|
|
19
22
|
};
|
|
20
23
|
const isNumber = (type, value)=>"number" === type;
|
|
21
24
|
const isValidRange = ([val1, val2])=>{
|
|
22
|
-
|
|
25
|
+
const time1 = normaliseTimeString(val1, true);
|
|
26
|
+
const time2 = normaliseTimeString(val2, true);
|
|
27
|
+
if (time1 && time2) return time2 > time1;
|
|
23
28
|
return true;
|
|
24
29
|
};
|
|
25
30
|
function getTypedRange([value1, value2], dataType, options) {
|
|
@@ -62,8 +67,24 @@ function getTypedValue(value, type, throwIfInvalid = false, options) {
|
|
|
62
67
|
throw new DataValidationError(`value ${value} is not a valid ${type}`, "decimal", getActualType(value));
|
|
63
68
|
case "boolean":
|
|
64
69
|
return "true" === value;
|
|
70
|
+
case "epochtimestamp":
|
|
71
|
+
case "epochtimestampnano":
|
|
72
|
+
case "date/time":
|
|
73
|
+
case "date":
|
|
74
|
+
{
|
|
75
|
+
const encoding = "epochtimestampnano" === type ? "epochNanos" : "epochMillis";
|
|
76
|
+
const timestamp = parseTemporalInput(value, {
|
|
77
|
+
kind: "date" === type ? "date" : "datetime",
|
|
78
|
+
encoding,
|
|
79
|
+
precision: "epochNanos" === encoding ? "ns" : "ms",
|
|
80
|
+
timeZone: getDefaultTimeZone()
|
|
81
|
+
});
|
|
82
|
+
if (timestamp) return timestamp.toWire(encoding);
|
|
83
|
+
if (!throwIfInvalid) return;
|
|
84
|
+
throw new DataValidationError(`value ${value} is not a valid ${type}`, "date/time", getActualType(value));
|
|
85
|
+
}
|
|
65
86
|
case "time":
|
|
66
|
-
if (isValidTimeString(value)) if (options?.type === "TimeString") return value;
|
|
87
|
+
if (isValidTimeString(value) || isValidTimeStringMillis(value)) if (options?.type === "TimeString") return value;
|
|
67
88
|
else return +Time(value).asDate();
|
|
68
89
|
if (value.length > 0 && Time.isDateInMillis(value)) return Number(value);
|
|
69
90
|
if (!throwIfInvalid) return;
|
|
@@ -72,4 +93,17 @@ function getTypedValue(value, type, throwIfInvalid = false, options) {
|
|
|
72
93
|
return value;
|
|
73
94
|
}
|
|
74
95
|
}
|
|
75
|
-
|
|
96
|
+
function getTypedTemporalValue(value, temporalInfo, throwIfInvalid = false, baseValue) {
|
|
97
|
+
if ("" === value) return;
|
|
98
|
+
const timestamp = parseTemporalInput(value, temporalInfo, baseValue);
|
|
99
|
+
if (timestamp) return timestamp.toWire(temporalInfo.encoding);
|
|
100
|
+
if (throwIfInvalid) throw new DataValidationError(`value ${value} is not a valid ${temporalInfo.kind}`, temporalInfo.kind, getActualType(value));
|
|
101
|
+
}
|
|
102
|
+
function getTypedValueForDescriptor(value, descriptor, throwIfInvalid = false) {
|
|
103
|
+
const temporalInfo = getTemporalInfo(descriptor);
|
|
104
|
+
if (temporalInfo) return getTypedTemporalValue(value, temporalInfo, throwIfInvalid);
|
|
105
|
+
const { serverDataType = "string", type } = descriptor;
|
|
106
|
+
const dataType = isTypeDescriptor(type) ? type.name : type ?? serverDataType;
|
|
107
|
+
return throwIfInvalid ? getTypedValue(value, dataType, true) : getTypedValue(value, dataType, false);
|
|
108
|
+
}
|
|
109
|
+
export { DataValidationError, getFieldName, getTypedRange, getTypedTemporalValue, getTypedValue, getTypedValueForDescriptor, isNumber, isValidRange };
|
package/src/formatting-utils.js
CHANGED
|
@@ -1,31 +1,51 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dateTimePattern,
|
|
1
|
+
import { isMappedValueTypeRenderer, isTypeDescriptor } from "./column-utils.js";
|
|
2
|
+
import { dateTimePattern, defaultPatternsByType } from "./date/dateTimePattern.js";
|
|
3
|
+
import { isStartOfDay } from "./date/time-zone.js";
|
|
4
|
+
import { EpochTimestamp } from "./date/EpochTimestamp.js";
|
|
5
|
+
import { formatTimestamp } from "./date/formatter.js";
|
|
6
|
+
import { getTemporalInfo } from "./date/temporal.js";
|
|
3
7
|
import { roundDecimal, roundScaledDecimal } from "./round-decimal.js";
|
|
4
8
|
import { isNumericType } from "./protocol-message-utils.js";
|
|
5
9
|
const DEFAULT_NUMERIC_FORMAT = {};
|
|
6
10
|
const defaultValueFormatter = (value)=>null == value ? "" : "string" == typeof value ? value : value.toString();
|
|
7
|
-
const
|
|
8
|
-
if (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
11
|
+
const getFractionalSecondDigits = (column, temporalInfo = getTemporalInfo(column))=>{
|
|
12
|
+
if (void 0 === temporalInfo) return 0;
|
|
13
|
+
const pattern = dateTimePattern(column.type, temporalInfo.kind);
|
|
14
|
+
const formatting = isTypeDescriptor(column.type) ? column.type.formatting : void 0;
|
|
15
|
+
return formatting?.fractionalSecondDigits ?? ("hh:mm:ss.ms" === pattern.time ? 3 : "ns" === temporalInfo.precision && pattern.time ? 9 : 0);
|
|
16
|
+
};
|
|
17
|
+
const temporalFormatter = (column, temporalInfo = getTemporalInfo(column))=>{
|
|
18
|
+
if (void 0 === temporalInfo) return defaultValueFormatter;
|
|
19
|
+
const { encoding, kind, timeZone } = temporalInfo;
|
|
20
|
+
const formatting = isTypeDescriptor(column.type) ? column.type.formatting ?? DEFAULT_NUMERIC_FORMAT : DEFAULT_NUMERIC_FORMAT;
|
|
21
|
+
const pattern = dateTimePattern(column.type, kind);
|
|
22
|
+
const fractionalSecondDigits = getFractionalSecondDigits(column, temporalInfo);
|
|
23
|
+
const formatter = formatTimestamp(pattern, {
|
|
24
|
+
fractionalSecondDigits,
|
|
25
|
+
locale: formatting.locale,
|
|
26
|
+
timeZone
|
|
27
|
+
});
|
|
28
|
+
return (value)=>{
|
|
29
|
+
const timestamp = EpochTimestamp.fromWire(value, encoding);
|
|
30
|
+
return timestamp ? formatter(timestamp) : "";
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
const timeOfDayPattern = /^\d{2}:\d{2}:\d{2}(\.\d{1,9})?$/;
|
|
34
|
+
const formatTemporalFilterValue = (value, column)=>{
|
|
35
|
+
const temporalInfo = getTemporalInfo(column);
|
|
36
|
+
if ("string" == typeof value && timeOfDayPattern.test(value)) return value;
|
|
37
|
+
if (void 0 === temporalInfo) return defaultValueFormatter(value);
|
|
38
|
+
const timestamp = EpochTimestamp.fromWire(value, temporalInfo.encoding);
|
|
39
|
+
if (void 0 === timestamp) return defaultValueFormatter(value);
|
|
40
|
+
if ("datetime" === temporalInfo.kind && 0 === timestamp.subMilliNanos && isStartOfDay(timestamp.epochMillis, temporalInfo.timeZone)) {
|
|
41
|
+
const { date = defaultPatternsByType.date } = dateTimePattern(column.type, "date");
|
|
42
|
+
return formatTimestamp({
|
|
43
|
+
date
|
|
44
|
+
}, {
|
|
45
|
+
timeZone: temporalInfo.timeZone
|
|
46
|
+
})(timestamp);
|
|
28
47
|
}
|
|
48
|
+
return temporalFormatter(column, temporalInfo)(value);
|
|
29
49
|
};
|
|
30
50
|
const numericFormatter = ({ align = "right", serverDataType, type })=>{
|
|
31
51
|
if (void 0 === type || "string" == typeof type) return defaultValueFormatter;
|
|
@@ -47,7 +67,8 @@ const NumericTypes = [
|
|
|
47
67
|
];
|
|
48
68
|
const getValueFormatter = (column)=>{
|
|
49
69
|
const { serverDataType = "string" } = column;
|
|
50
|
-
|
|
70
|
+
const temporalInfo = getTemporalInfo(column);
|
|
71
|
+
if (temporalInfo) return temporalFormatter(column, temporalInfo);
|
|
51
72
|
const { type } = column;
|
|
52
73
|
if (isTypeDescriptor(type) && isMappedValueTypeRenderer(type?.renderer)) return mapFormatter(type.renderer.map);
|
|
53
74
|
if (isNumericType(serverDataType) || isTypeDescriptor(type) && NumericTypes.includes(type.name)) return numericFormatter(column);
|
|
@@ -55,4 +76,4 @@ const getValueFormatter = (column)=>{
|
|
|
55
76
|
return defaultValueFormatter;
|
|
56
77
|
};
|
|
57
78
|
const lowerCase = (str)=>str.toLowerCase();
|
|
58
|
-
export { defaultValueFormatter, getValueFormatter, lowerCase, numericFormatter };
|
|
79
|
+
export { defaultValueFormatter, formatTemporalFilterValue, getFractionalSecondDigits, getValueFormatter, lowerCase, numericFormatter, temporalFormatter };
|
package/src/html-utils.js
CHANGED
|
@@ -81,6 +81,40 @@ function getScrollbarSize() {
|
|
|
81
81
|
}
|
|
82
82
|
return size;
|
|
83
83
|
}
|
|
84
|
+
const DEFAULT_MAX_SCROLL_HEIGHT = 33000000;
|
|
85
|
+
const MAX_PROBE_HEIGHT = 2 ** 26;
|
|
86
|
+
const maxScrollHeightByDevicePixelRatio = new Map();
|
|
87
|
+
function getMaxScrollHeight(doc = globalThis.document) {
|
|
88
|
+
if (doc?.body === void 0 || null === doc.body) return DEFAULT_MAX_SCROLL_HEIGHT;
|
|
89
|
+
const devicePixelRatio = doc.defaultView?.devicePixelRatio ?? 1;
|
|
90
|
+
const cachedValue = maxScrollHeightByDevicePixelRatio.get(devicePixelRatio);
|
|
91
|
+
if (void 0 !== cachedValue) return cachedValue;
|
|
92
|
+
const outer = doc.createElement("div");
|
|
93
|
+
outer.style.cssText = "position:absolute;top:-200px;left:-200px;width:50px;height:50px;overflow:scroll;visibility:hidden;";
|
|
94
|
+
const inner = doc.createElement("div");
|
|
95
|
+
inner.style.width = "1px";
|
|
96
|
+
outer.appendChild(inner);
|
|
97
|
+
doc.body.appendChild(outer);
|
|
98
|
+
const isHonoured = (height)=>{
|
|
99
|
+
inner.style.height = `${height}px`;
|
|
100
|
+
return outer.scrollHeight >= height - 1;
|
|
101
|
+
};
|
|
102
|
+
let lo = 0;
|
|
103
|
+
let hi = MAX_PROBE_HEIGHT;
|
|
104
|
+
while(hi - lo > 1){
|
|
105
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
106
|
+
if (isHonoured(mid)) lo = mid;
|
|
107
|
+
else hi = mid;
|
|
108
|
+
}
|
|
109
|
+
inner.style.height = `${lo}px`;
|
|
110
|
+
const maxScrollHeight = Math.min(lo, outer.scrollHeight);
|
|
111
|
+
outer.remove();
|
|
112
|
+
if (maxScrollHeight > 0) {
|
|
113
|
+
maxScrollHeightByDevicePixelRatio.set(devicePixelRatio, maxScrollHeight);
|
|
114
|
+
return maxScrollHeight;
|
|
115
|
+
}
|
|
116
|
+
return DEFAULT_MAX_SCROLL_HEIGHT;
|
|
117
|
+
}
|
|
84
118
|
const dispatchMouseEvent = (el, type)=>{
|
|
85
119
|
const evt = new MouseEvent(type, {
|
|
86
120
|
view: window,
|
|
@@ -105,4 +139,4 @@ const dispatchCustomEvent = (el, type)=>{
|
|
|
105
139
|
});
|
|
106
140
|
el.dispatchEvent(evt);
|
|
107
141
|
};
|
|
108
|
-
export { createEl, dispatchCustomEvent, dispatchKeyboardEvent, dispatchMouseEvent, focusFirstFocusableElement, getClosest, getClosestIndexItem, getElementByDataIndex, getElementDataIndex, getFocusableElement, getScrollbarSize, hasOpenOptionList, isDateInput, isInputElement, isSelectableElement, queryClosest };
|
|
142
|
+
export { DEFAULT_MAX_SCROLL_HEIGHT, createEl, dispatchCustomEvent, dispatchKeyboardEvent, dispatchMouseEvent, focusFirstFocusableElement, getClosest, getClosestIndexItem, getElementByDataIndex, getElementDataIndex, getFocusableElement, getMaxScrollHeight, getScrollbarSize, hasOpenOptionList, isDateInput, isInputElement, isSelectableElement, queryClosest };
|
package/src/keyset.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
const EMPTY = [];
|
|
2
2
|
class KeySet {
|
|
3
3
|
keys = new Map();
|
|
4
|
+
freeKeys = [];
|
|
4
5
|
nextKeyValue = 0;
|
|
5
6
|
range;
|
|
6
7
|
constructor(range){
|
|
7
|
-
this.range =
|
|
8
|
+
this.range = {
|
|
9
|
+
from: range.from,
|
|
10
|
+
to: range.to
|
|
11
|
+
};
|
|
8
12
|
this.init(range);
|
|
9
13
|
}
|
|
10
14
|
next(free = EMPTY) {
|
|
@@ -13,6 +17,7 @@ class KeySet {
|
|
|
13
17
|
}
|
|
14
18
|
init({ from, to }) {
|
|
15
19
|
this.keys.clear();
|
|
20
|
+
this.freeKeys.length = 0;
|
|
16
21
|
this.nextKeyValue = 0;
|
|
17
22
|
for(let rowIndex = from; rowIndex < to; rowIndex++){
|
|
18
23
|
const nextKeyValue = this.next();
|
|
@@ -20,23 +25,24 @@ class KeySet {
|
|
|
20
25
|
}
|
|
21
26
|
return true;
|
|
22
27
|
}
|
|
23
|
-
reset(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const freeKeys = [];
|
|
28
|
+
reset({ from, to }) {
|
|
29
|
+
this.range = {
|
|
30
|
+
from,
|
|
31
|
+
to
|
|
32
|
+
};
|
|
33
|
+
const { freeKeys } = this;
|
|
30
34
|
this.keys.forEach((keyValue, rowIndex)=>{
|
|
31
35
|
if (rowIndex < from || rowIndex >= to) {
|
|
32
36
|
freeKeys.push(keyValue);
|
|
33
37
|
this.keys.delete(rowIndex);
|
|
34
38
|
}
|
|
35
39
|
});
|
|
40
|
+
let freeKeyIndex = 0;
|
|
36
41
|
for(let rowIndex = from; rowIndex < to; rowIndex++)if (!this.keys.has(rowIndex)) {
|
|
37
|
-
const nextKeyValue = this.
|
|
42
|
+
const nextKeyValue = freeKeyIndex < freeKeys.length ? freeKeys[freeKeyIndex++] : this.nextKeyValue++;
|
|
38
43
|
this.keys.set(rowIndex, nextKeyValue);
|
|
39
44
|
}
|
|
45
|
+
if (freeKeyIndex > 0) freeKeys.splice(0, freeKeyIndex);
|
|
40
46
|
return false;
|
|
41
47
|
}
|
|
42
48
|
keyFor(rowIndex) {
|