@vuu-ui/vuu-utils 4.0.0-alpha.2 → 4.0.0-alpha.20

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 (41) hide show
  1. package/package.json +8 -7
  2. package/src/array-utils.js +17 -5
  3. package/src/column-utils.js +21 -6
  4. package/src/component-registry.js +2 -1
  5. package/src/data-utils.js +1 -0
  6. package/src/datasource/BaseDataSource.js +5 -3
  7. package/src/date/EpochTimestamp.js +85 -0
  8. package/src/date/date-utils.js +50 -18
  9. package/src/date/dateTimePattern.js +33 -5
  10. package/src/date/formatter.js +54 -16
  11. package/src/date/index.js +5 -2
  12. package/src/date/temporal.js +131 -0
  13. package/src/date/time-zone.js +56 -0
  14. package/src/filters/filter-utils.js +12 -6
  15. package/src/filters/filterAsQuery.js +107 -45
  16. package/src/filters/index.js +1 -0
  17. package/src/filters/temporal-cell-filter.js +63 -0
  18. package/src/form-utils.js +44 -6
  19. package/src/formatting-utils.js +46 -25
  20. package/src/html-utils.js +35 -1
  21. package/src/keyset.js +15 -9
  22. package/src/useResizeObserver.js +3 -3
  23. package/types/array-utils.d.ts +7 -4
  24. package/types/column-utils.d.ts +43 -40
  25. package/types/component-registry.d.ts +4 -4
  26. package/types/datasource/BaseDataSource.d.ts +2 -2
  27. package/types/date/EpochTimestamp.d.ts +66 -0
  28. package/types/date/date-utils.d.ts +34 -9
  29. package/types/date/dateTimePattern.d.ts +20 -2
  30. package/types/date/formatter.d.ts +26 -1
  31. package/types/date/index.d.ts +6 -2
  32. package/types/date/temporal.d.ts +68 -0
  33. package/types/date/time-zone.d.ts +52 -0
  34. package/types/filters/filter-utils.d.ts +6 -1
  35. package/types/filters/filterAsQuery.d.ts +41 -0
  36. package/types/filters/index.d.ts +1 -0
  37. package/types/filters/temporal-cell-filter.d.ts +27 -0
  38. package/types/form-utils.d.ts +19 -0
  39. package/types/formatting-utils.d.ts +23 -1
  40. package/types/html-utils.d.ts +16 -0
  41. package/types/keyset.d.ts +11 -1
