@trackunit/date-and-time-utils 1.14.4-alpha-d0f54ff2967.0 → 1.14.5

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/index.cjs.js CHANGED
@@ -911,6 +911,76 @@ const isEqualUtil = (from, to) => {
911
911
  .round({ smallestUnit: "seconds" })
912
912
  .equals(toZonedDateTimeUtil(to).round({ smallestUnit: "seconds" }));
913
913
  };
914
+ const localeScriptCache = new Map();
915
+ const localeScript = (locale) => {
916
+ const key = locale ?? "";
917
+ if (localeScriptCache.has(key))
918
+ return localeScriptCache.get(key);
919
+ let script;
920
+ try {
921
+ script = new Intl.Locale(locale ?? new Intl.NumberFormat().resolvedOptions().locale).maximize().script;
922
+ }
923
+ catch {
924
+ script = undefined;
925
+ }
926
+ localeScriptCache.set(key, script);
927
+ return script;
928
+ };
929
+ /**
930
+ * True when `Intl.NumberFormat` narrow for `locale`/`unit` renders in Latin script on a
931
+ * non-Latin-script locale — the symptom of a missing `units-narrow` slot silently
932
+ * inheriting from CLDR `root`. Detected directly on the formatter's output; this avoids
933
+ * any dependency on the ECMA-402 `DefaultLocale`, which would be the case if we tried to
934
+ * address `root` through `Intl` (there is no way: `Intl.NumberFormat("root")` throws, and
935
+ * `Intl.NumberFormat("und")` resolves via `LookupMatcher` to the runtime's default
936
+ * locale, not to CLDR root).
937
+ *
938
+ * Latin-script locales short-circuit to `false`: even if their narrow slot is empty and
939
+ * inherits from root, the fallback is still in the right script, so the output is not
940
+ * meaningfully delocalised.
941
+ */
942
+ const narrowUnitIsDelocalisedCache = new Map();
943
+ const narrowUnitIsDelocalised = (locale, unit) => {
944
+ const key = (locale ?? "") + "|" + unit;
945
+ const cached = narrowUnitIsDelocalisedCache.get(key);
946
+ if (cached !== undefined)
947
+ return cached;
948
+ if (localeScript(locale) === "Latn") {
949
+ narrowUnitIsDelocalisedCache.set(key, false);
950
+ return false;
951
+ }
952
+ const out = new Intl.NumberFormat(locale, { style: "unit", unit, unitDisplay: "narrow" }).format(1);
953
+ const delocalised = /[A-Za-z]/.test(out);
954
+ narrowUnitIsDelocalisedCache.set(key, delocalised);
955
+ return delocalised;
956
+ };
957
+ /**
958
+ * Formats a signed unit value using the Trackunit style axis.
959
+ * - `narrow-compact` past values → `Intl.NumberFormat` unit narrow (no affix), but only if
960
+ * the locale actually has native narrow data (i.e. not a silent root/Latin fallback);
961
+ * otherwise routes through `Intl.RelativeTimeFormat` narrow to preserve script.
962
+ * - `narrow-compact` future values → `Intl.RelativeTimeFormat` narrow (retains the CLDR-native
963
+ * leading affix — the API has no way to strip it).
964
+ * - No style specified → `Intl.RelativeTimeFormat` long (the default).
965
+ */
966
+ const formatRelativeUnit = (value, unit, locale, style) => {
967
+ if (style === "narrow-compact") {
968
+ if (value <= 0) {
969
+ const singularUnit = unit.endsWith("s") ? unit.slice(0, -1) : unit;
970
+ if (!narrowUnitIsDelocalised(locale, singularUnit)) {
971
+ return new Intl.NumberFormat(locale, {
972
+ style: "unit",
973
+ unit: singularUnit,
974
+ unitDisplay: "narrow",
975
+ }).format(Math.abs(value));
976
+ }
977
+ // Non-Latin script whose units-narrow output would render in Latin (CLDR root
978
+ // fallback) — route through `RelativeTimeFormat` narrow to preserve script.
979
+ }
980
+ return getRelativeTimeFormat(locale, "narrow").format(value, unit);
981
+ }
982
+ return getRelativeTimeFormat(locale).format(value, unit);
983
+ };
914
984
  /**
915
985
  * Calculates the time duration between two TemporalDate objects and returns a human-readable string representation of the duration.
916
986
  *
@@ -918,6 +988,12 @@ const isEqualUtil = (from, to) => {
918
988
  * @param {TemporalDate} to - The ending date.
919
989
  * @param {string} [timeZone] - The time zone to use for the calculations. Defaults to the local time zone.
920
990
  * @param {string} [locale] - The locale to use for formatting the duration. Defaults to the user's locale.
991
+ * @param {TuRelativeTimeStyle} [style] - Optional width override. Pass `"narrow-compact"` to render
992
+ * past values without any past-tense affix (`"2d"` in en, `"2j"` in fr) while future values retain
993
+ * the CLDR-native leading affix (`"in 2d"` in en). Non-Latin-script locales without native CLDR
994
+ * narrow data fall back to `RelativeTimeFormat` narrow to preserve script, which keeps the affix
995
+ * (`"2日前"` in ja) — see {@link TuRelativeTimeStyle} for the CLDR-table-sourcing details.
996
+ * Omit `style` to get the default long form (`"2 days ago"` in en, `"vor 2 Tagen"` in de).
921
997
  * @returns {string|Error} - The formatted duration string or an Error object if the duration is not valid.
922
998
  * @example
923
999
  * const startDate = "2023-05-1010:30:00.000Z"
@@ -937,8 +1013,14 @@ const isEqualUtil = (from, to) => {
937
1013
  *
938
1014
  * const result = timeSinceAuto(startDate, endDate, 'America/New_York', 'en-US');
939
1015
  * Output: "1 year ago"
1016
+ * @example
1017
+ * const startDate = "2023-05-10"
1018
+ * const endDate = "2023-05-18"
1019
+ *
1020
+ * const result = timeSinceAuto(startDate, endDate, 'America/New_York', 'en-US', 'narrow-compact');
1021
+ * Output: "8d"
940
1022
  */
