@vuu-ui/vuu-utils 3.4.0 → 3.5.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.4.0",
2
+ "version": "3.5.0-beta.1",
3
3
  "author": "heswell",
4
4
  "main": "./src/index.js",
5
5
  "license": "Apache-2.0",
@@ -15,14 +15,14 @@
15
15
  },
16
16
  "devDependencies": {
17
17
  "@dnd-kit/react": "^0.1.21",
18
- "@vuu-ui/vuu-data-types": "3.4.0",
19
- "@vuu-ui/vuu-table-types": "3.4.0",
20
- "@vuu-ui/vuu-filter-types": "3.4.0",
21
- "@vuu-ui/vuu-protocol-types": "3.4.0"
18
+ "@vuu-ui/vuu-data-types": "3.5.0-beta.1",
19
+ "@vuu-ui/vuu-table-types": "3.5.0-beta.1",
20
+ "@vuu-ui/vuu-filter-types": "3.5.0-beta.1",
21
+ "@vuu-ui/vuu-protocol-types": "3.5.0-beta.1"
22
22
  },
23
23
  "peerDependencies": {
24
24
  "@internationalized/date": "^3.0.0",
25
- "@vuu-ui/vuu-filter-parser": "3.4.0",
25
+ "@vuu-ui/vuu-filter-parser": "3.5.0-beta.1",
26
26
  "clsx": "^2.0.0",
27
27
  "react": "^19.2.3",
28
28
  "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 SORT_ASC = "asc";
4
5
  const NO_HEADINGS = [];
@@ -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;
@@ -533,6 +547,7 @@ const getDefaultColumnType = (serverDataType)=>{
533
547
  case "boolean":
534
548
  return "boolean";
535
549
  case "epochtimestamp":
550
+ case "epochtimestampnano":
536
551
  return "date/time";
537
552
  default:
538
553
  return "string";
@@ -591,7 +606,7 @@ const getTypeFormattingFromColumn = (column)=>{
591
606
  };
592
607
  const assertAllColumnsAreIncludedInSubscription = (columns, columnNames)=>{
593
608
  const unsubscribedColumns = [];
594
- for (const column of columns)if ("client" !== column.source && 'vuuAction' !== column.name && !columnNames?.includes(column.name)) unsubscribedColumns.push(column.name);
609
+ for (const column of columns)if ("client" !== column.source && "vuuAction" !== column.name && !columnNames?.includes(column.name)) unsubscribedColumns.push(column.name);
595
610
  if (unsubscribedColumns.length > 0) throw Error(`[column-utils] assertAllColumnsAreIncludedInSubscription columns not included in subscription: ${unsubscribedColumns.join(", ")}`);
596
611
  };
597
612
  const subscribedOnly = (columnNames)=>(column)=>"client" === column.source || columnNames?.includes(column.name);
@@ -784,4 +799,4 @@ const columnByAriaIndex = (columns, ariaColIndex)=>{
784
799
  }
785
800
  throw Error(`no column with aria-colIndex ${ariaColIndex}`);
786
801
  };
787
- export { AggregationType, 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 };
802
+ export { AggregationType, 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, isTypeDescriptor } from "./column-utils.js";
1
+ import { hasCustomRenderer, isColumnTypeRenderer, 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 (column.editable && !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,80 @@
1
+ import { fromDateTimeFields, getDateTimeFields } from "./time-zone.js";
2
+ const NANOS_PER_MILLI = 1000000n;
3
+ const integerPattern = /^-?\d+$/;
4
+ class EpochTimestamp {
5
+ epochMillis;
6
+ subMilliNanos;
7
+ constructor(epochMillis, subMilliNanos = 0){
8
+ this.epochMillis = epochMillis;
9
+ this.subMilliNanos = subMilliNanos;
10
+ }
11
+ static fromMillis(epochMillis, subMilliNanos = 0) {
12
+ const millis = Math.floor(epochMillis);
13
+ return new EpochTimestamp(millis, subMilliNanos);
14
+ }
15
+ static fromNanos(epochNanos) {
16
+ const nanos = "bigint" == typeof epochNanos ? epochNanos : BigInt(epochNanos);
17
+ let millis = nanos / NANOS_PER_MILLI;
18
+ let remainder = nanos % NANOS_PER_MILLI;
19
+ if (remainder < 0n) {
20
+ millis -= 1n;
21
+ remainder += NANOS_PER_MILLI;
22
+ }
23
+ return new EpochTimestamp(Number(millis), Number(remainder));
24
+ }
25
+ static fromDate(date) {
26
+ return new EpochTimestamp(date.getTime());
27
+ }
28
+ static fromFields(fields, timeZone, subMilliNanos = 0) {
29
+ return new EpochTimestamp(fromDateTimeFields(fields, timeZone), subMilliNanos);
30
+ }
31
+ static isEmpty(value) {
32
+ return null == value || "" === value || 0 === value || "0" === value || 0n === value;
33
+ }
34
+ static fromWire(value, encoding) {
35
+ if (EpochTimestamp.isEmpty(value)) return;
36
+ if (value instanceof EpochTimestamp) return value;
37
+ if ("epochNanos" === encoding) {
38
+ if ("bigint" == typeof value) return EpochTimestamp.fromNanos(value);
39
+ else if ("string" == typeof value && integerPattern.test(value)) return EpochTimestamp.fromNanos(value);
40
+ else if ("number" == typeof value && Number.isFinite(value)) return EpochTimestamp.fromNanos(BigInt(Math.trunc(value)));
41
+ } else if ("number" == typeof value && Number.isFinite(value)) return EpochTimestamp.fromMillis(value);
42
+ else if ("bigint" == typeof value) return EpochTimestamp.fromMillis(Number(value));
43
+ else if ("string" == typeof value && integerPattern.test(value)) return EpochTimestamp.fromMillis(Number(value));
44
+ }
45
+ get epochNanos() {
46
+ return BigInt(this.epochMillis) * NANOS_PER_MILLI + BigInt(this.subMilliNanos);
47
+ }
48
+ toDate() {
49
+ return new Date(this.epochMillis);
50
+ }
51
+ fractionalSecond(digits) {
52
+ if (digits <= 0) return "";
53
+ const millisOfSecond = (this.epochMillis % 1000 + 1000) % 1000;
54
+ const nineDigits = `${millisOfSecond}`.padStart(3, "0").concat(`${this.subMilliNanos}`.padStart(6, "0"));
55
+ return nineDigits.slice(0, Math.min(digits, 9));
56
+ }
57
+ getFields(timeZone) {
58
+ return getDateTimeFields(this.epochMillis, timeZone);
59
+ }
60
+ toWire(encoding) {
61
+ return "epochNanos" === encoding ? this.epochNanos.toString() : this.epochMillis;
62
+ }
63
+ toLiteral(encoding) {
64
+ return `${this.toWire(encoding)}`;
65
+ }
66
+ addUnits(units, encoding) {
67
+ return "epochNanos" === encoding ? EpochTimestamp.fromNanos(this.epochNanos + BigInt(units)) : EpochTimestamp.fromMillis(this.epochMillis + units, this.subMilliNanos);
68
+ }
69
+ compare(other) {
70
+ return this.epochMillis === other.epochMillis ? this.subMilliNanos - other.subMilliNanos : this.epochMillis - other.epochMillis;
71
+ }
72
+ equals(other) {
73
+ return void 0 !== other && 0 === this.compare(other);
74
+ }
75
+ toString() {
76
+ const fraction = this.subMilliNanos ? this.fractionalSecond(9) : this.fractionalSecond(3);
77
+ return this.toDate().toISOString().replace(/\.\d{3}Z$/, `.${fraction}Z`);
78
+ }
79
+ }
80
+ 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,3 @@
1
- import { isTypeDescriptor } from "../column-utils.js";
2
1
  import { isDateTimePattern } from "./types.js";
3
2
  const defaultPatternsByType = {
4
3
  time: "hh:mm:ss",
@@ -8,10 +7,23 @@ const fallbackDateTimePattern = {
8
7
  date: defaultPatternsByType["date"],
9
8
  time: defaultPatternsByType["time"]
10
9
  };
11
- function dateTimePattern(type) {
12
- if (isTypeDescriptor(type)) {
10
+ const defaultDateTimePatternByKind = {
11
+ date: {
12
+ date: defaultPatternsByType.date
13
+ },
14
+ datetime: fallbackDateTimePattern,
15
+ time: {
16
+ time: defaultPatternsByType.time
17
+ }
18
+ };
19
+ const kindFromType = (type)=>{
20
+ const typeName = "string" == typeof type ? type : type?.name;
21
+ return "time" === typeName ? "time" : "date" === typeName ? "date" : "date/time" === typeName ? "datetime" : void 0;
22
+ };
23
+ function dateTimePattern(type, kind = kindFromType(type)) {
24
+ if ("object" == typeof type && null !== type) {
13
25
  if (type.formatting && isDateTimePattern(type.formatting.pattern)) return type.formatting.pattern;
14
26
  }
15
- return fallbackDateTimePattern;
27
+ return kind ? defaultDateTimePatternByKind[kind] : fallbackDateTimePattern;
16
28
  }
17
- export { dateTimePattern, defaultPatternsByType, fallbackDateTimePattern };
29
+ export { dateTimePattern, defaultDateTimePatternByKind, defaultPatternsByType, fallbackDateTimePattern };
@@ -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 } 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 };