@@ -0,0 +1,131 @@
1
+ import { EpochTimestamp } from "./EpochTimestamp.js";
2
+ import { getDefaultTimeZone, resolveRelativeDate } from "./time-zone.js";
3
+ const temporalKindByTypeName = {
4
+ "date/time": "datetime",
5
+ date: "date",
6
+ time: "time"
7
+ };
8
+ const isTemporalTypeName = (typeName)=>"date/time" === typeName || "date" === typeName || "time" === typeName;
9
+ const getTypeName = ({ type })=>"string" == typeof type ? type : type?.name;
10
+ const getTemporalInfo = (descriptor)=>{
11
+ if (void 0 === descriptor) return;
12
+ const { serverDataType, type } = descriptor;
13
+ const typeName = getTypeName(descriptor);
14
+ const kindFromType = isTemporalTypeName(typeName) ? temporalKindByTypeName[typeName] : void 0;
15
+ let encoding;
16
+ switch(serverDataType){
17
+ case "epochtimestamp":
18
+ encoding = "number" === typeName ? void 0 : "epochMillis";
19
+ break;
20
+ case "epochtimestampnano":
21
+ encoding = "number" === typeName ? void 0 : "epochNanos";
22
+ break;
23
+ case void 0:
24
+ case "long":
25
+ case "int":
26
+ case "double":
27
+ encoding = kindFromType ? "epochMillis" : void 0;
28
+ break;
29
+ }
30
+ if (void 0 === encoding) return;
31
+ const timeZone = ("object" == typeof type ? type.formatting?.timeZone : void 0) ?? getDefaultTimeZone();
32
+ return {
33
+ kind: kindFromType ?? "datetime",
34
+ encoding,
35
+ precision: "epochNanos" === encoding ? "ns" : "ms",
36
+ timeZone
37
+ };
38
+ };
39
+ const isTemporal = (descriptor)=>void 0 !== getTemporalInfo(descriptor);
40
+ const defaultFractionalSecondDigits = (info)=>"ns" === info.precision ? 9 : 3;
41
+ const pad2 = (n)=>`${n}`.padStart(2, "0");
42
+ const formatTemporalInput = (value, { kind, precision, timeZone })=>{
43
+ if (void 0 === value) return "";
44
+ const { year, month, day, hour, minute, second } = value.getFields(timeZone);
45
+ const date = `${year}-${pad2(month)}-${pad2(day)}`;
46
+ if ("date" === kind) return date;
47
+ const fraction = value.fractionalSecond("ns" === precision ? 9 : 3).replace(/0+$/, "");
48
+ const time = `${pad2(hour)}:${pad2(minute)}:${pad2(second)}${fraction ? `.${fraction}` : ""}`;
49
+ return "time" === kind ? time : `${date} ${time}`;
50
+ };
51
+ const dateTimeInputPattern = /^\s*(\d{4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2}):(\d{2})(?::(\d{2})(?:\.(\d{1,9}))?)?)?\s*$/;
52
+ const timeInputPattern = /^\s*(\d{1,2}):(\d{2})(?::(\d{2})(?:\.(\d{1,9}))?)?\s*$/;
53
+ const epochDigitsPattern = /^\s*-?\d{9,}\s*$/;
54
+ const daysInMonth = (year, month)=>new Date(Date.UTC(year, month, 0)).getUTCDate();
55
+ const fractionToMillisAndNanos = (fraction = "")=>{
56
+ const nine = fraction.padEnd(9, "0");
57
+ return [
58
+ parseInt(nine.slice(0, 3), 10),
59
+ parseInt(nine.slice(3), 10)
60
+ ];
61
+ };
62
+ const parseTemporalInput = (input, { kind, encoding, precision, timeZone }, baseValue)=>{
63
+ if (epochDigitsPattern.test(input)) return EpochTimestamp.fromWire(input.trim(), encoding);
64
+ const base = baseValue?.getFields(timeZone);
65
+ const keepNanos = "ns" === precision;
66
+ if ("time" === kind) {
67
+ const match = timeInputPattern.exec(input);
68
+ if (match) {
69
+ const [, h, m, s = "0", fraction] = match;
70
+ const hour = parseInt(h, 10);
71
+ const minute = parseInt(m, 10);
72
+ const second = parseInt(s, 10);
73
+ if (hour > 23 || minute > 59 || second > 59) return;
74
+ const [millisecond, nanos] = fractionToMillisAndNanos(fraction);
75
+ const { year, month, day } = base ?? resolveRelativeDate("today", timeZone);
76
+ return EpochTimestamp.fromFields({
77
+ year,
78
+ month,
79
+ day,
80
+ hour,
81
+ minute,
82
+ second,
83
+ millisecond
84
+ }, timeZone, keepNanos ? nanos : 0);
85
+ }
86
+ return;
87
+ }
88
+ const match = dateTimeInputPattern.exec(input);
89
+ if (match) {
90
+ const [, y, mo, d, h, mi, s = "0", fraction] = match;
91
+ const year = parseInt(y, 10);
92
+ const month = parseInt(mo, 10);
93
+ const day = parseInt(d, 10);
94
+ if (month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)) return;
95
+ if (void 0 === h) {
96
+ if ("date" === kind && base && baseValue) {
97
+ const { hour, minute, second, millisecond } = base;
98
+ return EpochTimestamp.fromFields({
99
+ year,
100
+ month,
101
+ day,
102
+ hour,
103
+ minute,
104
+ second,
105
+ millisecond
106
+ }, timeZone, baseValue.subMilliNanos);
107
+ }
108
+ return EpochTimestamp.fromFields({
109
+ year,
110
+ month,
111
+ day
112
+ }, timeZone);
113
+ }
114
+ const hour = parseInt(h, 10);
115
+ const minute = parseInt(mi, 10);
116
+ const second = parseInt(s, 10);
117
+ if (hour > 23 || minute > 59 || second > 59) return;
118
+ const [millisecond, nanos] = fractionToMillisAndNanos(fraction);
119
+ return EpochTimestamp.fromFields({
120
+ year,
121
+ month,
122
+ day,
123
+ hour,
124
+ minute,
125
+ second,
126
+ millisecond
127
+ }, timeZone, keepNanos ? nanos : 0);
128
+ }
129
+ };
130
+ const temporalInputPlaceholder = (kind)=>"date" === kind ? "yyyy-mm-dd" : "time" === kind ? "hh:mm:ss" : "yyyy-mm-dd hh:mm:ss";
131
+ export { defaultFractionalSecondDigits, formatTemporalInput, getTemporalInfo, isTemporal, isTemporalTypeName, parseTemporalInput, temporalInputPlaceholder };
@@ -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" === column.type) return [
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 { isDateTimeDataValue } from "../column-utils.js";
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 `${f.column} ${f.op} [${f.values.map(quotedStrings).join(",")}]`;
28
- return singleValueFilterAsQuery(f, opts);
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
- if (column && isDateTimeDataValue(column)) return dateFilterAsQuery(f, opts);
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
- function dateFilterAsQuery(f, opts) {
40
- switch(f.op){
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
- const filters = [
63
- {
64
- op: "<",
65
- column: f.column,
66
- value: f.value
67
- },
68
- {
69
- op: ">=",
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 defaultSingleValueFilterAsQuery(f, opts);
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
- export { ONE_DAY_IN_MILLIS, dateFilterAsQuery, filterAsQuery };
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 };
@@ -1,2 +1,3 @@
1
1
  export * from "./filter-utils.js";
2
2
  export * from "./filterAsQuery.js";
3
+ export * from "./temporal-cell-filter.js";
@@ -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/index.js";
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
- if (isValidTimeString(val1) && isValidTimeString(val2)) return val2 > val1;
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) {
@@ -38,11 +43,15 @@ function getTypedValue(value, type, throwIfInvalid = false, options) {
38
43
  case "int":
39
44
  if (stringIsValidInt(value)) return parseInt(value, 10);
40
45
  if (!throwIfInvalid) return;
41
- throw Error(`value ${value} is not a valid ${type}`);
46
+ throw new DataValidationError(`value ${value} is not a valid ${type}`, "whole number", getActualType(value));
42
47
  case "long":
43
48
  if (stringIsValidLong(value)) return value;
44
49
  if (!throwIfInvalid) return;
45
- throw Error(`value ${value} is not a valid ${type}`);
50
+ throw new DataValidationError(`value ${value} is not a valid ${type}`, "whole number", getActualType(value));
51
+ case "char":
52
+ if (1 === value.length) return value;
53
+ if (!throwIfInvalid) return;
54
+ throw new DataValidationError(`value ${value} is not a valid ${type}`, "single character", getActualType(value));
46
55
  case "scaleddecimal2":
47
56
  return ScaledDecimal2(value);
48
57
  case "scaleddecimal4":
@@ -58,8 +67,24 @@ function getTypedValue(value, type, throwIfInvalid = false, options) {
58
67
  throw new DataValidationError(`value ${value} is not a valid ${type}`, "decimal", getActualType(value));
59
68
  case "boolean":
60
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
+ }
61
86
  case "time":
62
- if (isValidTimeString(value)) if (options?.type === "TimeString") return value;
87
+ if (isValidTimeString(value) || isValidTimeStringMillis(value)) if (options?.type === "TimeString") return value;
63
88
  else return +Time(value).asDate();
64
89
  if (value.length > 0 && Time.isDateInMillis(value)) return Number(value);
65
90
  if (!throwIfInvalid) return;
@@ -68,4 +93,17 @@ function getTypedValue(value, type, throwIfInvalid = false, options) {
68
93
  return value;
69
94
  }
70
95
  }
71
- export { DataValidationError, getFieldName, getTypedRange, getTypedValue, isNumber, isValidRange };
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 };
@@ -1,31 +1,51 @@
1
- import { isDateTimeDataValue, isMappedValueTypeRenderer, isNanoTimestampColumn, isTimeDataValue, isTypeDescriptor } from "./column-utils.js";
2
- import { dateTimePattern, formatDate } from "./date/index.js";
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 dateFormatter = (column)=>{
8
- if (isNanoTimestampColumn(column)) {
9
- const pattern = dateTimePattern(column.type);
10
- const formatter = formatDate(pattern);
11
- return (value)=>{
12
- if ("string" != typeof value || "" === value) return "";
13
- {
14
- const bigIntValue = BigInt(value);
15
- const dateTimeValue = Number(bigIntValue / 1000000n);
16
- const nanoSeconds = (bigIntValue % 1000000n).toString().padStart(9, "0");
17
- return `${formatter(new Date(dateTimeValue))}.${nanoSeconds}`;
18
- }
19
- };
20
- }
21
- {
22
- const pattern = dateTimePattern(column.type);
23
- const formatter = formatDate(pattern);
24
- return (value)=>{
25
- if ("number" == typeof value && 0 !== value) return formatter(new Date(value));
26
- return "";
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
- if (isDateTimeDataValue(column) || isTimeDataValue(column)) return dateFormatter(column);
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 };