@vuu-ui/vuu-utils 4.0.0-alpha.17 → 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.
Files changed (38) hide show
  1. package/package.json +6 -6
  2. package/src/column-utils.js +21 -6
  3. package/src/component-registry.js +2 -1
  4. package/src/data-utils.js +1 -0
  5. package/src/datasource/BaseDataSource.js +5 -3
  6. package/src/date/EpochTimestamp.js +85 -0
  7. package/src/date/date-utils.js +50 -18
  8. package/src/date/dateTimePattern.js +33 -5
  9. package/src/date/formatter.js +54 -16
  10. package/src/date/index.js +5 -2
  11. package/src/date/temporal.js +131 -0
  12. package/src/date/time-zone.js +56 -0
  13. package/src/filters/filter-utils.js +12 -6
  14. package/src/filters/filterAsQuery.js +107 -45
  15. package/src/filters/index.js +1 -0
  16. package/src/filters/temporal-cell-filter.js +63 -0
  17. package/src/form-utils.js +38 -4
  18. package/src/formatting-utils.js +46 -25
  19. package/src/html-utils.js +35 -1
  20. package/src/keyset.js +15 -9
  21. package/types/column-utils.d.ts +42 -7
  22. package/types/component-registry.d.ts +4 -4
  23. package/types/datasource/BaseDataSource.d.ts +2 -2
  24. package/types/date/EpochTimestamp.d.ts +66 -0
  25. package/types/date/date-utils.d.ts +34 -9
  26. package/types/date/dateTimePattern.d.ts +20 -2
  27. package/types/date/formatter.d.ts +26 -1
  28. package/types/date/index.d.ts +6 -2
  29. package/types/date/temporal.d.ts +68 -0
  30. package/types/date/time-zone.d.ts +52 -0
  31. package/types/filters/filter-utils.d.ts +6 -1
  32. package/types/filters/filterAsQuery.d.ts +41 -0
  33. package/types/filters/index.d.ts +1 -0
  34. package/types/filters/temporal-cell-filter.d.ts +27 -0
  35. package/types/form-utils.d.ts +19 -0
  36. package/types/formatting-utils.d.ts +23 -1
  37. package/types/html-utils.d.ts +16 -0
  38. package/types/keyset.d.ts +11 -1
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.0.0-alpha.17",
2
+ "version": "4.0.0-alpha.19",
3
3
  "author": "heswell",
4
4
  "main": "./src/index.js",
5
5
  "license": "Apache-2.0",
@@ -16,14 +16,14 @@
16
16
  },
17
17
  "devDependencies": {
18
18
  "@dnd-kit/react": "^0.1.21",
19
- "@vuu-ui/vuu-data-types": "4.0.0-alpha.17",
20
- "@vuu-ui/vuu-table-types": "4.0.0-alpha.17",
21
- "@vuu-ui/vuu-filter-types": "4.0.0-alpha.17",
22
- "@vuu-ui/vuu-protocol-types": "4.0.0-alpha.17"
19
+ "@vuu-ui/vuu-data-types": "4.0.0-alpha.19",
20
+ "@vuu-ui/vuu-table-types": "4.0.0-alpha.19",
21
+ "@vuu-ui/vuu-filter-types": "4.0.0-alpha.19",
22
+ "@vuu-ui/vuu-protocol-types": "4.0.0-alpha.19"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "@internationalized/date": "^3.0.0",
26
- "@vuu-ui/vuu-filter-parser": "4.0.0-alpha.17",
26
+ "@vuu-ui/vuu-filter-parser": "4.0.0-alpha.19",
27
27
  "clsx": "^2.0.0",
28
28
  "react": "^19.2.3",
29
29
  "react-dom": "^19.2.3"
@@ -1,4 +1,5 @@
1
1
  import { moveItem } from "./array-utils.js";
2
+ import { getTemporalInfo } from "./date/temporal.js";
2
3
  import { queryClosest } from "./html-utils.js";
3
4
  const NO_HEADINGS = [];
4
5
  const SORT_ASC = "asc";
@@ -77,22 +78,35 @@ const fromServerDataType = (serverDataType)=>{
77
78
  return "number";
78
79
  case "boolean":
79
80
  return "boolean";
81
+ case "epochtimestamp":
82
+ case "epochtimestampnano":
83
+ return "date/time";
80
84
  default:
81
85
  return "string";
82
86
  }
83
87
  };
84
88
  const isNanoTimestampColumn = ({ serverDataType })=>"epochtimestampnano" === serverDataType;
85
89
  const isTimestampColumn = (column)=>"epochtimestamp" === column.serverDataType || isNanoTimestampColumn(column);
