@jsenv/test 3.7.23 → 3.7.24

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 (2) hide show
  1. package/dist/jsenv_test.js +53 -25
  2. package/package.json +4 -4
@@ -854,44 +854,53 @@ const TIME_DICTIONARY_EN = {
854
854
  second: { long: "second", plural: "seconds", short: "s" },
855
855
  joinDuration: (primary, remaining) => `${primary} and ${remaining}`,
856
856
  };
857
- const TIME_DICTIONARY_FR = {
858
- year: { long: "an", plural: "ans", short: "a" },
859
- month: { long: "mois", plural: "mois", short: "m" },
860
- week: { long: "semaine", plural: "semaines", short: "s" },
861
- day: { long: "jour", plural: "jours", short: "j" },
862
- hour: { long: "heure", plural: "heures", short: "h" },
863
- minute: { long: "minute", plural: "minutes", short: "m" },
864
- second: { long: "seconde", plural: "secondes", short: "s" },
865
- joinDuration: (primary, remaining) => `${primary} et ${remaining}`,
866
- };
867
857
 
858
+ /**
859
+ * Converts a duration in milliseconds into a human-readable string intended for display in
860
+ * CLI output — where readability matters more than precision.
861
+ *
862
+ * - Values below 1ms are displayed as "0 second". Sub-millisecond durations are not
863
+ * meaningful at human scale, and showing "0.0001 second" (or switching to a "millisecond"
864
+ * unit) would hurt readability. The chosen trade-off is to always use "second" as the
865
+ * smallest unit and accept the loss of precision for very small values.
866
+ * - Values below 1s are displayed in fractional seconds (e.g. "0.05 second").
867
+ * - Values are expressed using the two most significant units (e.g. "1 hour and 23 minutes").
868
+ * - Rounding never causes a value to display as the next unit boundary
869
+ * (e.g. 59_999ms → "59.9 seconds", never "60 seconds").
870
+ *
871
+ * @param {number} ms - Duration in milliseconds.
872
+ * @param {object} [options]
873
+ * @param {boolean} [options.short=false] - Use compact unit symbols (e.g. "1h and 23m").
874
+ * @param {boolean} [options.rounded=true] - Round the last displayed digit. When false, truncates instead.
875
+ * @param {number} [options.decimals] - Override the number of decimal places shown.
876
+ * @returns {string}
877
+ */
868
878
  const humanizeDuration = (
869
879
  ms,
870
880
  {
871
881
  short,
872
882
  rounded = true,
873
883
  decimals,
874
- lang = "en",
875
- timeDictionnary = lang === "fr" ? TIME_DICTIONARY_FR : TIME_DICTIONARY_EN,
884
+ timeDictionnary = TIME_DICTIONARY_EN,
876
885
  } = {},
877
886
  ) => {
878
- // ignore ms below meaningfulMs so that:
879
- // humanizeDuration(0.5) -> "0 second"
880
- // humanizeDuration(1.1) -> "0.001 second" (and not "0.0011 second")
881
- // This tool is meant to be read by humans and it would be barely readable to see
882
- // "0.0001 second" (stands for 0.1 millisecond)
883
- // yes we could return "0.1 millisecond" but we choosed consistency over precision
884
- // so that the prefered unit is "second" (and does not become millisecond when ms is super small)
885
887
  if (ms < 1) {
886
- return short
887
- ? `0${timeDictionnary.second.short}`
888
- : `0 ${timeDictionnary.second.long}`;
888
+ if (short) {
889
+ return `0${timeDictionnary.second.short}`;
890
+ }
891
+ return `0 ${timeDictionnary.second.long}`;
889
892
  }
890
893
  const { primary, remaining } = parseMs(ms);
891
894
  if (!remaining) {
895
+ const primaryUnitIndex = UNIT_KEYS.indexOf(primary.name);
896
+ const nextUnitName = UNIT_KEYS[primaryUnitIndex - 1];
897
+ const maxCount = nextUnitName
898
+ ? UNIT_MS[nextUnitName] / UNIT_MS[primary.name]
899
+ : null;
892
900
  return humanizeDurationUnit(primary, {
893
901
  decimals:
894
902
  decimals === undefined ? (primary.name === "second" ? 1 : 0) : decimals,
903
+ maxCount,
895
904
  short,
896
905
  rounded,
897
906
  timeDictionnary,
@@ -909,15 +918,23 @@ const humanizeDuration = (
909
918
  rounded,
910
919
  timeDictionnary,
911
920
  });
921
+ if (short) {
922
+ return `${primaryText}${remainingText}`;
923
+ }
912
924
  return timeDictionnary.joinDuration(primaryText, remainingText);
913
925
  };
914
926
  const humanizeDurationUnit = (
915
927
  unit,
916
- { decimals, short, rounded, timeDictionnary },
928
+ { decimals, maxCount, short, rounded, timeDictionnary },
917
929
  ) => {
918
- const count = rounded
930
+ let count = rounded
919
931
  ? setRoundedPrecision(unit.count, { decimals })
920
932
  : setPrecision(unit.count, { decimals });
933
+ if (maxCount !== null && maxCount !== undefined && count >= maxCount) {
934
+ // Prevent rounding up to the next unit boundary (e.g. 59.999s → 60s → cap to 59.9s)
935
+ const factor = Math.pow(10, decimals ?? 0);
936
+ count = Math.floor(unit.count * factor) / factor;
937
+ }
921
938
  const name = unit.name;
922
939
  if (short) {
923
940
  const unitText = timeDictionnary[name].short;
@@ -970,6 +987,17 @@ const parseMs = (ms) => {
970
987
  },
971
988
  };
972
989
  }
990
+ // When remaining rounds up to a full next-unit (e.g. 59.999s rounds to 60s = 1min),
991
+ // drop the remaining to avoid displaying "59 minutes and 60 seconds".
992
+ const remainingUnitMs = UNIT_MS[remainingUnitName];
993
+ const nextUnitMs = UNIT_MS[firstUnitName];
994
+ const maxRemainingCount = nextUnitMs / remainingUnitMs; // e.g. 60 for seconds-in-a-minute
995
+ // Cap remaining so it never rounds up to the next unit boundary
996
+ // (e.g. 59.5s stays as 59s instead of rounding to 60s = 1min)
997
+ const cappedRemainingCount =
998
+ remainingUnitCount >= maxRemainingCount - 1
999
+ ? maxRemainingCount - 1
1000
+ : remainingUnitCount;
973
1001
  // - 1 year and 1 month is great
974
1002
  return {
975
1003
  primary: {
@@ -978,7 +1006,7 @@ const parseMs = (ms) => {
978
1006
  },
979
1007
  remaining: {
980
1008
  name: remainingUnitName,
981
- count: remainingUnitCount,
1009
+ count: cappedRemainingCount,
982
1010
  },
983
1011
  };
984
1012
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/test",
3
- "version": "3.7.23",
3
+ "version": "3.7.24",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,9 +30,9 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@c88/v8-coverage": "0.1.1",
33
- "@jsenv/ast": "6.8.2",
34
- "@jsenv/plugin-supervisor": "1.8.3",
35
- "@jsenv/sourcemap": "1.3.18",
33
+ "@jsenv/ast": "6.8.3",
34
+ "@jsenv/plugin-supervisor": "1.8.4",
35
+ "@jsenv/sourcemap": "1.3.19",
36
36
  "he": "1.2.0",
37
37
  "istanbul-lib-coverage": "3.2.2",
38
38
  "istanbul-lib-instrument": "6.0.3",