@dative-gpi/foundation-shared-services 1.0.192 → 1.0.193-test-unit-formatter

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.
@@ -5,4 +5,6 @@ export * from "./useDateFormat";
5
5
  export * from "./useFiles";
6
6
  export * from "./useFoundationShared";
7
7
  export * from "./useRouting";
8
- export * from "./useDateExpression";
8
+ export * from "./useDateExpression";
9
+ export * from "./useTimeDuration";
10
+ export * from "./useUnitFormatter";
@@ -0,0 +1,75 @@
1
+ import { TimeComparisonType } from "@dative-gpi/foundation-shared-domain/enums";
2
+ import { useDateExpression } from "@dative-gpi/foundation-shared-services/composables";
3
+ import { MILLISECONDS_PER_DAY, MILLISECONDS_PER_WEEK, MILLISECONDS_PER_MONTH_APPROX, MILLISECONDS_PER_YEAR_APPROX } from "@dative-gpi/foundation-shared-services/config";
4
+
5
+ export const useTimeDuration = () => {
6
+ /**
7
+ * Computes the duration between two timestamps, in **milliseconds**.
8
+ *
9
+ * Returns `null` if:
10
+ * - `periodStart` or `periodEnd` is missing,
11
+ * - a timestamp cannot be converted to a number,
12
+ * - the end is equal to or before the start.
13
+ *
14
+ * @param periodStart Date fixe (ISO-8601) ou expression relative (ex: 'now-1h', 'now/d'), ou `undefined`.
15
+ * @param periodEnd Date fixe (ISO-8601) ou expression relative (ex: 'now', 'now-1d/d'), ou `undefined`.
16
+ * @returns Durée en millisecondes, ou `0` si l'expression est invalide ou si la fin est avant le début.
17
+ */
18
+ const getPeriodDuration = (periodStart: string | undefined, periodEnd: string | undefined): number | null => {
19
+ if (!periodStart || !periodEnd) {
20
+ return null;
21
+ }
22
+ const { convert } = useDateExpression();
23
+
24
+ const startTimestamp = convert(periodStart);
25
+ const endTimestamp = convert(periodEnd);
26
+
27
+ if (isNaN(startTimestamp) || isNaN(endTimestamp)) {
28
+ return null;
29
+ }
30
+
31
+ const durationMs = endTimestamp - startTimestamp;
32
+ return durationMs > 0 ? durationMs : null;
33
+ };
34
+
35
+ /**
36
+ * Returns the duration (in **milliseconds**) for a given {@link TimeComparisonType}.
37
+ *
38
+ * - `Daily` / `Weekly`: fixed constants.
39
+ * - `Monthly` / `Yearly`: **approximate** constants (not exact calendar lengths).
40
+ * - `SinceReference`: duration between `periodStart` and `periodEnd` (needed only for this mode).
41
+ * - `None` / `Absolute`: returns `null` (no implied fixed duration).
42
+ *
43
+ * @param timeframe Timeframe mode to convert into a duration.
44
+ * @param periodStart Optional start timestamp string (usually ISO-8601). Used only for `SinceReference`.
45
+ * @param periodEnd Optional end timestamp string (usually ISO-8601). Used only for `SinceReference`.
46
+ * @returns Duration in **milliseconds**.
47
+ */
48
+ const getTimeframeDuration = (timeframe: TimeComparisonType, periodStart: string | null = null, periodEnd: string | null = null): number | null => {
49
+ switch (timeframe) {
50
+ case TimeComparisonType.SinceReference:
51
+ return getPeriodDuration(periodStart ?? undefined, periodEnd ?? undefined);
52
+ case TimeComparisonType.Daily:
53
+ return MILLISECONDS_PER_DAY;
54
+ case TimeComparisonType.Weekly:
55
+ return MILLISECONDS_PER_WEEK;
56
+ case TimeComparisonType.Monthly:
57
+ return MILLISECONDS_PER_MONTH_APPROX;
58
+ case TimeComparisonType.Yearly:
59
+ return MILLISECONDS_PER_YEAR_APPROX;
60
+ case TimeComparisonType.None:
61
+ // No timeframe selected: no duration.
62
+ return null;
63
+ case TimeComparisonType.Absolute:
64
+ // Absolute comparisons do not imply a fixed duration.
65
+ return null;
66
+ default:
67
+ return null;
68
+ }
69
+ };
70
+
71
+ return {
72
+ getPeriodDuration,
73
+ getTimeframeDuration,
74
+ };
75
+ }
@@ -0,0 +1,230 @@
1
+ import { useAppLanguageCode } from "@dative-gpi/foundation-shared-services/composables";
2
+ import { SI_PREFIXES } from "@dative-gpi/foundation-shared-services/config/units";
3
+ import { unitRegistry, unitFamilies, type UnitDefinition } from "@dative-gpi/foundation-shared-domain/models";
4
+
5
+ export function useUnitFormatter() {
6
+ const { languageCode } = useAppLanguageCode();
7
+
8
+ function findBestSIPrefix(value: number): { prefix: string; factor: number } {
9
+ if (value === 0) {
10
+ return SI_PREFIXES[3];
11
+ }
12
+
13
+ const absValue = Math.abs(value);
14
+ const magnitude = Math.floor(Math.log10(absValue) / 3) * 3;
15
+
16
+ const prefixIndex = Math.floor((magnitude + 9) / 3);
17
+
18
+ return SI_PREFIXES[Math.max(0, Math.min(prefixIndex, SI_PREFIXES.length - 1))];
19
+ }
20
+
21
+ function parseUnitWithPrefix(unitString: string): { prefix: string; baseUnit: string } {
22
+ const s = unitString.trim();
23
+
24
+ // 1) Priorité au match exact (évite "m" => prefix "m" + base "")
25
+ // et évite "Pa" => prefix "P" + base "a"
26
+ if (unitRegistry[s]) {
27
+ return { prefix: "", baseUnit: s };
28
+ }
29
+
30
+ // 2) Préfixes SI (sans ""), du plus long au plus court (robuste si un jour tu ajoutes "da")
31
+ const prefixes = SI_PREFIXES
32
+ .map(p => p.prefix)
33
+ .filter(p => p !== "")
34
+ .sort((a, b) => b.length - a.length);
35
+
36
+ // 3) On n'accepte un préfixe que si le reste est une unité connue du registry
37
+ for (const prefix of prefixes) {
38
+ if (!s.startsWith(prefix)) {continue;}
39
+
40
+ const baseUnit = s.slice(prefix.length);
41
+ if (!baseUnit) {continue;}
42
+
43
+ if (unitRegistry[baseUnit]) {
44
+ return { prefix, baseUnit };
45
+ }
46
+ }
47
+
48
+ // 4) Aucun préfixe valide détecté => on garde l'unité telle quelle
49
+ return { prefix: "", baseUnit: s };
50
+ }
51
+
52
+
53
+ function findPrefixByName(prefixName: string): { prefix: string; factor: number } | null {
54
+ const found = SI_PREFIXES.find(p => p.prefix === prefixName);
55
+ return found || null;
56
+ }
57
+
58
+ function convertWithinFamily(value: number, fromUnit: string, toUnit: string): number {
59
+ const from = unitRegistry[fromUnit];
60
+ const to = unitRegistry[toUnit];
61
+
62
+ if (!from || !to) {
63
+ throw new Error(`Unknown units: ${fromUnit} or ${toUnit}`);
64
+ }
65
+
66
+ if (from.family !== to.family) {
67
+ throw new Error(`Different families: ${from.family} vs ${to.family}`);
68
+ }
69
+
70
+ return (value * from.toPivot) / to.toPivot;
71
+ }
72
+
73
+
74
+ function applySpecialConversions(value: number, unit: string, unitDefinition: UnitDefinition ): { value: number; unit: string } {
75
+ if (!unitDefinition.specialConversions?.length) {
76
+ return { value, unit };
77
+ }
78
+
79
+ for (const conversion of unitDefinition.specialConversions) {
80
+ if (Math.abs(value) >= conversion.threshold) {
81
+ const convertedValue = convertWithinFamily(value, unit, conversion.toUnit);
82
+
83
+ return { value: convertedValue, unit: conversion.toUnit };
84
+ }
85
+ }
86
+
87
+ return { value, unit };
88
+ }
89
+
90
+ function selectBestUnit(value: number, unit: string, unitPrecision?: string): { value: number; unit: string; symbol: string; } {
91
+ const unitSourceDefinition = unitRegistry[unit];
92
+
93
+ // Unknown unit : apply SI prefixes only
94
+ if (!unitSourceDefinition) {
95
+ const prefix = findBestSIPrefix(value);
96
+ const scaledValue = value / prefix.factor;
97
+ return {
98
+ value: scaledValue,
99
+ unit: `${prefix.prefix}${unit}`,
100
+ symbol: `${prefix.prefix}${unit}`
101
+ };
102
+ }
103
+
104
+ // if unitPrecision is specified and valid
105
+ if (unitPrecision) {
106
+ const parsed = parseUnitWithPrefix(unitPrecision);
107
+ const precisionUnitDefinition = unitRegistry[parsed.baseUnit];
108
+
109
+ if (precisionUnitDefinition && precisionUnitDefinition.family === unitSourceDefinition.family) {
110
+ // Convert value to the precision unit
111
+ const valueInPrecision = convertWithinFamily(value, unit, parsed.baseUnit);
112
+
113
+ // If a prefix is specified in unitPrecision, apply it EXACTLY
114
+ if (parsed.prefix) {
115
+ const forcedPrefix = findPrefixByName(parsed.prefix);
116
+ if (forcedPrefix) {
117
+ const scaledValue = valueInPrecision / forcedPrefix.factor;
118
+ return {
119
+ value: scaledValue,
120
+ unit: parsed.baseUnit,
121
+ symbol: `${forcedPrefix.prefix}${precisionUnitDefinition.symbol}`,
122
+ };
123
+ }
124
+ }
125
+
126
+ // No prefix specified in unitPrecision: return WITHOUT any SI prefix
127
+ return {
128
+ value: valueInPrecision,
129
+ unit: parsed.baseUnit,
130
+ symbol: precisionUnitDefinition.symbol,
131
+ };
132
+ }
133
+ }
134
+
135
+ // Apply special conversions
136
+ const afterSpecial = applySpecialConversions(value, unit, unitSourceDefinition);
137
+ if (afterSpecial.unit !== unit) {
138
+ return selectBestUnit(afterSpecial.value, afterSpecial.unit, unitPrecision);
139
+ }
140
+
141
+ // if the unit support SI prefix
142
+ if (unitSourceDefinition.usesSIPrefixes) {
143
+ const prefix = findBestSIPrefix(value);
144
+ const scaledValue = value / prefix.factor;
145
+ return {
146
+ value: scaledValue,
147
+ unit,
148
+ symbol: `${prefix.prefix}${unitSourceDefinition.symbol}`,
149
+ };
150
+ }
151
+
152
+ return {
153
+ value,
154
+ unit,
155
+ symbol: unitSourceDefinition.symbol,
156
+ };
157
+ }
158
+
159
+ function formatNumber(value: number, precision: number, locale?: string): string {
160
+ return new Intl.NumberFormat(locale, {
161
+ minimumFractionDigits: 0,
162
+ maximumFractionDigits: precision,
163
+ }).format(value);
164
+ }
165
+
166
+ /**
167
+ * take a value with its source unit, convert it to targetUnit (if specified),
168
+ * find the best SI prefix (unless unitPrecision is specified),
169
+ * and format it as a string.
170
+ */
171
+ function formatQuantity(valueToConvert: number, sourceUnit: string, options?: { targetUnit?: string; unitPrecision?: string; decimalPrecision?: number; }):
172
+ { formatted: string;
173
+ value: string;
174
+ unit: string;
175
+ } {
176
+ if (!isFinite(valueToConvert)) {
177
+ return {
178
+ formatted: "—",
179
+ value: "—",
180
+ unit: ""
181
+ };
182
+ }
183
+
184
+ const sourceUnitDef = unitRegistry[sourceUnit];
185
+ let finalValue = valueToConvert;
186
+ let finalUnit = sourceUnit;
187
+
188
+ if (options?.targetUnit && options.targetUnit !== sourceUnit) {
189
+ const targetUnitDef = unitRegistry[options.targetUnit];
190
+
191
+ if (!sourceUnitDef || !targetUnitDef) {
192
+ throw new Error(`Unknown unit: ${sourceUnit} or ${options.targetUnit}`);
193
+ }
194
+
195
+ // Conversion with custom function (temperatures)
196
+ const family = unitFamilies[sourceUnitDef.family];
197
+ if (family.customConverter) {
198
+ finalValue = family.customConverter(valueToConvert, sourceUnit, options.targetUnit);
199
+ finalUnit = options.targetUnit;
200
+ }
201
+ // Conversion between units of the same family
202
+ else if (sourceUnitDef.family === targetUnitDef.family) {
203
+ finalValue = convertWithinFamily(valueToConvert, sourceUnit, options.targetUnit);
204
+ finalUnit = options.targetUnit;
205
+ }
206
+ // Different families: error
207
+ else {
208
+ throw new Error(`Cannot convert between ${sourceUnitDef.family} and ${targetUnitDef.family}`);
209
+ }
210
+ }
211
+
212
+ // Find the best prefix
213
+ const result = selectBestUnit(finalValue, finalUnit, options?.unitPrecision);
214
+
215
+ const decimalPrecision = options?.decimalPrecision ?? 2;
216
+
217
+ const formattedValue = formatNumber(result.value, decimalPrecision, languageCode.value);
218
+ const formatted = `${formattedValue} ${result.symbol}`;
219
+
220
+ return {
221
+ formatted,
222
+ value: formattedValue,
223
+ unit: result.symbol
224
+ };
225
+ }
226
+
227
+ return {
228
+ formatQuantity
229
+ };
230
+ }
package/config/index.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export * from "./literals";
2
+ export * from "./timeDuration";
3
+ export * from "./units";
2
4
  export * from "./urls";