86
- const isNumericColumn = ({ serverDataType, type })=>{
90
+ const isTemporalColumn = (column)=>void 0 !== getTemporalInfo(column);
91
+ const isNumericColumn = (column)=>{
92
+ const { serverDataType, type } = column;
93
+ if (isTemporalColumn(column)) return false;
87
94
  if ("int" === serverDataType || "long" === serverDataType || "double" === serverDataType) return true;
88
95
  if ("string" == typeof type) return "number" === type;
89
96
  if ("string" == typeof type?.name) return type?.name === "number";
90
97
  return false;
91
98
  };
92
- const isDateTimeDataType = (column)=>(isTypeDescriptor(column.type) ? column.type.name : column.type) === "date/time";
99
+ const isDateTimeDataType = (column)=>{
100
+ const typeName = isTypeDescriptor(column.type) ? column.type.name : column.type;
101
+ return "date/time" === typeName || "date" === typeName;
102
+ };
93
103
  const isTimeDataType = (column)=>(isTypeDescriptor(column.type) ? column.type.name : column.type) === "time";
94
- const isDateTimeDataValue = (column)=>isTimestampColumn(column) || isDateTimeDataType(column);
95
- const isTimeDataValue = (column)=>(isTypeDescriptor(column?.type) ? column.type.name : column?.type) === "time";
104
+ const isDateTimeDataValue = (column)=>{
105
+ const kind = getTemporalInfo(column)?.kind;
106
+ return "datetime" === kind || "date" === kind;
107
+ };
108
+ const isTimeDataValue = (column)=>getTemporalInfo(column)?.kind === "time";
109
+ const isTimeDataValueWithMilliseconds = (column)=>isTimeDataValue(column) && isTypeDescriptor(column.type) && column.type.formatting?.pattern?.time === "hh:mm:ss.ms";
96
110
  const isPinned = (column)=>"string" == typeof column.pin;
97
111
  const hasHeadings = (column)=>Array.isArray(column.heading) && column.heading.length > 0;
98
112
  const isResizing = (column)=>column.resizing;
@@ -538,6 +552,7 @@ const getDefaultColumnType = (serverDataType)=>{
538
552
  case "boolean":
539
553
  return "boolean";
540
554
  case "epochtimestamp":
555
+ case "epochtimestampnano":
541
556
  return "date/time";
542
557
  default:
543
558
  return "string";
@@ -596,7 +611,7 @@ const getTypeFormattingFromColumn = (column)=>{
596
611
  };
597
612
  const assertAllColumnsAreIncludedInSubscription = (columns, columnNames)=>{
598
613
  const unsubscribedColumns = [];
599
- for (const column of columns)if ("client" !== column.source && 'vuuAction' !== column.name && !columnNames?.includes(column.name)) unsubscribedColumns.push(column.name);
614
+ for (const column of columns)if ("client" !== column.source && "vuuAction" !== column.name && !columnNames?.includes(column.name)) unsubscribedColumns.push(column.name);
600
615
  if (unsubscribedColumns.length > 0) throw Error(`[column-utils] assertAllColumnsAreIncludedInSubscription columns not included in subscription: ${unsubscribedColumns.join(", ")}`);
601
616
  };
602
617
  const subscribedOnly = (columnNames)=>(column)=>"client" === column.source || columnNames?.includes(column.name);
@@ -789,4 +804,4 @@ const columnByAriaIndex = (columns, ariaColIndex)=>{
789
804
  }
790
805
  throw Error(`no column with aria-colIndex ${ariaColIndex}`);
791
806
  };
792
- export { AggregationType, NO_HEADINGS, addColumnToSubscribedColumns, applyDefaultColumnConfig, applyGroupByToColumns, applyRuntimeColumnWidthsToConfig, applySortToColumns, applyWidthToColumns, assertAllColumnsAreIncludedInSubscription, buildColumnMap, checkConfirmationPending, columnByAriaIndex, dataAndColumnUnchanged, dataColumnAndKeyUnchanged, dataSourceRowToDataRowDto, existingSort, extractGroupColumn, findColumn, flattenColumnGroup, fromServerDataType, getAllCellsInColumn, getAriaColIndex, getAriaRowIndex, getCalculatedColumnDetails, getColumnLabel, getColumnName, getColumnStyle, getColumnsInViewport, getDefaultAlignment, getDefaultColumnType, getGroupIcon, getGroupValue, getPinStateFromElement, getRuntimeColumnWidth, getServerDataType, getTableHeadings, getTypeFormattingFromColumn, hasCustomRenderer, hasHeadings, hasValidationRules, isCalculatedColumn, isColumnTypeRenderer, isDataLoading, isDataValueEditable, isDateTimeDataType, isDateTimeDataValue, isGroupColumn, isJsonAttribute, isJsonColumn, isJsonGroup, isLookupRenderer, isMappedValueTypeRenderer, isNanoTimestampColumn, isNotHidden, isNumericColumn, isPinned, isResizing, isStringColumn, isTextColumn, isTimeDataType, isTimeDataValue, isTimestampColumn, isTypeDescriptor, isValidColumnAlignment, isValidPinLocation, isValueListRenderer, isVuuColumnDataType, mapSortCriteria, measurePinnedColumns, metadataKeys, moveColumnTo, projectUpdates, removeSort, replaceColumn, setAggregations, setCalculatedColumnExpression, setCalculatedColumnName, setCalculatedColumnType, sortPinnedColumns, subscribedOnly, toColumnDescriptor, toColumnName, toDataSourceColumns, updateColumn, updateColumnFormatting, updateColumnRenderProps, updateColumnType, visibleColumnAtIndex };
807
+ export { AggregationType, NO_HEADINGS, addColumnToSubscribedColumns, applyDefaultColumnConfig, applyGroupByToColumns, applyRuntimeColumnWidthsToConfig, applySortToColumns, applyWidthToColumns, assertAllColumnsAreIncludedInSubscription, buildColumnMap, checkConfirmationPending, columnByAriaIndex, dataAndColumnUnchanged, dataColumnAndKeyUnchanged, dataSourceRowToDataRowDto, existingSort, extractGroupColumn, findColumn, flattenColumnGroup, fromServerDataType, getAllCellsInColumn, getAriaColIndex, getAriaRowIndex, getCalculatedColumnDetails, getColumnLabel, getColumnName, getColumnStyle, getColumnsInViewport, getDefaultAlignment, getDefaultColumnType, getGroupIcon, getGroupValue, getPinStateFromElement, getRuntimeColumnWidth, getServerDataType, getTableHeadings, getTypeFormattingFromColumn, hasCustomRenderer, hasHeadings, hasValidationRules, isCalculatedColumn, isColumnTypeRenderer, isDataLoading, isDataValueEditable, isDateTimeDataType, isDateTimeDataValue, isGroupColumn, isJsonAttribute, isJsonColumn, isJsonGroup, isLookupRenderer, isMappedValueTypeRenderer, isNanoTimestampColumn, isNotHidden, isNumericColumn, isPinned, isResizing, isStringColumn, isTemporalColumn, isTextColumn, isTimeDataType, isTimeDataValue, isTimeDataValueWithMilliseconds, isTimestampColumn, isTypeDescriptor, isValidColumnAlignment, isValidPinLocation, isValueListRenderer, isVuuColumnDataType, mapSortCriteria, measurePinnedColumns, metadataKeys, moveColumnTo, projectUpdates, removeSort, replaceColumn, setAggregations, setCalculatedColumnExpression, setCalculatedColumnName, setCalculatedColumnType, sortPinnedColumns, subscribedOnly, toColumnDescriptor, toColumnName, toDataSourceColumns, updateColumn, updateColumnFormatting, updateColumnRenderProps, updateColumnType, visibleColumnAtIndex };
@@ -1,4 +1,4 @@
1
- import { hasCustomRenderer, isColumnTypeRenderer, isDataValueEditable, isTypeDescriptor } from "./column-utils.js";
1
+ import { hasCustomRenderer, isColumnTypeRenderer, isDataValueEditable, isTemporalColumn, isTypeDescriptor } from "./column-utils.js";
2
2
  const containersSet = new Set();
3
3
  const viewsSet = new Set();
4
4
  const layoutComponentsMap = new Map();
@@ -76,6 +76,7 @@ function getEditRuleValidator(name) {
76
76
  }
77
77
  function dataCellRenderer(column) {
78
78
  if ("boolean" === column.serverDataType && !hasCustomRenderer(column.type)) return cellRenderersMap.get("checkbox-cell");
79
+ if (column.editable && !hasCustomRenderer(column.type) && isTemporalColumn(column) && cellRenderersMap.has("temporal-input-cell")) return cellRenderersMap.get("temporal-input-cell");
79
80
  if (isDataValueEditable(column, "update") && !hasCustomRenderer(column.type)) return cellRenderersMap.get("input-cell");
80
81
  if (isTypeDescriptor(column.type) && isColumnTypeRenderer(column.type.renderer)) return cellRenderersMap.get(column.type.renderer?.name);
81
82
  if ("boolean" === column.serverDataType) return cellRenderersMap.get("checkbox-cell");
package/src/data-utils.js CHANGED
@@ -56,6 +56,7 @@ const dataDescriptorTypeToVuuRowDataItemType = ({ serverDataType = "string" })=>
56
56
  case "double":
57
57
  case "int":
58
58
  case "long":
59
+ case "epochtimestamp":
59
60
  return "number";
60
61
  case "boolean":
61
62
  return "boolean";
@@ -3,7 +3,7 @@ import { EventEmitter } from "../event-emitter.js";
3
3
  import { uuid } from "../nanoid/index.js";
4
4
  import { isConfigChanged, stripVisualLink, vanillaConfig, withConfigDefaults } from "./datasource-utils.js";
5
5
  import { Range } from "../range-utils.js";
6
- import { filterAsQuery } from "../filters/index.js";
6
+ import { filterAsQuery, getColumnsByNameForFilter } from "../filters/index.js";
7
7
  const defaultSuspenseProps = {
8
8
  escalateToDisable: true
9
9
  };
@@ -88,9 +88,11 @@ class BaseDataSource extends EventEmitter {
88
88
  filterSpec: filter
89
89
  };
90
90
  }
91
- setFilter(filter) {
91
+ setFilter(filter, options) {
92
92
  const dataSourceFilter = {
93
- filter: filterAsQuery(filter),
93
+ filter: filterAsQuery(filter, {
94
+ columnsByName: getColumnsByNameForFilter(this.tableSchema, options?.columnsByName)
95
+ }),
94
96
  filterStruct: filter
95
97
  };
96
98
  this.filter = dataSourceFilter;
@@ -0,0 +1,85 @@
1
+ import { fromDateTimeFields, getDateTimeFields } from "./time-zone.js";
2
+ const NANOS_PER_MILLI = 1000000n;
3
+ const integerPattern = /^-?\d+$/;
4
+ const MAX_EPOCH_MILLIS = 8.64e15;
5
+ class EpochTimestamp {
6
+ epochMillis;
7
+ subMilliNanos;
8
+ constructor(epochMillis, subMilliNanos = 0){
9
+ this.epochMillis = epochMillis;
10
+ this.subMilliNanos = subMilliNanos;
11
+ }
12
+ static fromMillis(epochMillis, subMilliNanos = 0) {
13
+ const millis = Math.floor(epochMillis);
14
+ return new EpochTimestamp(millis, subMilliNanos);
15
+ }
16
+ static fromNanos(epochNanos) {
17
+ const nanos = "bigint" == typeof epochNanos ? epochNanos : BigInt(epochNanos);
18
+ let millis = nanos / NANOS_PER_MILLI;
19
+ let remainder = nanos % NANOS_PER_MILLI;
20
+ if (remainder < 0n) {
21
+ millis -= 1n;
22
+ remainder += NANOS_PER_MILLI;
23
+ }
24
+ return new EpochTimestamp(Number(millis), Number(remainder));
25
+ }
26
+ static fromDate(date) {
27
+ return new EpochTimestamp(date.getTime());
28
+ }
29
+ static fromFields(fields, timeZone, subMilliNanos = 0) {
30
+ return new EpochTimestamp(fromDateTimeFields(fields, timeZone), subMilliNanos);
31
+ }
32
+ static isEmpty(value) {
33
+ return null == value || "" === value || 0 === value || "0" === value || 0n === value;
34
+ }
35
+ static fromWire(value, encoding) {
36
+ const ts = EpochTimestamp.decode(value, encoding);
37
+ return void 0 !== ts && Math.abs(ts.epochMillis) <= MAX_EPOCH_MILLIS ? ts : void 0;
38
+ }
39
+ static decode(value, encoding) {
40
+ if (EpochTimestamp.isEmpty(value)) return;
41
+ if (value instanceof EpochTimestamp) return value;
42
+ if ("epochNanos" === encoding) {
43
+ if ("bigint" == typeof value) return EpochTimestamp.fromNanos(value);
44
+ else if ("string" == typeof value && integerPattern.test(value)) return EpochTimestamp.fromNanos(value);
45
+ else if ("number" == typeof value && Number.isFinite(value)) return EpochTimestamp.fromNanos(BigInt(Math.trunc(value)));
46
+ } else if ("number" == typeof value && Number.isFinite(value)) return EpochTimestamp.fromMillis(value);
47
+ else if ("bigint" == typeof value) return EpochTimestamp.fromMillis(Number(value));
48
+ else if ("string" == typeof value && integerPattern.test(value)) return EpochTimestamp.fromMillis(Number(value));
49
+ }
50
+ get epochNanos() {
51
+ return BigInt(this.epochMillis) * NANOS_PER_MILLI + BigInt(this.subMilliNanos);
52
+ }
53
+ toDate() {
54
+ return new Date(this.epochMillis);
55
+ }
56
+ fractionalSecond(digits) {
57
+ if (digits <= 0) return "";
58
+ const millisOfSecond = (this.epochMillis % 1000 + 1000) % 1000;
59
+ const nineDigits = `${millisOfSecond}`.padStart(3, "0").concat(`${this.subMilliNanos}`.padStart(6, "0"));
60
+ return nineDigits.slice(0, Math.min(digits, 9));
61
+ }
62
+ getFields(timeZone) {
63
+ return getDateTimeFields(this.epochMillis, timeZone);
64
+ }
65
+ toWire(encoding) {
66
+ return "epochNanos" === encoding ? this.epochNanos.toString() : this.epochMillis;
67
+ }
68
+ toLiteral(encoding) {
69
+ return `${this.toWire(encoding)}`;
70
+ }
71
+ addUnits(units, encoding) {
72
+ return "epochNanos" === encoding ? EpochTimestamp.fromNanos(this.epochNanos + BigInt(units)) : EpochTimestamp.fromMillis(this.epochMillis + units, this.subMilliNanos);
73
+ }
74
+ compare(other) {
75
+ return this.epochMillis === other.epochMillis ? this.subMilliNanos - other.subMilliNanos : this.epochMillis - other.epochMillis;
76
+ }
77
+ equals(other) {
78
+ return void 0 !== other && 0 === this.compare(other);
79
+ }
80
+ toString() {
81
+ const fraction = this.subMilliNanos ? this.fractionalSecond(9) : this.fractionalSecond(3);
82
+ return this.toDate().toISOString().replace(/\.\d{3}Z$/, `.${fraction}Z`);
83
+ }
84
+ }
85
+ export { EpochTimestamp };
@@ -3,50 +3,71 @@ function toCalendarDate(d) {
3
3
  return new CalendarDate(d.getFullYear(), d.getMonth() + 1, d.getDate());
4
4
  }
5
5
  const zeroTime = "00:00:00";
6
+ const zeroTimeMillis = "00:00:00.000";
6
7
  const zeroTimeUnit = "00";
8
+ const unitMaxValue = {
9
+ hours: 23,
10
+ minutes: 59,
11
+ seconds: 59,
12
+ milliseconds: 999
13
+ };
14
+ const unitLength = (unit)=>"milliseconds" === unit ? 3 : 2;
7
15
  function incrementTimeUnitValue(unit, value) {
8
16
  const num = parseInt(value);
9
- if ("hours" === unit && num < 23) return `${num + 1}`.padStart(2, "0").slice(-2);
10
- if ("hours" === unit && 23 === num) return "00";
11
- if (num < 59) return `${num + 1}`.padStart(2, "0").slice(-2);
12
- if (59 === num) return "00";
13
- return value;
17
+ const newValue = num >= unitMaxValue[unit] ? 0 : num + 1;
18
+ return `${newValue}`.padStart(unitLength(unit), "0");
14
19
  }
15
20
  function decrementTimeUnitValue(unit, value) {
16
21
  const num = parseInt(value);
17
- if ("hours" === unit && num > 0) return `${num - 1}`.padStart(2, "0").slice(-2);
18
- if ("hours" === unit && 0 === num) return "23";
19
- if (num > 0) return `${num - 1}`.padStart(2, "0").slice(-2);
20
- if (0 === num) return "59";
21
- return value;
22
+ const newValue = num <= 0 ? unitMaxValue[unit] : num - 1;
23
+ return `${newValue}`.padStart(unitLength(unit), "0");
22
24
  }
23
25
  function updateTimeString(timeString, unit, value) {
24
- const newTimeString = "hours" === unit ? value.concat(timeString.slice(2)) : "minutes" === unit ? timeString.slice(0, 3).concat(value).concat(timeString.slice(5)) : timeString.slice(0, 6).concat(value);
25
- if (isValidTimeString(newTimeString)) return newTimeString;
26
+ const newTimeString = "hours" === unit ? value.concat(timeString.slice(2)) : "minutes" === unit ? timeString.slice(0, 3).concat(value).concat(timeString.slice(5)) : "seconds" === unit ? timeString.slice(0, 6).concat(value).concat(timeString.slice(8)) : timeString.slice(0, 9).concat(value);
27
+ if (isValidTimeString(newTimeString) || isValidTimeStringMillis(newTimeString)) return newTimeString;
26
28
  throw Error(`[date-utils] udateTimeSting invalid result ${newTimeString}`);
27
29
  }
28
- const validTimePattern = /(?:[0-1][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]/;
30
+ const validTimePattern = /^(?:[0-1][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/;
31
+ const validTimeMillisPattern = /^(?:[0-1][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]\.[0-9]{3}$/;
29
32
  const isValidTimeString = (value)=>"string" == typeof value && validTimePattern.test(value);
33
+ const isValidTimeStringMillis = (value)=>"string" == typeof value && validTimeMillisPattern.test(value);
34
+ function normaliseTimeString(value, milliseconds = false) {
35
+ if (isValidTimeString(value)) return milliseconds ? `${value}.000` : value;
36
+ if (isValidTimeStringMillis(value)) return milliseconds ? value : value.slice(0, 8);
37
+ }
30
38
  function asTimeString(value, allowUndefined = false) {
31
39
  if (void 0 === value) if (allowUndefined) return value;
32
40
  else throw Error("[date-utils] asTimeString, value cannot be undefined");
33
41
  if (isValidTimeString(value)) return value;
42
+ if (isValidTimeStringMillis(value)) return value.slice(0, 8);
34
43
  if ("number" == typeof value) return Time.millisToTimeString(value);
35
44
  if ("string" == typeof value) {
36
45
  const valueAsInt = parseInt(value);
37
46
  if (!isNaN(valueAsInt)) return Time.millisToTimeString(valueAsInt);
38
47
  } else throw Error(`[date-utils] asTimeString, value ${value} is not valid TimeString`);
39
48
  }
49
+ function asTimeStringMillis(value, allowUndefined = false) {
50
+ if (void 0 === value) if (allowUndefined) return value;
51
+ else throw Error("[date-utils] asTimeStringMillis, value cannot be undefined");
52
+ const timeString = normaliseTimeString(value, true);
53
+ if (timeString) return timeString;
54
+ if ("number" == typeof value) return Time.millisToTimeStringMillis(value);
55
+ if ("string" == typeof value && /^\d+$/.test(value)) return Time.millisToTimeStringMillis(parseInt(value));
56
+ throw Error(`[date-utils] asTimeStringMillis, value ${value} is not valid TimeStringMillis`);
57
+ }
40
58
  const padZero = (val)=>`${val}`.padStart(2, "0");
41
59
  class TimeImpl {
42
60
  #hours;
43
61
  #minutes;
44
62
  #seconds;
63
+ #milliseconds;
45
64
  constructor(timeString){
46
- const [hours, minutes, seconds] = timeString.split(":");
65
+ const [hours, minutes, secondsAndMillis] = timeString.split(":");
66
+ const [seconds, milliseconds] = secondsAndMillis.split(".");
47
67
  this.#hours = parseInt(hours);
48
68
  this.#minutes = parseInt(minutes);
49
69
  this.#seconds = parseInt(seconds);
70
+ this.#milliseconds = void 0 === milliseconds ? void 0 : parseInt(milliseconds);
50
71
  }
51
72
  get hours() {
52
73
  return this.#hours;
@@ -57,24 +78,35 @@ class TimeImpl {
57
78
  get seconds() {
58
79
  return this.#seconds;
59
80
  }
81
+ get milliseconds() {
82
+ return this.#milliseconds ?? 0;
83
+ }
60
84
  asDate(date) {
61
85
  const dt = void 0 === date ? new Date() : "string" == typeof date ? new Date(date) : date;
62
86
  dt.setHours(this.#hours);
63
87
  dt.setMinutes(this.#minutes);
64
88
  dt.setSeconds(this.seconds);
65
- dt.setMilliseconds(0);
89
+ dt.setMilliseconds(this.milliseconds);
66
90
  return dt;
67
91
  }
68
92
  toString() {
69
- return Time.toString(this.#hours, this.#minutes, this.#seconds);
93
+ return Time.toString(this.#hours, this.#minutes, this.#seconds, this.#milliseconds);
70
94
  }
71
95
  }
72
96
  const Time = (timeString)=>new TimeImpl(timeString);
73
97
  Time.millisToTimeString = (timestamp)=>new Date(timestamp).toTimeString().slice(0, 8);
74
- Time.toString = (hours, minutes, seconds)=>`${padZero(hours)}:${padZero(minutes)}:${padZero(seconds)}`;
98
+ Time.millisToTimeStringMillis = (timestamp)=>{
99
+ const date = new Date(timestamp);
100
+ return `${date.toTimeString().slice(0, 8)}.${`${date.getMilliseconds()}`.padStart(3, "0")}`;
101
+ };
102
+ function timeToString(hours, minutes, seconds, milliseconds) {
103
+ const timeString = `${padZero(hours)}:${padZero(minutes)}:${padZero(seconds)}`;
104
+ return void 0 === milliseconds ? timeString : `${timeString}.${`${milliseconds}`.padStart(3, "0")}`;
105
+ }
106
+ Time.toString = timeToString;
75
107
  Time.isDateInMillis = (dtInMillis)=>{
76
108
  const n = "number" == typeof dtInMillis ? dtInMillis : Number(dtInMillis);
77
109
  if (!Number.isFinite(n) || !Number.isInteger(n)) return false;
78
110
  return new Date(n).getTime() === n;
79
111
  };
80
- export { Time, asTimeString, decrementTimeUnitValue, incrementTimeUnitValue, isValidTimeString, toCalendarDate, updateTimeString, zeroTime, zeroTimeUnit };
112
+ export { Time, asTimeString, asTimeStringMillis, decrementTimeUnitValue, incrementTimeUnitValue, isValidTimeString, isValidTimeStringMillis, normaliseTimeString, toCalendarDate, updateTimeString, zeroTime, zeroTimeMillis, zeroTimeUnit };
@@ -1,4 +1,4 @@
1
- import { isTypeDescriptor } from "../column-utils.js";
1
+ import { getTemporalInfo } from "./temporal.js";
2
2
  import { isDateTimePattern } from "./types.js";
3
3
  const defaultPatternsByType = {
4
4
  time: "hh:mm:ss",
@@ -8,10 +8,38 @@ const fallbackDateTimePattern = {
8
8
  date: defaultPatternsByType["date"],
9
9
  time: defaultPatternsByType["time"]
10
10
  };
11
- function dateTimePattern(type) {
12
- if (isTypeDescriptor(type)) {
11
+ const defaultDateTimePatternByKind = {
12
+ date: {
13
+ date: defaultPatternsByType.date
14
+ },
15
+ datetime: fallbackDateTimePattern,
16
+ time: {
17
+ time: defaultPatternsByType.time
18
+ }
19
+ };
20
+ const kindFromType = (type)=>{
21
+ const typeName = "string" == typeof type ? type : type?.name;
22
+ return "time" === typeName ? "time" : "date" === typeName ? "date" : "date/time" === typeName ? "datetime" : void 0;
23
+ };
24
+ function dateTimePattern(type, kind = kindFromType(type)) {
25
+ if ("object" == typeof type && null !== type) {
13
26
  if (type.formatting && isDateTimePattern(type.formatting.pattern)) return type.formatting.pattern;
14
27
  }
15
- return fallbackDateTimePattern;
28
+ return kind ? defaultDateTimePatternByKind[kind] : fallbackDateTimePattern;
16
29
  }
17
- export { dateTimePattern, defaultPatternsByType, fallbackDateTimePattern };
30
+ const withDateTimePattern = (descriptor, pattern)=>{
31
+ if (void 0 === pattern || void 0 === getTemporalInfo(descriptor)) return descriptor;
32
+ const name = pattern.date && pattern.time ? "date/time" : pattern.date ? "date" : "time";
33
+ const formatting = "object" == typeof descriptor.type ? descriptor.type.formatting : void 0;
34
+ return {
35
+ ...descriptor,
36
+ type: {
37
+ name,
38
+ formatting: {
39
+ ...formatting,
40
+ pattern
41
+ }
42
+ }
43
+ };
44
+ };
45
+ export { dateTimePattern, defaultDateTimePatternByKind, defaultPatternsByType, fallbackDateTimePattern, withDateTimePattern };
@@ -1,4 +1,6 @@
1
1
  import { DateFormatter } from "@internationalized/date";
2
+ import { EpochTimestamp } from "./EpochTimestamp.js";
3
+ import { resolveTimeZone } from "./time-zone.js";
2
4
  const baseTimeFormatOptions = {
3
5
  hour: "2-digit",
4
6
  minute: "2-digit",
@@ -23,8 +25,7 @@ const formatConfigByTimePatterns = {
23
25
  locale: "en-GB",
24
26
  options: {
25
27
  ...baseTimeFormatOptions,
26
- hour12: false,
27
- fractionalSecondDigits: 3
28
+ hour12: false
28
29
  }
29
30
  }
30
31
  };
@@ -81,21 +82,58 @@ const formatConfigByDatePatterns = {
81
82
  }
82
83
  }
83
84
  };
84
- const dateFormatterISO = {
85
- format: (date)=>new Date(date - 60000 * date.getTimezoneOffset()).toISOString().replace(/T.*/, "")
85
+ const NARROW_NBSP = /\u202f/g;
86
+ const intlFormatterCache = new Map();
87
+ const getIntlFormatter = (locale, options)=>{
88
+ const key = `${locale}:${JSON.stringify(options)}`;
89
+ let formatter = intlFormatterCache.get(key);
90
+ if (void 0 === formatter) {
91
+ formatter = new Intl.DateTimeFormat(locale, options);
92
+ intlFormatterCache.set(key, formatter);
93
+ }
94
+ return formatter;
86
95
  };
87
- function getDateFormatter({ locale, options }) {
88
- return new DateFormatter(locale, options);
96
+ const isoDateFormatter = (timeZone)=>{
97
+ const formatter = getIntlFormatter("en-GB", {
98
+ ...baseDateFormatOptions,
99
+ month: "2-digit",
100
+ timeZone
101
+ });
102
+ return (ts)=>{
103
+ const parts = formatter.formatToParts(ts.toDate());
104
+ const get = (type)=>parts.find((p)=>p.type === type)?.value ?? "";
105
+ return `${get("year")}-${get("month")}-${get("day")}`;
106
+ };
107
+ };
108
+ const dateFormatter = (pattern, timeZone, localeOverride)=>{
109
+ const { locale, options } = formatConfigByDatePatterns[pattern];
110
+ const formatter = getIntlFormatter(localeOverride ?? locale, {
111
+ ...options,
112
+ timeZone
113
+ });
114
+ return (ts)=>formatter.format(ts.toDate());
115
+ };
116
+ const timeFormatter = (pattern, timeZone, fractionalSecondDigits, localeOverride)=>{
117
+ const { locale, options } = formatConfigByTimePatterns[pattern];
118
+ const formatter = getIntlFormatter(localeOverride ?? locale, {
119
+ ...options,
120
+ timeZone
121
+ });
122
+ return (ts)=>formatter.formatToParts(ts.toDate()).map(({ type, value })=>"second" === type && fractionalSecondDigits > 0 ? `${value}.${ts.fractionalSecond(fractionalSecondDigits)}` : value.replace(NARROW_NBSP, " ")).join("");
123
+ };
124
+ function formatTimestamp({ date, time }, { fractionalSecondDigits = "hh:mm:ss.ms" === time ? 3 : 0, locale, timeZone } = {}) {
125
+ const tz = resolveTimeZone(timeZone);
126
+ const formatters = [];
127
+ if ("yyyy-mm-dd" === date) formatters.push(isoDateFormatter(tz));
128
+ else if (date) formatters.push(dateFormatter(date, tz, locale));
129
+ if (time) formatters.push(timeFormatter(time, tz, fractionalSecondDigits, locale));
130
+ return (ts)=>formatters.map((f)=>f(ts)).join(" ");
89
131
  }
90
- function getDateAndTimeFormatters({ date, time }) {
91
- const out = [];
92
- if ("yyyy-mm-dd" === date) out.push(dateFormatterISO);
93
- else if (date) out.push(getDateFormatter(formatConfigByDatePatterns[date]));
94
- if (time) out.push(getDateFormatter(formatConfigByTimePatterns[time]));
95
- return out;
132
+ function formatDate(pattern, options) {
133
+ const formatter = formatTimestamp(pattern, options);
134
+ return (d)=>formatter(EpochTimestamp.fromDate(d));
96
135
  }
97
- function formatDate(pattern) {
98
- const formatters = getDateAndTimeFormatters(pattern);
99
- return (d)=>formatters.map((f)=>f.format(d)).join(" ");
136
+ function getDateFormatter({ locale, options }) {
137
+ return new DateFormatter(locale, options);
100
138
  }
101
- export { formatDate, getDateFormatter };
139
+ export { formatDate, formatTimestamp, getDateFormatter };
package/src/date/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  export * from "./formatter.js";
2
- export { Time, asTimeString, decrementTimeUnitValue, incrementTimeUnitValue, isValidTimeString, toCalendarDate, updateTimeString, zeroTime, zeroTimeUnit } from "./date-utils.js";
3
- export { dateTimePattern, defaultPatternsByType, fallbackDateTimePattern } from "./dateTimePattern.js";
2
+ export * from "./temporal.js";
3
+ export * from "./time-zone.js";
4
+ export { Time, asTimeString, asTimeStringMillis, decrementTimeUnitValue, incrementTimeUnitValue, isValidTimeString, isValidTimeStringMillis, normaliseTimeString, toCalendarDate, updateTimeString, zeroTime, zeroTimeMillis, zeroTimeUnit } from "./date-utils.js";
5
+ export { dateTimePattern, defaultDateTimePatternByKind, defaultPatternsByType, fallbackDateTimePattern, withDateTimePattern } from "./dateTimePattern.js";
4
6
  export { dateTimeLabelByType, isDatePattern, isTimePattern, supportedDateTimePatterns } from "./types.js";
7
+ export { EpochTimestamp } from "./EpochTimestamp.js";
@@ -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 };