@dative-gpi/foundation-shared-services 1.0.190 → 1.0.191-add-unit-formatter-2
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/composables/index.ts +3 -1
- package/composables/useTimeDuration.ts +75 -0
- package/composables/useUnitFormatter.ts +201 -0
- package/config/index.ts +2 -0
- package/config/timeDuration/index.ts +1 -0
- package/config/timeDuration/timePerPeriod.ts +4 -0
- package/config/units/index.ts +1 -0
- package/config/units/unitPrefixes.ts +13 -0
- package/package.json +3 -3
package/composables/index.ts
CHANGED
|
@@ -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,201 @@
|
|
|
1
|
+
import { useAppLanguageCode } from "@dative-gpi/foundation-shared-services/composables";
|
|
2
|
+
import { SI_PREFIXES } from "@dative-gpi/foundation-shared-services/config/units";
|
|
3
|
+
import { type UnitDefinition, unitRegistry } from "@dative-gpi/foundation-shared-domain/models";
|
|
4
|
+
|
|
5
|
+
export function useUnitFormatter() {
|
|
6
|
+
const { languageCode } = useAppLanguageCode();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Converts a value from one unit to another of the same family
|
|
10
|
+
*/
|
|
11
|
+
function convert(value: number, fromUnit: string, toUnit: string): number {
|
|
12
|
+
const sourceUnitDefinition = unitRegistry[fromUnit];
|
|
13
|
+
const targetUnitDefinition = unitRegistry[toUnit];
|
|
14
|
+
|
|
15
|
+
if (!sourceUnitDefinition?.family || !targetUnitDefinition?.family) {
|
|
16
|
+
throw new Error(`Cannot convert between ${fromUnit} and ${toUnit}: missing family`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (sourceUnitDefinition.family !== targetUnitDefinition.family) {
|
|
20
|
+
throw new Error(`Cannot convert between different families: ${sourceUnitDefinition.family} vs ${targetUnitDefinition.family}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Conversion via the pivot: pivot_value = sourceUnitDefinition_value * sourceUnitDefinition_factor
|
|
24
|
+
// targetUnitDefinition_value = pivot_value / targetUnitDefinition_factor
|
|
25
|
+
return (value * (sourceUnitDefinition.factor ?? 1)) / (targetUnitDefinition.factor ?? 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Finds the best unit in a family to display a value
|
|
30
|
+
* (the one that gives a result between 1 and 999)
|
|
31
|
+
*/
|
|
32
|
+
function findBestUnit(value: number, currentUnit: string, fixedUnit?: string): string {
|
|
33
|
+
const unitDef = unitRegistry[currentUnit];
|
|
34
|
+
|
|
35
|
+
if (!unitDef?.family) {
|
|
36
|
+
return currentUnit;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Convert the value to the pivot (in absolute value)
|
|
40
|
+
const absoluteValueInPivot = Math.abs(value * (unitDef.factor ?? 1));
|
|
41
|
+
|
|
42
|
+
// Find all units in the same family
|
|
43
|
+
const unitsInSameFamily = Object.entries(unitRegistry)
|
|
44
|
+
.filter(([, def]) => def.family === unitDef.family)
|
|
45
|
+
.map(([unit, def]) => ({
|
|
46
|
+
unit,
|
|
47
|
+
factor: def.factor ?? 1,
|
|
48
|
+
precision: def.precision,
|
|
49
|
+
}))
|
|
50
|
+
.sort((a, b) => a.factor - b.factor); // Sort by ascending factor
|
|
51
|
+
|
|
52
|
+
// Determine the maximum allowed factor
|
|
53
|
+
const maxFactor = fixedUnit && unitRegistry[fixedUnit]?.family === unitDef.family
|
|
54
|
+
? unitRegistry[fixedUnit]?.factor ?? Infinity
|
|
55
|
+
: Infinity;
|
|
56
|
+
|
|
57
|
+
// Find the unit that gives a result >= 1 and < 1000
|
|
58
|
+
let bestUnit = unitsInSameFamily[0]?.unit ?? currentUnit; // Default: the smallest unit
|
|
59
|
+
|
|
60
|
+
for (let i = unitsInSameFamily.length - 1; i >= 0; i--) {
|
|
61
|
+
const candidateUnit = unitsInSameFamily[i];
|
|
62
|
+
|
|
63
|
+
// Do not exceed fixedUnit
|
|
64
|
+
if (candidateUnit.factor > maxFactor) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const valueInCandidateUnit = absoluteValueInPivot / candidateUnit.factor;
|
|
69
|
+
|
|
70
|
+
// Find the unit that gives a result >= 1
|
|
71
|
+
if (valueInCandidateUnit >= 1) {
|
|
72
|
+
bestUnit = candidateUnit.unit;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return bestUnit;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Applies alternative conversions (non-family)
|
|
82
|
+
* These conversions take priority over the family system
|
|
83
|
+
*/
|
|
84
|
+
function applyAlternativeConversions(value: number, unit: string, unitDefinition: UnitDefinition) {
|
|
85
|
+
if (!unitDefinition.conversions?.length) {
|
|
86
|
+
return { value, unit };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const conversion of unitDefinition.conversions) {
|
|
90
|
+
if (conversion.minThreshold != null && Math.abs(value) < conversion.minThreshold) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
value: value * conversion.conversionRate,
|
|
96
|
+
unit: conversion.targetUnit
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { value, unit };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Applies SI prefixes for unknown units
|
|
105
|
+
*/
|
|
106
|
+
function applySIScale(value: number, unit: string) {
|
|
107
|
+
const absoluteValue = Math.abs(value);
|
|
108
|
+
|
|
109
|
+
let selectedPrefix = SI_PREFIXES[3]; // None
|
|
110
|
+
|
|
111
|
+
for (let i = SI_PREFIXES.length - 1; i >= 0; i--) {
|
|
112
|
+
const currentPrefix = SI_PREFIXES[i];
|
|
113
|
+
|
|
114
|
+
if (absoluteValue >= currentPrefix.factor) {
|
|
115
|
+
selectedPrefix = currentPrefix;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
value: value / selectedPrefix.factor,
|
|
122
|
+
unit: `${selectedPrefix.prefix}${unit}`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formatNumber(value: number, decimalPrecision: number, locale?: string) {
|
|
127
|
+
return new Intl.NumberFormat(locale, {
|
|
128
|
+
minimumFractionDigits: 0,
|
|
129
|
+
maximumFractionDigits: decimalPrecision,
|
|
130
|
+
}).format(value);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const formatQuantity = (value: number, unit: string, options?: { decimalPrecision?: number; fixedUnit?: string; } ): string => {
|
|
134
|
+
if (!isFinite(value)) {
|
|
135
|
+
return "—";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const originalUnitDefinition = unitRegistry[unit];
|
|
139
|
+
let convertedResult: { value: number; unit: string };
|
|
140
|
+
let decimalPrecision: number;
|
|
141
|
+
|
|
142
|
+
if (originalUnitDefinition) {
|
|
143
|
+
convertedResult = applyAlternativeConversions(value, unit, originalUnitDefinition);
|
|
144
|
+
|
|
145
|
+
const convertedUnitDefinition = unitRegistry[convertedResult.unit];
|
|
146
|
+
if (convertedUnitDefinition?.family) {
|
|
147
|
+
const bestUnit = findBestUnit(convertedResult.value, convertedResult.unit, options?.fixedUnit);
|
|
148
|
+
|
|
149
|
+
if (bestUnit !== convertedResult.unit) {
|
|
150
|
+
convertedResult.value = convert(convertedResult.value, convertedResult.unit, bestUnit);
|
|
151
|
+
convertedResult.unit = bestUnit;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
decimalPrecision = options?.decimalPrecision ?? (unitRegistry[convertedResult.unit]?.precision ?? 2);
|
|
156
|
+
} else {
|
|
157
|
+
convertedResult = applySIScale(value, unit);
|
|
158
|
+
decimalPrecision = options?.decimalPrecision ?? 2;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const finalUnitSymbol = unitRegistry[convertedResult.unit]?.symbol ?? convertedResult.unit;
|
|
162
|
+
const formattedValue = formatNumber(convertedResult.value, decimalPrecision, languageCode.value);
|
|
163
|
+
|
|
164
|
+
return `${formattedValue} ${finalUnitSymbol}`;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const formatQuantityParts = (value: number, unit: string, options?: { decimalPrecision?: number; fixedUnit?: string; } ): { value: string; unit: string } => {
|
|
168
|
+
|
|
169
|
+
const fullFormattedQuantity = formatQuantity(value, unit, { decimalPrecision: options?.decimalPrecision,fixedUnit: options?.fixedUnit});
|
|
170
|
+
|
|
171
|
+
if (fullFormattedQuantity === "—") {
|
|
172
|
+
return {
|
|
173
|
+
value: "—",
|
|
174
|
+
unit: ""
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const indexOfLastSpace = fullFormattedQuantity.lastIndexOf(" ");
|
|
179
|
+
|
|
180
|
+
if (indexOfLastSpace === -1) {
|
|
181
|
+
return {
|
|
182
|
+
value: fullFormattedQuantity,
|
|
183
|
+
unit: ""
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const valueWithoutUnit = fullFormattedQuantity.slice(0, indexOfLastSpace);
|
|
188
|
+
const unitOnly = fullFormattedQuantity.slice(indexOfLastSpace + 1);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
value: valueWithoutUnit,
|
|
192
|
+
unit: unitOnly
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
formatQuantity,
|
|
198
|
+
formatQuantityParts,
|
|
199
|
+
convert
|
|
200
|
+
};
|
|
201
|
+
}
|
package/config/index.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./timePerPeriod";
|
|
@@ -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.
|
|
7
|
+
"version": "1.0.191-add-unit-formatter-2",
|
|
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.
|
|
16
|
+
"@dative-gpi/foundation-shared-domain": "1.0.191-add-unit-formatter-2",
|
|
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": "
|
|
25
|
+
"gitHead": "7c34bcea5b12a9461c14adbdb2227f6dda0d46aa"
|
|
26
26
|
}
|