@jsenv/humanize 1.7.7 → 1.7.8

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.
@@ -677,10 +677,13 @@ const derivedErrorNameArray = [
677
677
  ];
678
678
 
679
679
  const inspectDate = (value, { nestedHumanize, useNew, parenthesis }) => {
680
- const dateSource = nestedHumanize(value.valueOf(), {
680
+ const dateSource = nestedHumanize(value.toISOString(), {
681
681
  numericSeparator: false,
682
682
  });
683
- return inspectConstructor(`Date(${dateSource})`, { useNew, parenthesis });
683
+ return inspectConstructor(`Date(${dateSource})`, {
684
+ useNew,
685
+ parenthesis,
686
+ });
684
687
  };
685
688
 
686
689
  const inspectFunction = (
@@ -1014,29 +1017,16 @@ const TIME_DICTIONARY_EN = {
1014
1017
  second: { long: "second", plural: "seconds", short: "s" },
1015
1018
  joinDuration: (primary, remaining) => `${primary} and ${remaining}`,
1016
1019
  };
1017
- const TIME_DICTIONARY_FR = {
1018
- year: { long: "an", plural: "ans", short: "a" },
1019
- month: { long: "mois", plural: "mois", short: "m" },
1020
- week: { long: "semaine", plural: "semaines", short: "s" },
1021
- day: { long: "jour", plural: "jours", short: "j" },
1022
- hour: { long: "heure", plural: "heures", short: "h" },
1023
- minute: { long: "minute", plural: "minutes", short: "m" },
1024
- second: { long: "seconde", plural: "secondes", short: "s" },
1025
- joinDuration: (primary, remaining) => `${primary} et ${remaining}`,
1026
- };
1027
1020
 
1028
1021
  const humanizeEllapsedTime = (
1029
1022
  ms,
1030
- {
1031
- short,
1032
- lang = "en",
1033
- timeDictionnary = lang === "fr" ? TIME_DICTIONARY_FR : TIME_DICTIONARY_EN,
1034
- } = {},
1023
+ { short, timeDictionnary = TIME_DICTIONARY_EN } = {},
1035
1024
  ) => {
1036
1025
  if (ms < 1000) {
1037
- return short
1038
- ? `0${timeDictionnary.second.short}`
1039
- : `0 ${timeDictionnary.second.long}`;
1026
+ if (short) {
1027
+ return `0${timeDictionnary.second.short}`;
1028
+ }
1029
+ return `0 ${timeDictionnary.second.long}`;
1040
1030
  }
1041
1031
  const { primary, remaining } = parseMs(ms);
1042
1032
  if (!remaining) {
@@ -1068,33 +1058,52 @@ const inspectEllapsedUnit = (unit, { short, timeDictionnary }) => {
1068
1058
  return `${count} ${unitText}`;
1069
1059
  };
1070
1060
 
1061
+ /**
1062
+ * Converts a duration in milliseconds into a human-readable string intended for display in
1063
+ * CLI output — where readability matters more than precision.
1064
+ *
1065
+ * - Values below 1ms are displayed as "0 second". Sub-millisecond durations are not
1066
+ * meaningful at human scale, and showing "0.0001 second" (or switching to a "millisecond"
1067
+ * unit) would hurt readability. The chosen trade-off is to always use "second" as the
1068
+ * smallest unit and accept the loss of precision for very small values.
1069
+ * - Values below 1s are displayed in fractional seconds (e.g. "0.05 second").
1070
+ * - Values are expressed using the two most significant units (e.g. "1 hour and 23 minutes").
1071
+ * - Rounding never causes a value to display as the next unit boundary
1072
+ * (e.g. 59_999ms → "59.9 seconds", never "60 seconds").
1073
+ *
1074
+ * @param {number} ms - Duration in milliseconds.
1075
+ * @param {object} [options]
1076
+ * @param {boolean} [options.short=false] - Use compact unit symbols (e.g. "1h and 23m").
1077
+ * @param {boolean} [options.rounded=true] - Round the last displayed digit. When false, truncates instead.
1078
+ * @param {number} [options.decimals] - Override the number of decimal places shown.
1079
+ * @returns {string}
1080
+ */
1071
1081
  const humanizeDuration = (
1072
1082
  ms,
1073
1083
  {
1074
1084
  short,
1075
1085
  rounded = true,
1076
1086
  decimals,
1077
- lang = "en",
1078
- timeDictionnary = lang === "fr" ? TIME_DICTIONARY_FR : TIME_DICTIONARY_EN,
1087
+ timeDictionnary = TIME_DICTIONARY_EN,
1079
1088
  } = {},
1080
1089
  ) => {
1081
- // ignore ms below meaningfulMs so that:
1082
- // humanizeDuration(0.5) -> "0 second"
1083
- // humanizeDuration(1.1) -> "0.001 second" (and not "0.0011 second")
1084
- // This tool is meant to be read by humans and it would be barely readable to see
1085
- // "0.0001 second" (stands for 0.1 millisecond)
1086
- // yes we could return "0.1 millisecond" but we choosed consistency over precision
1087
- // so that the prefered unit is "second" (and does not become millisecond when ms is super small)
1088
1090
  if (ms < 1) {
1089
- return short
1090
- ? `0${timeDictionnary.second.short}`
1091
- : `0 ${timeDictionnary.second.long}`;
1091
+ if (short) {
1092
+ return `0${timeDictionnary.second.short}`;
1093
+ }
1094
+ return `0 ${timeDictionnary.second.long}`;
1092
1095
  }
1093
1096
  const { primary, remaining } = parseMs(ms);
1094
1097
  if (!remaining) {
1098
+ const primaryUnitIndex = UNIT_KEYS.indexOf(primary.name);
1099
+ const nextUnitName = UNIT_KEYS[primaryUnitIndex - 1];
1100
+ const maxCount = nextUnitName
1101
+ ? UNIT_MS[nextUnitName] / UNIT_MS[primary.name]
1102
+ : null;
1095
1103
  return humanizeDurationUnit(primary, {
1096
1104
  decimals:
1097
1105
  decimals === undefined ? (primary.name === "second" ? 1 : 0) : decimals,
1106
+ maxCount,
1098
1107
  short,
1099
1108
  rounded,
1100
1109
  timeDictionnary,
@@ -1112,15 +1121,23 @@ const humanizeDuration = (
1112
1121
  rounded,
1113
1122
  timeDictionnary,
1114
1123
  });
1124
+ if (short) {
1125
+ return `${primaryText}${remainingText}`;
1126
+ }
1115
1127
  return timeDictionnary.joinDuration(primaryText, remainingText);
1116
1128
  };
1117
1129
  const humanizeDurationUnit = (
1118
1130
  unit,
1119
- { decimals, short, rounded, timeDictionnary },
1131
+ { decimals, maxCount, short, rounded, timeDictionnary },
1120
1132
  ) => {
1121
- const count = rounded
1133
+ let count = rounded
1122
1134
  ? setRoundedPrecision(unit.count, { decimals })
1123
1135
  : setPrecision(unit.count, { decimals });
1136
+ if (maxCount !== null && maxCount !== undefined && count >= maxCount) {
1137
+ // Prevent rounding up to the next unit boundary (e.g. 59.999s → 60s → cap to 59.9s)
1138
+ const factor = Math.pow(10, decimals ?? 0);
1139
+ count = Math.floor(unit.count * factor) / factor;
1140
+ }
1124
1141
  const name = unit.name;
1125
1142
  if (short) {
1126
1143
  const unitText = timeDictionnary[name].short;
@@ -1173,6 +1190,17 @@ const parseMs = (ms) => {
1173
1190
  },
1174
1191
  };
1175
1192
  }
1193
+ // When remaining rounds up to a full next-unit (e.g. 59.999s rounds to 60s = 1min),
1194
+ // drop the remaining to avoid displaying "59 minutes and 60 seconds".
1195
+ const remainingUnitMs = UNIT_MS[remainingUnitName];
1196
+ const nextUnitMs = UNIT_MS[firstUnitName];
1197
+ const maxRemainingCount = nextUnitMs / remainingUnitMs; // e.g. 60 for seconds-in-a-minute
1198
+ // Cap remaining so it never rounds up to the next unit boundary
1199
+ // (e.g. 59.5s stays as 59s instead of rounding to 60s = 1min)
1200
+ const cappedRemainingCount =
1201
+ remainingUnitCount >= maxRemainingCount - 1
1202
+ ? maxRemainingCount - 1
1203
+ : remainingUnitCount;
1176
1204
  // - 1 year and 1 month is great
1177
1205
  return {
1178
1206
  primary: {
@@ -1181,7 +1209,7 @@ const parseMs = (ms) => {
1181
1209
  },
1182
1210
  remaining: {
1183
1211
  name: remainingUnitName,
1184
- count: remainingUnitCount,
1212
+ count: cappedRemainingCount,
1185
1213
  },
1186
1214
  };
1187
1215
  };