941
- const timeSinceAuto = (from, to, timeZone, locale) => {
1023
+ const timeSinceAuto = (from, to, timeZone, locale, style) => {
942
1024
  let since;
943
1025
  try {
944
1026
  since = toZonedDateTimeUtil(from, timeZone)
@@ -955,25 +1037,25 @@ const timeSinceAuto = (from, to, timeZone, locale) => {
955
1037
  }
956
1038
  const duration = polyfill.Temporal.Duration.from(since);
957
1039
  if (duration.blank) {
958
- return getRelativeTimeFormat(locale).format(-1, "seconds");
1040
+ return formatRelativeUnit(-1, "seconds", locale, style);
959
1041
  }
960
1042
  if (duration.years !== 0) {
961
- return getRelativeTimeFormat(locale).format(duration.years, "years");
1043
+ return formatRelativeUnit(duration.years, "years", locale, style);
962
1044
  }
963
1045
  if (duration.months !== 0) {
964
- return getRelativeTimeFormat(locale).format(duration.months, "months");
1046
+ return formatRelativeUnit(duration.months, "months", locale, style);
965
1047
  }
966
1048
  if (duration.days !== 0) {
967
- return getRelativeTimeFormat(locale).format(duration.days, "days");
1049
+ return formatRelativeUnit(duration.days, "days", locale, style);
968
1050
  }
969
1051
  if (duration.hours !== 0) {
970
- return getRelativeTimeFormat(locale).format(duration.hours, "hours");
1052
+ return formatRelativeUnit(duration.hours, "hours", locale, style);
971
1053
  }
972
1054
  if (duration.minutes !== 0) {
973
- return getRelativeTimeFormat(locale).format(duration.minutes, "minutes");
1055
+ return formatRelativeUnit(duration.minutes, "minutes", locale, style);
974
1056
  }
975
1057
  if (duration.seconds !== 0) {
976
- return getRelativeTimeFormat(locale).format(duration.seconds, "seconds");
1058
+ return formatRelativeUnit(duration.seconds, "seconds", locale, style);
977
1059
  }
978
1060
  // eslint-disable-next-line no-console
979
1061
  console.warn(new Error("Not a valid duration", { cause: duration }));
@@ -994,7 +1076,7 @@ const timeSinceAuto = (from, to, timeZone, locale) => {
994
1076
  * const result = timeSinceInSeconds(startDate, endDate, 'America/New_York', 'en-US');
995
1077
  * Output: "480 seconds ago"
996
1078
  */
997
- const timeSinceInSeconds = (from, to, timeZone, locale) => {
1079
+ const timeSinceInSeconds = (from, to, timeZone, locale, style) => {
998
1080
  let since = new polyfill.Temporal.Duration();
999
1081
  try {
1000
1082
  since = toZonedDateTimeUtil(from, timeZone).since(toZonedDateTimeUtil(to, timeZone), {
@@ -1006,7 +1088,7 @@ const timeSinceInSeconds = (from, to, timeZone, locale) => {
1006
1088
  console.warn("Error: ", error);
1007
1089
  }
1008
1090
  const duration = polyfill.Temporal.Duration.from(since);
1009
- return getRelativeTimeFormat(locale).format(duration.seconds, "seconds");
1091
+ return formatRelativeUnit(duration.seconds, "seconds", locale, style);
1010
1092
  };
1011
1093
  /**
1012
1094
  * Calculates the time duration between two TemporalDate objects and returns the duration in minutes as a formatted string.
@@ -1023,7 +1105,7 @@ const timeSinceInSeconds = (from, to, timeZone, locale) => {
1023
1105
  * const result = timeSinceInSeconds(startDate, endDate, 'America/New_York', 'en-US');
1024
1106
  * Output: "8 minutes ago"
1025
1107
  */
1026
- const timeSinceInMinutes = (from, to, timeZone, locale) => {
1108
+ const timeSinceInMinutes = (from, to, timeZone, locale, style) => {
1027
1109
  let since = new polyfill.Temporal.Duration();
1028
1110
  try {
1029
1111
  since = toZonedDateTimeUtil(from, timeZone).since(toZonedDateTimeUtil(to, timeZone), {
@@ -1035,7 +1117,7 @@ const timeSinceInMinutes = (from, to, timeZone, locale) => {
1035
1117
  console.warn("Error: ", error);
1036
1118
  }
1037
1119
  const duration = polyfill.Temporal.Duration.from(since);
1038
- return getRelativeTimeFormat(locale).format(duration.minutes, "minutes");
1120
+ return formatRelativeUnit(duration.minutes, "minutes", locale, style);
1039
1121
  };
1040
1122
  /**
1041
1123
  * Calculates the time duration between two TemporalDate objects and returns the duration in hours as a formatted string.
@@ -1052,7 +1134,7 @@ const timeSinceInMinutes = (from, to, timeZone, locale) => {
1052
1134
  * const result = timeSinceInHours(startDate, endDate);
1053
1135
  * Output: "3 hours ago"
1054
1136
  */
1055
- const timeSinceInHours = (from, to, timeZone, locale) => {
1137
+ const timeSinceInHours = (from, to, timeZone, locale, style) => {
1056
1138
  let since = new polyfill.Temporal.Duration();
1057
1139
  try {
1058
1140
  since = toZonedDateTimeUtil(from, timeZone).since(toZonedDateTimeUtil(to, timeZone), {
@@ -1064,7 +1146,7 @@ const timeSinceInHours = (from, to, timeZone, locale) => {
1064
1146
  console.warn("Error: ", error);
1065
1147
  }
1066
1148
  const duration = polyfill.Temporal.Duration.from(since);
1067
- return getRelativeTimeFormat(locale).format(duration.hours, "hours");
1149
+ return formatRelativeUnit(duration.hours, "hours", locale, style);
1068
1150
  };
1069
1151
  /**
1070
1152
  * Calculates the time duration between two TemporalDate objects and returns the duration in days as a formatted string.
@@ -1087,7 +1169,7 @@ const timeSinceInHours = (from, to, timeZone, locale) => {
1087
1169
  * const result = timeSinceInDays(startDate, endDate, 'America/New_York', 'en-US');
1088
1170
  * Output: "6 days ago"
1089
1171
  */
1090
- const timeSinceInDays = (from, to, timeZone, locale) => {
1172
+ const timeSinceInDays = (from, to, timeZone, locale, style) => {
1091
1173
  let since = new polyfill.Temporal.Duration();
1092
1174
  try {
1093
1175
  since = toZonedDateTimeUtil(from, timeZone).since(toZonedDateTimeUtil(to, timeZone), {
@@ -1099,7 +1181,7 @@ const timeSinceInDays = (from, to, timeZone, locale) => {
1099
1181
  console.warn("Error: ", error);
1100
1182
  }
1101
1183
  const duration = polyfill.Temporal.Duration.from(since);
1102
- return getRelativeTimeFormat(locale).format(Math.ceil(duration.hours / 24.0), "days");
1184
+ return formatRelativeUnit(Math.ceil(duration.hours / 24.0), "days", locale, style);
1103
1185
  };
1104
1186
  /**
1105
1187
  * Calculates the time duration between two TemporalDate objects and returns the duration in months as a formatted string.
@@ -1109,6 +1191,11 @@ const timeSinceInDays = (from, to, timeZone, locale) => {
1109
1191
  * @param {string} [timeZone] - The time zone to use for the calculations. Defaults to the local time zone.
1110
1192
  * @param {string} [locale] - The locale to use for formatting the duration. Defaults to the user's locale.
1111
1193
  * @returns {string} - The formatted duration in months.
1194
+ * @remarks When both `from` and `to` fall in a leap year, the function falls back to
1195
+ * `timeSinceInDays` and returns the raw day count for the whole span. Callers relying on a
1196
+ * specific unit — especially with `style="narrow-compact"`, where the unit is visible —
1197
+ * should be aware that the output can be an arbitrarily large day count (e.g. `"60d"` for a
1198
+ * two-month gap) instead of months on those inputs.
1112
1199
  * @example
1113
1200
  * const startDate = "2023-05-10"
1114
1201
  * const endDate = "2023-07-11"
@@ -1116,10 +1203,10 @@ const timeSinceInDays = (from, to, timeZone, locale) => {
1116
1203
  * const result = timeSinceInMonths(startDate, endDate, 'America/New_York', 'en-US');
1117
1204
  * Output: "2 months ago"
1118
1205
  */
1119
- const timeSinceInMonths = (from, to, timeZone, locale) => {
1206
+ const timeSinceInMonths = (from, to, timeZone, locale, style) => {
1120
1207
  const isLeapYear = toZonedDateTimeUtil(from, timeZone).inLeapYear && toZonedDateTimeUtil(to, timeZone).inLeapYear;
1121
1208
  if (isLeapYear) {
1122
- return timeSinceInDays(from, to, timeZone, locale);
1209
+ return timeSinceInDays(from, to, timeZone, locale, style);
1123
1210
  }
1124
1211
  let since = new polyfill.Temporal.Duration();
1125
1212
  try {
@@ -1132,7 +1219,7 @@ const timeSinceInMonths = (from, to, timeZone, locale) => {
1132
1219
  console.warn("Error: ", error);
1133
1220
  }
1134
1221
  const duration = polyfill.Temporal.Duration.from(since);
1135
- return getRelativeTimeFormat(locale).format(duration.months, "months");
1222
+ return formatRelativeUnit(duration.months, "months", locale, style);
1136
1223
  };
1137
1224
  /**
1138
1225
  * Calculates the time duration between two TemporalDate objects and returns the duration in years as a formatted string.
@@ -1142,6 +1229,11 @@ const timeSinceInMonths = (from, to, timeZone, locale) => {
1142
1229
  * @param {string} [timeZone] - The time zone to use for the calculations. Defaults to the local time zone.
1143
1230
  * @param {string} [locale] - The locale to use for formatting the duration. Defaults to the user's locale.
1144
1231
  * @returns {string} - The formatted duration in years.
1232
+ * @remarks When both `from` and `to` fall in a leap year, the function falls back to
1233
+ * `timeSinceInDays` and returns the raw day count for the whole span. Callers relying on a
1234
+ * specific unit — especially with `style="narrow-compact"`, where the unit is visible —
1235
+ * should be aware that the output can be an arbitrarily large day count (e.g. `"1462d"` for
1236
+ * a ~4-year gap between two leap-year endpoints) instead of years on those inputs.
1145
1237
  * @example
1146
1238
  * const startDate = "2023-05-10"
1147
1239
  * const endDate = "2024-05-11"
@@ -1149,10 +1241,10 @@ const timeSinceInMonths = (from, to, timeZone, locale) => {
1149
1241
  * const result = timeSinceInYears(startDate, endDate, 'America/New_York', 'en-US');
1150
1242
  * Output: "1 year ago"
1151
1243
  */
1152
- const timeSinceInYears = (from, to, timeZone, locale) => {
1244
+ const timeSinceInYears = (from, to, timeZone, locale, style) => {
1153
1245
  const isLeapYear = toZonedDateTimeUtil(from, timeZone).inLeapYear && toZonedDateTimeUtil(to, timeZone).inLeapYear;
1154
1246
  if (isLeapYear) {
1155
- return timeSinceInDays(from, to, timeZone, locale);
1247
+ return timeSinceInDays(from, to, timeZone, locale, style);
1156
1248
  }
1157
1249
  let since = new polyfill.Temporal.Duration();
1158
1250
  try {
@@ -1165,9 +1257,9 @@ const timeSinceInYears = (from, to, timeZone, locale) => {
1165
1257
  console.warn("Error: ", error);
1166
1258
  }
1167
1259
  const duration = polyfill.Temporal.Duration.from(since);
1168
- return getRelativeTimeFormat(locale).format(duration.years, "years");
1260
+ return formatRelativeUnit(duration.years, "years", locale, style);
1169
1261
  };
1170
- const getRelativeTimeFormat = (locale) => new Intl.RelativeTimeFormat(locale, { style: "long", numeric: "always" });
1262
+ const getRelativeTimeFormat = (locale, style = "long") => new Intl.RelativeTimeFormat(locale, { style, numeric: "always" });
1171
1263
  /**
1172
1264
  * Get duration in human-readable form
1173
1265
  *
package/index.esm.js CHANGED
@@ -910,6 +910,76 @@ const isEqualUtil = (from, to) => {
910
910
  .round({ smallestUnit: "seconds" })
911
911
  .equals(toZonedDateTimeUtil(to).round({ smallestUnit: "seconds" }));
912
912
  };
913
+ const localeScriptCache = new Map();
914
+ const localeScript = (locale) => {
915
+ const key = locale ?? "";
916
+ if (localeScriptCache.has(key))
917
+ return localeScriptCache.get(key);
918
+ let script;
919
+ try {
920
+ script = new Intl.Locale(locale ?? new Intl.NumberFormat().resolvedOptions().locale).maximize().script;
921
+ }
922
+ catch {
923
+ script = undefined;
924
+ }
925
+ localeScriptCache.set(key, script);
926
+ return script;
927
+ };
928
+ /**
929
+ * True when `Intl.NumberFormat` narrow for `locale`/`unit` renders in Latin script on a
930
+ * non-Latin-script locale — the symptom of a missing `units-narrow` slot silently
931
+ * inheriting from CLDR `root`. Detected directly on the formatter's output; this avoids
932
+ * any dependency on the ECMA-402 `DefaultLocale`, which would be the case if we tried to
933
+ * address `root` through `Intl` (there is no way: `Intl.NumberFormat("root")` throws, and
934
+ * `Intl.NumberFormat("und")` resolves via `LookupMatcher` to the runtime's default
935
+ * locale, not to CLDR root).
936
+ *
937
+ * Latin-script locales short-circuit to `false`: even if their narrow slot is empty and
938
+ * inherits from root, the fallback is still in the right script, so the output is not
939
+ * meaningfully delocalised.
940
+ */
941
+ const narrowUnitIsDelocalisedCache = new Map();
942
+ const narrowUnitIsDelocalised = (locale, unit) => {
943
+ const key = (locale ?? "") + "|" + unit;
944
+ const cached = narrowUnitIsDelocalisedCache.get(key);
945
+ if (cached !== undefined)
946
+ return cached;
947
+ if (localeScript(locale) === "Latn") {
948
+ narrowUnitIsDelocalisedCache.set(key, false);
949
+ return false;
950
+ }
951
+ const out = new Intl.NumberFormat(locale, { style: "unit", unit, unitDisplay: "narrow" }).format(1);
952
+ const delocalised = /[A-Za-z]/.test(out);
953
+ narrowUnitIsDelocalisedCache.set(key, delocalised);
954
+ return delocalised;
955
+ };
956
+ /**
957
+ * Formats a signed unit value using the Trackunit style axis.
958
+ * - `narrow-compact` past values → `Intl.NumberFormat` unit narrow (no affix), but only if
959
+ * the locale actually has native narrow data (i.e. not a silent root/Latin fallback);
960
+ * otherwise routes through `Intl.RelativeTimeFormat` narrow to preserve script.
961
+ * - `narrow-compact` future values → `Intl.RelativeTimeFormat` narrow (retains the CLDR-native
962
+ * leading affix — the API has no way to strip it).
963
+ * - No style specified → `Intl.RelativeTimeFormat` long (the default).
964
+ */
965
+ const formatRelativeUnit = (value, unit, locale, style) => {
966
+ if (style === "narrow-compact") {
967
+ if (value <= 0) {
968
+ const singularUnit = unit.endsWith("s") ? unit.slice(0, -1) : unit;
969
+ if (!narrowUnitIsDelocalised(locale, singularUnit)) {
970
+ return new Intl.NumberFormat(locale, {
971
+ style: "unit",
972
+ unit: singularUnit,
973
+ unitDisplay: "narrow",
974
+ }).format(Math.abs(value));
975
+ }
976
+ // Non-Latin script whose units-narrow output would render in Latin (CLDR root
977
+ // fallback) — route through `RelativeTimeFormat` narrow to preserve script.
978
+ }
979
+ return getRelativeTimeFormat(locale, "narrow").format(value, unit);
980
+ }
981
+ return getRelativeTimeFormat(locale).format(value, unit);
982
+ };
913
983
  /**
914
984
  * Calculates the time duration between two TemporalDate objects and returns a human-readable string representation of the duration.
915
985
  *
@@ -917,6 +987,12 @@ const isEqualUtil = (from, to) => {
917
987
  * @param {TemporalDate} to - The ending date.
918
988
  * @param {string} [timeZone] - The time zone to use for the calculations. Defaults to the local time zone.
919
989
  * @param {string} [locale] - The locale to use for formatting the duration. Defaults to the user's locale.
990
+ * @param {TuRelativeTimeStyle} [style] - Optional width override. Pass `"narrow-compact"` to render
991
+ * past values without any past-tense affix (`"2d"` in en, `"2j"` in fr) while future values retain
992
+ * the CLDR-native leading affix (`"in 2d"` in en). Non-Latin-script locales without native CLDR
993
+ * narrow data fall back to `RelativeTimeFormat` narrow to preserve script, which keeps the affix
994
+ * (`"2日前"` in ja) — see {@link TuRelativeTimeStyle} for the CLDR-table-sourcing details.
995
+ * Omit `style` to get the default long form (`"2 days ago"` in en, `"vor 2 Tagen"` in de).
920
996
  * @returns {string|Error} - The formatted duration string or an Error object if the duration is not valid.
921
997
  * @example
922
998
  * const startDate = "2023-05-1010:30:00.000Z"
@@ -936,8 +1012,14 @@ const isEqualUtil = (from, to) => {
936
1012
  *
937
1013
  * const result = timeSinceAuto(startDate, endDate, 'America/New_York', 'en-US');
938
1014
  * Output: "1 year ago"
1015
+ * @example
1016
+ * const startDate = "2023-05-10"
1017
+ * const endDate = "2023-05-18"
1018
+ *
1019
+ * const result = timeSinceAuto(startDate, endDate, 'America/New_York', 'en-US', 'narrow-compact');
1020
+ * Output: "8d"
939
1021
  */
940
- const timeSinceAuto = (from, to, timeZone, locale) => {
1022
+ const timeSinceAuto = (from, to, timeZone, locale, style) => {
941
1023
  let since;
942
1024
  try {
943
1025
  since = toZonedDateTimeUtil(from, timeZone)
@@ -954,25 +1036,25 @@ const timeSinceAuto = (from, to, timeZone, locale) => {
954
1036
  }
955
1037
  const duration = Temporal.Duration.from(since);
956
1038
  if (duration.blank) {
957
- return getRelativeTimeFormat(locale).format(-1, "seconds");
1039
+ return formatRelativeUnit(-1, "seconds", locale, style);
958
1040
  }
959
1041
  if (duration.years !== 0) {
960
- return getRelativeTimeFormat(locale).format(duration.years, "years");
1042
+ return formatRelativeUnit(duration.years, "years", locale, style);
961
1043
  }
962
1044
  if (duration.months !== 0) {
963
- return getRelativeTimeFormat(locale).format(duration.months, "months");
1045
+ return formatRelativeUnit(duration.months, "months", locale, style);
964
1046
  }
965
1047
  if (duration.days !== 0) {
966
- return getRelativeTimeFormat(locale).format(duration.days, "days");
1048
+ return formatRelativeUnit(duration.days, "days", locale, style);
967
1049
  }
968
1050
  if (duration.hours !== 0) {
969
- return getRelativeTimeFormat(locale).format(duration.hours, "hours");
1051
+ return formatRelativeUnit(duration.hours, "hours", locale, style);
970
1052
  }
971
1053
  if (duration.minutes !== 0) {
972
- return getRelativeTimeFormat(locale).format(duration.minutes, "minutes");
1054
+ return formatRelativeUnit(duration.minutes, "minutes", locale, style);
973
1055
  }
974
1056
  if (duration.seconds !== 0) {
975
- return getRelativeTimeFormat(locale).format(duration.seconds, "seconds");
1057
+ return formatRelativeUnit(duration.seconds, "seconds", locale, style);
976
1058
  }
977
1059
  // eslint-disable-next-line no-console
978
1060
  console.warn(new Error("Not a valid duration", { cause: duration }));
@@ -993,7 +1075,7 @@ const timeSinceAuto = (from, to, timeZone, locale) => {
993
1075
  * const result = timeSinceInSeconds(startDate, endDate, 'America/New_York', 'en-US');
994
1076
  * Output: "480 seconds ago"
995
1077
  */
996
- const timeSinceInSeconds = (from, to, timeZone, locale) => {
1078
+ const timeSinceInSeconds = (from, to, timeZone, locale, style) => {
997
1079
  let since = new Temporal.Duration();
998
1080
  try {
999
1081
  since = toZonedDateTimeUtil(from, timeZone).since(toZonedDateTimeUtil(to, timeZone), {
@@ -1005,7 +1087,7 @@ const timeSinceInSeconds = (from, to, timeZone, locale) => {
1005
1087
  console.warn("Error: ", error);
1006
1088
  }
1007
1089
  const duration = Temporal.Duration.from(since);
1008
- return getRelativeTimeFormat(locale).format(duration.seconds, "seconds");
1090
+ return formatRelativeUnit(duration.seconds, "seconds", locale, style);
1009
1091
  };
1010
1092
  /**
1011
1093
  * Calculates the time duration between two TemporalDate objects and returns the duration in minutes as a formatted string.
@@ -1022,7 +1104,7 @@ const timeSinceInSeconds = (from, to, timeZone, locale) => {
1022
1104
  * const result = timeSinceInSeconds(startDate, endDate, 'America/New_York', 'en-US');
1023
1105
  * Output: "8 minutes ago"
1024
1106
  */
1025
- const timeSinceInMinutes = (from, to, timeZone, locale) => {
1107
+ const timeSinceInMinutes = (from, to, timeZone, locale, style) => {
1026
1108
  let since = new Temporal.Duration();
1027
1109
  try {
1028
1110
  since = toZonedDateTimeUtil(from, timeZone).since(toZonedDateTimeUtil(to, timeZone), {
@@ -1034,7 +1116,7 @@ const timeSinceInMinutes = (from, to, timeZone, locale) => {
1034
1116
  console.warn("Error: ", error);
1035
1117
  }
1036
1118
  const duration = Temporal.Duration.from(since);
1037
- return getRelativeTimeFormat(locale).format(duration.minutes, "minutes");
1119
+ return formatRelativeUnit(duration.minutes, "minutes", locale, style);
1038
1120
  };
1039
1121
  /**
1040
1122
  * Calculates the time duration between two TemporalDate objects and returns the duration in hours as a formatted string.
@@ -1051,7 +1133,7 @@ const timeSinceInMinutes = (from, to, timeZone, locale) => {
1051
1133
  * const result = timeSinceInHours(startDate, endDate);
1052
1134
  * Output: "3 hours ago"
1053
1135
  */
1054
- const timeSinceInHours = (from, to, timeZone, locale) => {
1136
+ const timeSinceInHours = (from, to, timeZone, locale, style) => {
1055
1137
  let since = new Temporal.Duration();
1056
1138
  try {
1057
1139
  since = toZonedDateTimeUtil(from, timeZone).since(toZonedDateTimeUtil(to, timeZone), {
@@ -1063,7 +1145,7 @@ const timeSinceInHours = (from, to, timeZone, locale) => {
1063
1145
  console.warn("Error: ", error);
1064
1146
  }
1065
1147
  const duration = Temporal.Duration.from(since);
1066
- return getRelativeTimeFormat(locale).format(duration.hours, "hours");
1148
+ return formatRelativeUnit(duration.hours, "hours", locale, style);
1067
1149
  };
1068
1150
  /**
1069
1151
  * Calculates the time duration between two TemporalDate objects and returns the duration in days as a formatted string.
@@ -1086,7 +1168,7 @@ const timeSinceInHours = (from, to, timeZone, locale) => {
1086
1168
  * const result = timeSinceInDays(startDate, endDate, 'America/New_York', 'en-US');
1087
1169
  * Output: "6 days ago"
1088
1170
  */
1089
- const timeSinceInDays = (from, to, timeZone, locale) => {
1171
+ const timeSinceInDays = (from, to, timeZone, locale, style) => {
1090
1172
  let since = new Temporal.Duration();
1091
1173
  try {
1092
1174
  since = toZonedDateTimeUtil(from, timeZone).since(toZonedDateTimeUtil(to, timeZone), {
@@ -1098,7 +1180,7 @@ const timeSinceInDays = (from, to, timeZone, locale) => {
1098
1180
  console.warn("Error: ", error);
1099
1181
  }
1100
1182
  const duration = Temporal.Duration.from(since);
1101
- return getRelativeTimeFormat(locale).format(Math.ceil(duration.hours / 24.0), "days");
1183
+ return formatRelativeUnit(Math.ceil(duration.hours / 24.0), "days", locale, style);
1102
1184
  };
1103
1185
  /**
1104
1186
  * Calculates the time duration between two TemporalDate objects and returns the duration in months as a formatted string.
@@ -1108,6 +1190,11 @@ const timeSinceInDays = (from, to, timeZone, locale) => {
1108
1190
  * @param {string} [timeZone] - The time zone to use for the calculations. Defaults to the local time zone.
1109
1191
  * @param {string} [locale] - The locale to use for formatting the duration. Defaults to the user's locale.
1110
1192
  * @returns {string} - The formatted duration in months.
1193
+ * @remarks When both `from` and `to` fall in a leap year, the function falls back to
1194
+ * `timeSinceInDays` and returns the raw day count for the whole span. Callers relying on a
1195
+ * specific unit — especially with `style="narrow-compact"`, where the unit is visible —
1196
+ * should be aware that the output can be an arbitrarily large day count (e.g. `"60d"` for a
1197
+ * two-month gap) instead of months on those inputs.
1111
1198
  * @example
1112
1199
  * const startDate = "2023-05-10"
1113
1200
  * const endDate = "2023-07-11"
@@ -1115,10 +1202,10 @@ const timeSinceInDays = (from, to, timeZone, locale) => {
1115
1202
  * const result = timeSinceInMonths(startDate, endDate, 'America/New_York', 'en-US');
1116
1203
  * Output: "2 months ago"
1117
1204
  */
1118
- const timeSinceInMonths = (from, to, timeZone, locale) => {
1205
+ const timeSinceInMonths = (from, to, timeZone, locale, style) => {
1119
1206
  const isLeapYear = toZonedDateTimeUtil(from, timeZone).inLeapYear && toZonedDateTimeUtil(to, timeZone).inLeapYear;
1120
1207
  if (isLeapYear) {
1121
- return timeSinceInDays(from, to, timeZone, locale);
1208
+ return timeSinceInDays(from, to, timeZone, locale, style);
1122
1209
  }
1123
1210
  let since = new Temporal.Duration();
1124
1211
  try {
@@ -1131,7 +1218,7 @@ const timeSinceInMonths = (from, to, timeZone, locale) => {
1131
1218
  console.warn("Error: ", error);
1132
1219
  }
1133
1220
  const duration = Temporal.Duration.from(since);
1134
- return getRelativeTimeFormat(locale).format(duration.months, "months");
1221
+ return formatRelativeUnit(duration.months, "months", locale, style);
1135
1222
  };
1136
1223
  /**
1137
1224
  * Calculates the time duration between two TemporalDate objects and returns the duration in years as a formatted string.
@@ -1141,6 +1228,11 @@ const timeSinceInMonths = (from, to, timeZone, locale) => {
1141
1228
  * @param {string} [timeZone] - The time zone to use for the calculations. Defaults to the local time zone.
1142
1229
  * @param {string} [locale] - The locale to use for formatting the duration. Defaults to the user's locale.
1143
1230
  * @returns {string} - The formatted duration in years.
1231
+ * @remarks When both `from` and `to` fall in a leap year, the function falls back to
1232
+ * `timeSinceInDays` and returns the raw day count for the whole span. Callers relying on a
1233
+ * specific unit — especially with `style="narrow-compact"`, where the unit is visible —
1234
+ * should be aware that the output can be an arbitrarily large day count (e.g. `"1462d"` for
1235
+ * a ~4-year gap between two leap-year endpoints) instead of years on those inputs.
1144
1236
  * @example
1145
1237
  * const startDate = "2023-05-10"
1146
1238
  * const endDate = "2024-05-11"
@@ -1148,10 +1240,10 @@ const timeSinceInMonths = (from, to, timeZone, locale) => {
1148
1240
  * const result = timeSinceInYears(startDate, endDate, 'America/New_York', 'en-US');
1149
1241
  * Output: "1 year ago"
1150
1242
  */
1151
- const timeSinceInYears = (from, to, timeZone, locale) => {
1243
+ const timeSinceInYears = (from, to, timeZone, locale, style) => {
1152
1244
  const isLeapYear = toZonedDateTimeUtil(from, timeZone).inLeapYear && toZonedDateTimeUtil(to, timeZone).inLeapYear;
1153
1245
  if (isLeapYear) {
1154
- return timeSinceInDays(from, to, timeZone, locale);
1246
+ return timeSinceInDays(from, to, timeZone, locale, style);
1155
1247
  }
1156
1248
  let since = new Temporal.Duration();
1157
1249
  try {
@@ -1164,9 +1256,9 @@ const timeSinceInYears = (from, to, timeZone, locale) => {
1164
1256
  console.warn("Error: ", error);
1165
1257
  }
1166
1258
  const duration = Temporal.Duration.from(since);
1167
- return getRelativeTimeFormat(locale).format(duration.years, "years");
1259
+ return formatRelativeUnit(duration.years, "years", locale, style);
1168
1260
  };
1169
- const getRelativeTimeFormat = (locale) => new Intl.RelativeTimeFormat(locale, { style: "long", numeric: "always" });
1261
+ const getRelativeTimeFormat = (locale, style = "long") => new Intl.RelativeTimeFormat(locale, { style, numeric: "always" });
1170
1262
  /**
1171
1263
  * Get duration in human-readable form
1172
1264
  *
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.js","sourceRoot":"","sources":["../../../../../libs/date-and-time/utils/migrations/entry.ts"],"names":[],"mappings":"","sourcesContent":["export {};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/date-and-time-utils",
3
- "version": "1.14.4-alpha-d0f54ff2967.0",
3
+ "version": "1.14.5",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -683,6 +683,43 @@ export declare const isSameYearUtil: (date: TemporalDate) => boolean;
683
683
  * Output: true
684
684
  */
685
685
  export declare const isEqualUtil: (from: TemporalDate, to: TemporalDate) => boolean;
686
+ /**
687
+ * Trackunit-owned relative-time style axis. The single supported value is `"narrow-compact"` —
688
+ * the shortest possible locale-native output for past durations. When `style` is omitted, the
689
+ * default `Intl.RelativeTimeFormat` long output is used (e.g. `"2 days ago"` in en).
690
+ *
691
+ * Past values are pulled from CLDR's `units-narrow` table via `Intl.NumberFormat`
692
+ * `style: "unit"`, which omits any past-tense affix by design (it's a measurement-quantity
693
+ * API — "5 kg", "12 mi"). Future values continue to come from `Intl.RelativeTimeFormat`
694
+ * narrow, which retains the future affix ("in") because CLDR treats it as grammar rather
695
+ * than an appendable string.
696
+ *
697
+ * ### Cross-locale fallback
698
+ * CLDR does not populate `units-narrow` for every locale — notably Japanese (`ja`) has no
699
+ * narrow entries at all, and several other locales lack entries for some units (e.g.
700
+ * Spanish `day`/`month`, Swedish and Norwegian `day`/`minute`/…). Missing slots inherit
701
+ * from CLDR `root`, which renders in Latin (`"2d"`, `"2y"`, …). For Latin-script locales
702
+ * that's cosmetic — the fallback is still in the right script — but for non-Latin locales
703
+ * it silently delocalises the output. To avoid that, when the target locale's script is
704
+ * non-Latin *and* the `NumberFormat` narrow output actually contains Latin letters, this
705
+ * style transparently falls through to `RelativeTimeFormat` narrow for that (locale, unit)
706
+ * combination. That path keeps the past-tense affix, which is unavoidable there, but the
707
+ * affix is short in the affected locales (e.g. Japanese `"2日前"`) and correct localization
708
+ * beats aggressive compaction.
709
+ *
710
+ * ### Known CLDR quirks
711
+ * Because past/future paths read from *different* CLDR tables, the symbol for a given unit
712
+ * may differ between them in a given locale (e.g. English past-month renders as `"3m"` from
713
+ * `units-narrow`, while future-month renders as `"in 3mo"` from `dateFields-narrow`, which
714
+ * CLDR disambiguates against `minute`). Both are the source-of-truth CLDR narrow displays
715
+ * for their respective APIs — this is by design in CLDR, not something we rewrite in JS.
716
+ *
717
+ * ### When to use `"narrow-compact"`
718
+ * When the surrounding UI already makes "past" obvious for negative durations — for example
719
+ * a "last seen" icon or a hover title that repeats the phrase in full. Otherwise omit `style`
720
+ * so the default long form is used, which is unambiguous in isolation.
721
+ */
722
+ export type TuRelativeTimeStyle = "narrow-compact";
686
723
  /**
687
724
  * Calculates the time duration between two TemporalDate objects and returns a human-readable string representation of the duration.
688
725
  *
@@ -690,6 +727,12 @@ export declare const isEqualUtil: (from: TemporalDate, to: TemporalDate) => bool
690
727
  * @param {TemporalDate} to - The ending date.
691
728
  * @param {string} [timeZone] - The time zone to use for the calculations. Defaults to the local time zone.
692
729
  * @param {string} [locale] - The locale to use for formatting the duration. Defaults to the user's locale.
730
+ * @param {TuRelativeTimeStyle} [style] - Optional width override. Pass `"narrow-compact"` to render
731
+ * past values without any past-tense affix (`"2d"` in en, `"2j"` in fr) while future values retain
732
+ * the CLDR-native leading affix (`"in 2d"` in en). Non-Latin-script locales without native CLDR
733
+ * narrow data fall back to `RelativeTimeFormat` narrow to preserve script, which keeps the affix
734
+ * (`"2日前"` in ja) — see {@link TuRelativeTimeStyle} for the CLDR-table-sourcing details.
735
+ * Omit `style` to get the default long form (`"2 days ago"` in en, `"vor 2 Tagen"` in de).
693
736
  * @returns {string|Error} - The formatted duration string or an Error object if the duration is not valid.
694
737
  * @example
695
738
  * const startDate = "2023-05-1010:30:00.000Z"
@@ -709,8 +752,14 @@ export declare const isEqualUtil: (from: TemporalDate, to: TemporalDate) => bool
709
752
  *
710
753
  * const result = timeSinceAuto(startDate, endDate, 'America/New_York', 'en-US');
711
754
  * Output: "1 year ago"
755
+ * @example
756
+ * const startDate = "2023-05-10"
757
+ * const endDate = "2023-05-18"
758
+ *
759
+ * const result = timeSinceAuto(startDate, endDate, 'America/New_York', 'en-US', 'narrow-compact');
760
+ * Output: "8d"
712
761
  */
713
- export declare const timeSinceAuto: (from: TemporalDate, to: TemporalDate, timeZone?: string, locale?: string) => string;
762
+ export declare const timeSinceAuto: (from: TemporalDate, to: TemporalDate, timeZone?: string, locale?: string, style?: TuRelativeTimeStyle) => string;
714
763
  /**
715
764
  * Calculates the time duration between two TemporalDate objects and returns the duration in seconds as a formatted string.
716
765
  *
@@ -726,7 +775,7 @@ export declare const timeSinceAuto: (from: TemporalDate, to: TemporalDate, timeZ
726
775
  * const result = timeSinceInSeconds(startDate, endDate, 'America/New_York', 'en-US');
727
776
  * Output: "480 seconds ago"
728
777
  */
729
- export declare const timeSinceInSeconds: (from: TemporalDate, to: TemporalDate, timeZone?: string, locale?: string) => string;
778
+ export declare const timeSinceInSeconds: (from: TemporalDate, to: TemporalDate, timeZone?: string, locale?: string, style?: TuRelativeTimeStyle) => string;
730
779
  /**
731
780
  * Calculates the time duration between two TemporalDate objects and returns the duration in minutes as a formatted string.
732
781
  *
@@ -742,7 +791,7 @@ export declare const timeSinceInSeconds: (from: TemporalDate, to: TemporalDate,
742
791
  * const result = timeSinceInSeconds(startDate, endDate, 'America/New_York', 'en-US');
743
792
  * Output: "8 minutes ago"
744
793
  */
745
- export declare const timeSinceInMinutes: (from: TemporalDate, to: TemporalDate, timeZone?: string, locale?: string) => string;
794
+ export declare const timeSinceInMinutes: (from: TemporalDate, to: TemporalDate, timeZone?: string, locale?: string, style?: TuRelativeTimeStyle) => string;
746
795
  /**
747
796
  * Calculates the time duration between two TemporalDate objects and returns the duration in hours as a formatted string.
748
797
  *
@@ -758,7 +807,7 @@ export declare const timeSinceInMinutes: (from: TemporalDate, to: TemporalDate,
758
807
  * const result = timeSinceInHours(startDate, endDate);
759
808
  * Output: "3 hours ago"
760
809
  */
761
- export declare const timeSinceInHours: (from: TemporalDate, to: TemporalDate, timeZone?: string, locale?: string) => string;
810
+ export declare const timeSinceInHours: (from: TemporalDate, to: TemporalDate, timeZone?: string, locale?: string, style?: TuRelativeTimeStyle) => string;
762
811
  /**
763
812
  * Calculates the time duration between two TemporalDate objects and returns the duration in days as a formatted string.
764
813
  *
@@ -780,7 +829,7 @@ export declare const timeSinceInHours: (from: TemporalDate, to: TemporalDate, ti
780
829
  * const result = timeSinceInDays(startDate, endDate, 'America/New_York', 'en-US');
781
830
  * Output: "6 days ago"
782
831
  */
783
- export declare const timeSinceInDays: (from: TemporalDate, to: TemporalDate, timeZone?: string, locale?: string) => string;
832
+ export declare const timeSinceInDays: (from: TemporalDate, to: TemporalDate, timeZone?: string, locale?: string, style?: TuRelativeTimeStyle) => string;
784
833
  /**
785
834
  * Calculates the time duration between two TemporalDate objects and returns the duration in months as a formatted string.
786
835
  *
@@ -789,6 +838,11 @@ export declare const timeSinceInDays: (from: TemporalDate, to: TemporalDate, tim
789
838
  * @param {string} [timeZone] - The time zone to use for the calculations. Defaults to the local time zone.
790
839
  * @param {string} [locale] - The locale to use for formatting the duration. Defaults to the user's locale.
791
840
  * @returns {string} - The formatted duration in months.
841
+ * @remarks When both `from` and `to` fall in a leap year, the function falls back to
842
+ * `timeSinceInDays` and returns the raw day count for the whole span. Callers relying on a
843
+ * specific unit — especially with `style="narrow-compact"`, where the unit is visible —
844
+ * should be aware that the output can be an arbitrarily large day count (e.g. `"60d"` for a
845
+ * two-month gap) instead of months on those inputs.
792
846
  * @example
793
847
  * const startDate = "2023-05-10"
794
848
  * const endDate = "2023-07-11"
@@ -796,7 +850,7 @@ export declare const timeSinceInDays: (from: TemporalDate, to: TemporalDate, tim
796
850
  * const result = timeSinceInMonths(startDate, endDate, 'America/New_York', 'en-US');
797
851
  * Output: "2 months ago"
798
852
  */
799
- export declare const timeSinceInMonths: (from: TemporalDate, to: TemporalDate, timeZone?: string, locale?: string) => string;
853
+ export declare const timeSinceInMonths: (from: TemporalDate, to: TemporalDate, timeZone?: string, locale?: string, style?: TuRelativeTimeStyle) => string;
800
854
  /**
801
855
  * Calculates the time duration between two TemporalDate objects and returns the duration in years as a formatted string.
802
856
  *
@@ -805,6 +859,11 @@ export declare const timeSinceInMonths: (from: TemporalDate, to: TemporalDate, t
805
859
  * @param {string} [timeZone] - The time zone to use for the calculations. Defaults to the local time zone.
806
860
  * @param {string} [locale] - The locale to use for formatting the duration. Defaults to the user's locale.
807
861
  * @returns {string} - The formatted duration in years.
862
+ * @remarks When both `from` and `to` fall in a leap year, the function falls back to
863
+ * `timeSinceInDays` and returns the raw day count for the whole span. Callers relying on a
864
+ * specific unit — especially with `style="narrow-compact"`, where the unit is visible —
865
+ * should be aware that the output can be an arbitrarily large day count (e.g. `"1462d"` for
866
+ * a ~4-year gap between two leap-year endpoints) instead of years on those inputs.
808
867
  * @example
809
868
  * const startDate = "2023-05-10"
810
869
  * const endDate = "2024-05-11"
@@ -812,7 +871,7 @@ export declare const timeSinceInMonths: (from: TemporalDate, to: TemporalDate, t
812
871
  * const result = timeSinceInYears(startDate, endDate, 'America/New_York', 'en-US');
813
872
  * Output: "1 year ago"
814
873
  */
815
- export declare const timeSinceInYears: (from: TemporalDate, to: TemporalDate, timeZone?: string, locale?: string) => string;
874
+ export declare const timeSinceInYears: (from: TemporalDate, to: TemporalDate, timeZone?: string, locale?: string, style?: TuRelativeTimeStyle) => string;
816
875
  /**
817
876
  * Get duration in human-readable form
818
877
  *