@@ -0,0 +1 @@
1
+ export * from "./timePerPeriod";
@@ -0,0 +1,4 @@
1
+ export const MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24;
2
+ export const MILLISECONDS_PER_WEEK = MILLISECONDS_PER_DAY * 7;
3
+ export const MILLISECONDS_PER_MONTH_APPROX = MILLISECONDS_PER_DAY * 30;
4
+ export const MILLISECONDS_PER_YEAR_APPROX = MILLISECONDS_PER_DAY * 365;
@@ -0,0 +1 @@
1
+ export * from "./unitPrefixes";
@@ -0,0 +1,13 @@
1
+ import { UnitPrefix } from "@dative-gpi/foundation-shared-domain/enums/units";
2
+
3
+ export const SI_PREFIXES = [
4
+ { prefix: UnitPrefix.Nano, factor: 1e-9 },
5
+ { prefix: UnitPrefix.Micro, factor: 1e-6 },
6
+ { prefix: UnitPrefix.Milli, factor: 1e-3 },
7
+ { prefix: UnitPrefix.None, factor: 1 },
8
+ { prefix: UnitPrefix.Kilo, factor: 1e3 },
9
+ { prefix: UnitPrefix.Mega, factor: 1e6 },
10
+ { prefix: UnitPrefix.Giga, factor: 1e9 },
11
+ { prefix: UnitPrefix.Tera, factor: 1e12 },
12
+ { prefix: UnitPrefix.Peta, factor: 1e15 },
13
+ ];
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "url": "https://github.com/Dative-GPI/foundation-shared-ui.git"
5
5
  },
6
6
  "sideEffects": false,
7
- "version": "1.0.192",
7
+ "version": "1.0.193-test-unit-formatter",
8
8
  "description": "",
9
9
  "publishConfig": {
10
10
  "access": "public"
@@ -13,7 +13,7 @@
13
13
  "author": "",
14
14
  "license": "ISC",
15
15
  "dependencies": {
16
- "@dative-gpi/foundation-shared-domain": "1.0.192",
16
+ "@dative-gpi/foundation-shared-domain": "1.0.193-test-unit-formatter",
17
17
  "@vueuse/core": "^14.0.0"
18
18
  },
19
19
  "peerDependencies": {
@@ -22,5 +22,5 @@
22
22
  "vue": "^3.4.38",
23
23
  "vue-router": "^4.3.0"
24
24
  },
25
- "gitHead": "ee4d3a1f2581f8fc1765618f7b7b0aeb6368a0b2"
25
+ "gitHead": "fc948d90c3e19270003dc7f695459ae96bf1539c"
26
26
  }