fast-publicodes 0.0.0 → 1.0.0
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/LICENSE +26 -0
- package/README.md +83 -0
- package/dist/api.d.ts +11 -0
- package/dist/api.js +17 -0
- package/dist/binding.d.ts +28 -0
- package/dist/binding.js +27 -0
- package/dist/engine.d.ts +37 -0
- package/dist/engine.js +222 -0
- package/dist/error.d.ts +39 -0
- package/dist/error.js +45 -0
- package/dist/evaluation.d.ts +5 -0
- package/dist/evaluation.js +28 -0
- package/dist/format.d.ts +14 -0
- package/dist/format.js +65 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +22 -0
- package/dist/options.d.ts +22 -0
- package/dist/options.js +58 -0
- package/dist/ruleUtils.d.ts +19 -0
- package/dist/ruleUtils.js +78 -0
- package/dist/types.d.ts +111 -0
- package/dist/types.js +1 -0
- package/dist/units.d.ts +10 -0
- package/dist/units.js +165 -0
- package/dist/web.d.ts +2 -0
- package/dist/web.js +6 -0
- package/native/fast-publicodes.darwin-arm64.node +0 -0
- package/native/fast-publicodes.darwin-x64.node +0 -0
- package/native/fast-publicodes.linux-arm64-gnu.node +0 -0
- package/native/fast-publicodes.linux-arm64-musl.node +0 -0
- package/native/fast-publicodes.linux-x64-gnu.node +0 -0
- package/native/fast-publicodes.linux-x64-musl.node +0 -0
- package/native/fast-publicodes.win32-x64-msvc.node +0 -0
- package/native/index.cjs +783 -0
- package/native/index.d.cts +64 -0
- package/package.json +65 -1
- package/wasm/fast_publicodes.d.ts +121 -0
- package/wasm/fast_publicodes.js +618 -0
- package/wasm/fast_publicodes_bg.wasm +0 -0
- package/wasm/fast_publicodes_bg.wasm.d.ts +23 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import * as wasm from "../wasm/fast_publicodes.js";
|
|
4
|
+
import { useBinding } from "./binding.js";
|
|
5
|
+
useBinding(loadAddon() ?? loadWasm());
|
|
6
|
+
/** The addon built for this platform, if one is installed. */
|
|
7
|
+
function loadAddon() {
|
|
8
|
+
try {
|
|
9
|
+
return createRequire(import.meta.url)("../native/index.cjs");
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function loadWasm() {
|
|
16
|
+
wasm.initSync({
|
|
17
|
+
module: readFileSync(new URL("../wasm/fast_publicodes_bg.wasm", import.meta.url)),
|
|
18
|
+
});
|
|
19
|
+
return wasm;
|
|
20
|
+
}
|
|
21
|
+
export * from "./api.js";
|
|
22
|
+
export { default } from "./api.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { EngineOptions } from "./types.js";
|
|
2
|
+
/** The options as `publicodes` resolves them, then reads them as conditions. */
|
|
3
|
+
export declare function resolveOptions(options: EngineOptions): {
|
|
4
|
+
strict: {
|
|
5
|
+
situation: boolean;
|
|
6
|
+
noOrphanRule: boolean;
|
|
7
|
+
noCycleRuntime: boolean;
|
|
8
|
+
checkPossibleValues: boolean;
|
|
9
|
+
};
|
|
10
|
+
warn: {
|
|
11
|
+
cyclicReferences: boolean;
|
|
12
|
+
experimentalRules: boolean;
|
|
13
|
+
unitConversion: boolean;
|
|
14
|
+
deprecatedSyntax: boolean;
|
|
15
|
+
situationIssues: boolean;
|
|
16
|
+
};
|
|
17
|
+
flag: {
|
|
18
|
+
filterNotApplicablePossibilities: boolean;
|
|
19
|
+
automaticNamespaceDisabling: boolean;
|
|
20
|
+
};
|
|
21
|
+
inversionMaxIterations: number | null;
|
|
22
|
+
};
|
package/dist/options.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** The options as `publicodes` resolves them, then reads them as conditions. */
|
|
2
|
+
export function resolveOptions(options) {
|
|
3
|
+
const { strict, warn, flag } = options;
|
|
4
|
+
const strictOptions = {
|
|
5
|
+
situation: true,
|
|
6
|
+
noOrphanRule: true,
|
|
7
|
+
noCycleRuntime: false,
|
|
8
|
+
checkPossibleValues: false,
|
|
9
|
+
...(typeof strict === "boolean"
|
|
10
|
+
? {
|
|
11
|
+
situation: strict,
|
|
12
|
+
noOrphanRule: options.allowOrphanRules === true ? false : strict,
|
|
13
|
+
checkPossibleValues: strict,
|
|
14
|
+
noCycleRuntime: strict,
|
|
15
|
+
}
|
|
16
|
+
: typeof strict === "object"
|
|
17
|
+
? strict
|
|
18
|
+
: {}),
|
|
19
|
+
};
|
|
20
|
+
const warnOptions = {
|
|
21
|
+
cyclicReferences: true,
|
|
22
|
+
experimentalRules: true,
|
|
23
|
+
unitConversion: true,
|
|
24
|
+
deprecatedSyntax: true,
|
|
25
|
+
situationIssues: true,
|
|
26
|
+
...(typeof warn === "boolean"
|
|
27
|
+
? {
|
|
28
|
+
cyclicReferences: warn,
|
|
29
|
+
experimentalRules: warn,
|
|
30
|
+
unitConversion: warn,
|
|
31
|
+
}
|
|
32
|
+
: typeof warn === "object"
|
|
33
|
+
? warn
|
|
34
|
+
: {}),
|
|
35
|
+
};
|
|
36
|
+
const iterations = options
|
|
37
|
+
.inversionMaxIterations;
|
|
38
|
+
return {
|
|
39
|
+
strict: {
|
|
40
|
+
situation: Boolean(strictOptions.situation),
|
|
41
|
+
noOrphanRule: Boolean(strictOptions.noOrphanRule),
|
|
42
|
+
noCycleRuntime: Boolean(strictOptions.noCycleRuntime),
|
|
43
|
+
checkPossibleValues: Boolean(strictOptions.checkPossibleValues),
|
|
44
|
+
},
|
|
45
|
+
warn: {
|
|
46
|
+
cyclicReferences: Boolean(warnOptions.cyclicReferences),
|
|
47
|
+
experimentalRules: Boolean(warnOptions.experimentalRules),
|
|
48
|
+
unitConversion: Boolean(warnOptions.unitConversion),
|
|
49
|
+
deprecatedSyntax: Boolean(warnOptions.deprecatedSyntax),
|
|
50
|
+
situationIssues: Boolean(warnOptions.situationIssues),
|
|
51
|
+
},
|
|
52
|
+
flag: {
|
|
53
|
+
filterNotApplicablePossibilities: Boolean(flag?.filterNotApplicablePossibilities ?? false),
|
|
54
|
+
automaticNamespaceDisabling: Boolean(flag?.automaticNamespaceDisabling ?? true),
|
|
55
|
+
},
|
|
56
|
+
inversionMaxIterations: iterations == null ? null : Number(iterations),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RuleNode } from "./types.js";
|
|
2
|
+
type Rules = Record<string, Pick<RuleNode, "private" | "rawNode" | "title"> | undefined>;
|
|
3
|
+
export declare const nameLeaf: (dottedName: string) => string | undefined;
|
|
4
|
+
export declare const encodeRuleName: (dottedName: string) => string;
|
|
5
|
+
export declare const decodeRuleName: (dottedName: string) => string;
|
|
6
|
+
export declare const ruleParent: (dottedName: string) => string;
|
|
7
|
+
export declare const contextNameToDottedName: (contextName: string) => string;
|
|
8
|
+
/** The ancestors of a rule, nearest first. */
|
|
9
|
+
export declare function ruleParents(dottedName: string): string[];
|
|
10
|
+
export declare const getChildrenRules: (rules: Rules, dottedName: string) => string[];
|
|
11
|
+
export declare function findCommonAncestor(dottedName1: string, dottedName2: string): string;
|
|
12
|
+
export declare function isAccessible(rules: Rules, contextName: string, name: string): boolean;
|
|
13
|
+
export declare function isExperimental(rules: Rules, name: string): boolean;
|
|
14
|
+
export declare function disambiguateReference(rules: Rules, referencedFrom: string | undefined, partialName: string): string;
|
|
15
|
+
export declare function ruleWithDedicatedDocumentationPage(rule: {
|
|
16
|
+
virtualRule?: boolean;
|
|
17
|
+
type?: string;
|
|
18
|
+
}): boolean;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { PublicodesError } from "./error.js";
|
|
2
|
+
const splitName = (name) => name.split(" . ");
|
|
3
|
+
const joinName = (parts) => parts.join(" . ");
|
|
4
|
+
export const nameLeaf = (dottedName) => splitName(dottedName).at(-1);
|
|
5
|
+
export const encodeRuleName = (dottedName) => dottedName
|
|
6
|
+
?.replace(/\s\.\s/g, "/")
|
|
7
|
+
.replace(/-/g, "‑")
|
|
8
|
+
.replace(/\s/g, "-");
|
|
9
|
+
export const decodeRuleName = (dottedName) => dottedName.replace(/\//g, " . ").replace(/-/g, " ").replace(/‑/g, "-");
|
|
10
|
+
export const ruleParent = (dottedName) => joinName(splitName(dottedName).slice(0, -1));
|
|
11
|
+
export const contextNameToDottedName = (contextName) => contextName.endsWith("$SITUATION") ? ruleParent(contextName) : contextName;
|
|
12
|
+
/** The ancestors of a rule, nearest first. */
|
|
13
|
+
export function ruleParents(dottedName) {
|
|
14
|
+
const parts = splitName(dottedName).slice(0, -1);
|
|
15
|
+
return parts.map((_, i) => joinName(parts.slice(0, i + 1))).reverse();
|
|
16
|
+
}
|
|
17
|
+
export const getChildrenRules = (rules, dottedName) => Object.keys(rules).filter((name) => name.startsWith(dottedName) &&
|
|
18
|
+
splitName(name).length === splitName(dottedName).length + 1);
|
|
19
|
+
export function findCommonAncestor(dottedName1, dottedName2) {
|
|
20
|
+
const parts1 = splitName(dottedName1);
|
|
21
|
+
const parts2 = splitName(dottedName2);
|
|
22
|
+
const index = parts1.findIndex((part, i) => parts2[i] !== part);
|
|
23
|
+
return index === -1 ? dottedName1 : joinName(parts1.slice(0, index));
|
|
24
|
+
}
|
|
25
|
+
function exists(rules, name) {
|
|
26
|
+
if (!(name in rules)) {
|
|
27
|
+
throw new PublicodesError("InternalError", `La règle "${name}" n'existe pas`, {
|
|
28
|
+
dottedName: name,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function isAccessible(rules, contextName, name) {
|
|
33
|
+
exists(rules, name);
|
|
34
|
+
const commonAncestor = findCommonAncestor(contextName, name);
|
|
35
|
+
const parents = [name, ...ruleParents(name), ""];
|
|
36
|
+
const checked = parents.slice(0, Math.max(parents.indexOf(commonAncestor) - 1, 0));
|
|
37
|
+
return checked.every((parent) => !(parent in rules) || rules[parent]?.private === false);
|
|
38
|
+
}
|
|
39
|
+
export function isExperimental(rules, name) {
|
|
40
|
+
exists(rules, name);
|
|
41
|
+
return [name, ...ruleParents(name)].some((parent) => parent in rules && rules[parent]?.rawNode?.experimental === "oui");
|
|
42
|
+
}
|
|
43
|
+
const withContext = (context, name) => context ? `${context} . ${name}` : name;
|
|
44
|
+
export function disambiguateReference(rules, referencedFrom = "", partialName) {
|
|
45
|
+
const contexts = ruleParents(referencedFrom);
|
|
46
|
+
contexts.push(referencedFrom);
|
|
47
|
+
let name = partialName;
|
|
48
|
+
if (name.startsWith("^ . ")) {
|
|
49
|
+
const prefix = name.match(/^(\^ \. )+/)?.[0] ?? "";
|
|
50
|
+
name = name.slice(prefix.length);
|
|
51
|
+
contexts.splice(-(prefix.length / 4));
|
|
52
|
+
}
|
|
53
|
+
const root = contexts.pop();
|
|
54
|
+
contexts.unshift(root);
|
|
55
|
+
contexts.push("");
|
|
56
|
+
const context = contexts.find((context) => {
|
|
57
|
+
const dottedName = withContext(context, name);
|
|
58
|
+
return (dottedName in rules &&
|
|
59
|
+
dottedName !== referencedFrom &&
|
|
60
|
+
isAccessible(rules, referencedFrom, dottedName));
|
|
61
|
+
});
|
|
62
|
+
if (context !== undefined)
|
|
63
|
+
return withContext(context, name);
|
|
64
|
+
if (referencedFrom.endsWith(name))
|
|
65
|
+
return referencedFrom;
|
|
66
|
+
const candidates = contexts.map((context) => withContext(context, name));
|
|
67
|
+
const existing = candidates.find((candidate) => candidate in rules);
|
|
68
|
+
throw new PublicodesError("SyntaxError", existing === undefined
|
|
69
|
+
? `La référence "${name}" est introuvable.\nVérifiez que l'orthographe et l'espace de nom sont corrects`
|
|
70
|
+
: `La règle "${existing}" n'est pas accessible depuis "${referencedFrom}".\n\tCela vient du fait qu'elle est privée ou qu'un de ses parent est privé`, { dottedName: contextNameToDottedName(referencedFrom) });
|
|
71
|
+
}
|
|
72
|
+
export function ruleWithDedicatedDocumentationPage(rule) {
|
|
73
|
+
return (rule.virtualRule !== true &&
|
|
74
|
+
rule.type !== "groupe" &&
|
|
75
|
+
rule.type !== "texte" &&
|
|
76
|
+
rule.type !== "paragraphe" &&
|
|
77
|
+
rule.type !== "notification");
|
|
78
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { GetUnitKey, Unit } from "./units.js";
|
|
2
|
+
export type PublicodesExpression = string | number | Record<string, unknown>;
|
|
3
|
+
export type Rule = Record<string, unknown> & {
|
|
4
|
+
titre?: string;
|
|
5
|
+
question?: string;
|
|
6
|
+
experimental?: "oui";
|
|
7
|
+
privé?: "oui";
|
|
8
|
+
};
|
|
9
|
+
export type RawRule = Omit<Rule, "nom"> | PublicodesExpression | null;
|
|
10
|
+
export type RawPublicodes<RuleNames extends string = string> = Partial<Record<RuleNames, RawRule>>;
|
|
11
|
+
export type Situation<RuleNames extends string = string> = Partial<Record<RuleNames, PublicodesExpression>>;
|
|
12
|
+
export type Evaluation = number | string | boolean | null | undefined;
|
|
13
|
+
export interface EvaluatedNode {
|
|
14
|
+
nodeValue: Evaluation;
|
|
15
|
+
unit?: Unit;
|
|
16
|
+
missingVariables: Record<string, number>;
|
|
17
|
+
}
|
|
18
|
+
/** One of the values a rule with `une possibilité` takes. */
|
|
19
|
+
export interface Possibility {
|
|
20
|
+
type: "number" | "string" | "reference";
|
|
21
|
+
nodeValue: string | number;
|
|
22
|
+
publicodesValue: string;
|
|
23
|
+
notApplicable: {
|
|
24
|
+
nodeKind: string;
|
|
25
|
+
nodeValue: Evaluation;
|
|
26
|
+
};
|
|
27
|
+
unit?: Unit;
|
|
28
|
+
dottedName?: string;
|
|
29
|
+
title?: string;
|
|
30
|
+
}
|
|
31
|
+
/** A parsed rule, without its explanation tree. */
|
|
32
|
+
export interface RuleNode<Name extends string = string> {
|
|
33
|
+
dottedName: Name;
|
|
34
|
+
title: string;
|
|
35
|
+
nodeKind: "rule";
|
|
36
|
+
private: boolean;
|
|
37
|
+
virtualRule: boolean;
|
|
38
|
+
rawNode: Rule;
|
|
39
|
+
possibilities: {
|
|
40
|
+
nodeKind: "une possibilité";
|
|
41
|
+
type: "number" | "string" | "reference";
|
|
42
|
+
unit?: Unit;
|
|
43
|
+
explanation: Possibility[];
|
|
44
|
+
} | null;
|
|
45
|
+
}
|
|
46
|
+
export type ParsedRules<RuleNames extends string = string> = Record<RuleNames, RuleNode<RuleNames>>;
|
|
47
|
+
export interface Logger {
|
|
48
|
+
log(message: string): void;
|
|
49
|
+
warn(message: string): void;
|
|
50
|
+
error(message: string): void;
|
|
51
|
+
}
|
|
52
|
+
export interface StrictOptions {
|
|
53
|
+
situation?: boolean;
|
|
54
|
+
noOrphanRule?: boolean;
|
|
55
|
+
noCycleRuntime?: boolean;
|
|
56
|
+
checkPossibleValues?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export interface FlagOptions {
|
|
59
|
+
filterNotApplicablePossibilities?: boolean;
|
|
60
|
+
automaticNamespaceDisabling?: boolean;
|
|
61
|
+
}
|
|
62
|
+
export interface WarnOptions {
|
|
63
|
+
experimentalRules?: boolean;
|
|
64
|
+
unitConversion?: boolean;
|
|
65
|
+
cyclicReferences?: boolean;
|
|
66
|
+
deprecatedSyntax?: boolean;
|
|
67
|
+
situationIssues?: boolean;
|
|
68
|
+
}
|
|
69
|
+
export interface EngineOptions {
|
|
70
|
+
allowOrphanRules?: boolean;
|
|
71
|
+
strict?: boolean | StrictOptions;
|
|
72
|
+
logger?: Logger;
|
|
73
|
+
flag?: FlagOptions;
|
|
74
|
+
warn?: boolean | WarnOptions;
|
|
75
|
+
getUnitKey?: GetUnitKey;
|
|
76
|
+
}
|
|
77
|
+
type BinaryOperator = "+" | "-" | "*" | "/" | "**" | ">" | "<" | ">=" | "<=" | "=" | "!=";
|
|
78
|
+
export type BinaryOp = {
|
|
79
|
+
[Operator in BinaryOperator]: {
|
|
80
|
+
[Key in Operator]: [ExprAST, ExprAST];
|
|
81
|
+
};
|
|
82
|
+
}[BinaryOperator];
|
|
83
|
+
export type UnaryOp = {
|
|
84
|
+
"-": [{
|
|
85
|
+
constant: {
|
|
86
|
+
type: "number";
|
|
87
|
+
nodeValue: 0;
|
|
88
|
+
};
|
|
89
|
+
}, ExprAST];
|
|
90
|
+
};
|
|
91
|
+
/** The tree of a publicodes expression. */
|
|
92
|
+
export type ExprAST = BinaryOp | UnaryOp | {
|
|
93
|
+
variable: string;
|
|
94
|
+
} | {
|
|
95
|
+
constant: {
|
|
96
|
+
type: "number";
|
|
97
|
+
nodeValue: number;
|
|
98
|
+
rawUnit?: string;
|
|
99
|
+
};
|
|
100
|
+
} | {
|
|
101
|
+
constant: {
|
|
102
|
+
type: "boolean";
|
|
103
|
+
nodeValue: boolean;
|
|
104
|
+
};
|
|
105
|
+
} | {
|
|
106
|
+
constant: {
|
|
107
|
+
type: "string" | "date";
|
|
108
|
+
nodeValue: string;
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
export {};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/units.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface Unit {
|
|
2
|
+
numerators: string[];
|
|
3
|
+
denominators: string[];
|
|
4
|
+
}
|
|
5
|
+
export type FormatUnit = (unit: string, count: number) => string;
|
|
6
|
+
export type GetUnitKey = (writtenUnit: string) => string;
|
|
7
|
+
export declare function parseUnit(text: string, getUnitKey?: GetUnitKey): Unit;
|
|
8
|
+
export declare function serializeUnit(rawUnit: Unit | undefined | string, count?: number, formatUnit?: FormatUnit): string | undefined;
|
|
9
|
+
export declare function simplifyUnit(unit: Unit): Unit;
|
|
10
|
+
export declare function convertUnit<Value extends number | undefined | null>(from: Unit | undefined, to: Unit | undefined, value: Value): Value;
|
package/dist/units.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { PublicodesError } from "./error.js";
|
|
2
|
+
/** Each base unit with its power, the power written as trailing digits. */
|
|
3
|
+
function unitCounts(units) {
|
|
4
|
+
const counts = new Map();
|
|
5
|
+
for (const unit of units) {
|
|
6
|
+
const power = unit.match(/(\d+)(?!.*[A-Za-z])/g)?.[0];
|
|
7
|
+
const base = power === undefined ? unit : (unit.split(power)[0] ?? "");
|
|
8
|
+
counts.set(base, (counts.get(base) ?? 0) + (power === undefined ? 1 : +power));
|
|
9
|
+
}
|
|
10
|
+
return counts;
|
|
11
|
+
}
|
|
12
|
+
export function parseUnit(text, getUnitKey = (x) => x) {
|
|
13
|
+
if (text.includes("/ ")) {
|
|
14
|
+
throw new Error(`L'unité "${text}" ne doit pas contenir d'espace après "/"`);
|
|
15
|
+
}
|
|
16
|
+
const [numerators = "", ...denominators] = text.split("/");
|
|
17
|
+
const split = (part) => [
|
|
18
|
+
...unitCounts(part.trim().split(".").filter(Boolean).map(getUnitKey)).entries(),
|
|
19
|
+
].flatMap(([unit, power]) => Array(power).fill(unit));
|
|
20
|
+
return {
|
|
21
|
+
numerators: split(numerators),
|
|
22
|
+
denominators: denominators.flatMap(split),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Cancels the units found both as numerator and denominator, except the empty unit, which
|
|
27
|
+
* `publicodes` finds but reads as false.
|
|
28
|
+
*/
|
|
29
|
+
function simplify(unit, same) {
|
|
30
|
+
let { numerators, denominators } = unit;
|
|
31
|
+
const removeOnce = (list, item) => {
|
|
32
|
+
const index = list.findIndex((e) => same(e, item));
|
|
33
|
+
return list.filter((_, i) => i !== index);
|
|
34
|
+
};
|
|
35
|
+
for (const next of [...unit.numerators, ...unit.denominators]) {
|
|
36
|
+
if (numerators.find((u) => same(next, u)) &&
|
|
37
|
+
denominators.find((u) => same(next, u))) {
|
|
38
|
+
numerators = removeOnce(numerators, next);
|
|
39
|
+
denominators = removeOnce(denominators, next);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { numerators, denominators };
|
|
43
|
+
}
|
|
44
|
+
const plural = 2;
|
|
45
|
+
export function serializeUnit(rawUnit, count = plural, formatUnit = (x) => x) {
|
|
46
|
+
if (rawUnit === null || typeof rawUnit !== "object") {
|
|
47
|
+
return typeof rawUnit === "string" ? formatUnit(rawUnit, count) : rawUnit;
|
|
48
|
+
}
|
|
49
|
+
const { numerators = [], denominators = [] } = simplify(rawUnit, (a, b) => a === b);
|
|
50
|
+
const print = (units, n) => [...unitCounts(units.map((unit) => formatUnit(unit, n))).entries()]
|
|
51
|
+
.map(([unit, power]) => (power > 1 ? `${unit}${power}` : unit))
|
|
52
|
+
.join(".");
|
|
53
|
+
if (!numerators.length && !denominators.length)
|
|
54
|
+
return "";
|
|
55
|
+
if (!denominators.length)
|
|
56
|
+
return print(numerators, count);
|
|
57
|
+
if (!numerators.length)
|
|
58
|
+
return `/${print(denominators, 1)}`;
|
|
59
|
+
return `${print(numerators, plural)}/${print(denominators, 1)}`;
|
|
60
|
+
}
|
|
61
|
+
const conversions = {
|
|
62
|
+
"mois/an": 12,
|
|
63
|
+
"jour/an": 365,
|
|
64
|
+
"jour/mois": 365 / 12,
|
|
65
|
+
"trimestre/an": 4,
|
|
66
|
+
"mois/trimestre": 3,
|
|
67
|
+
"jour/trimestre": (365 / 12) * 3,
|
|
68
|
+
"€/k€": 10 ** 3,
|
|
69
|
+
"g/kg": 10 ** 3,
|
|
70
|
+
"mg/g": 10 ** 3,
|
|
71
|
+
"mg/kg": 10 ** 6,
|
|
72
|
+
"m/km": 10 ** 3,
|
|
73
|
+
"cm/m": 10 ** 2,
|
|
74
|
+
"mm/cm": 10 ** 1,
|
|
75
|
+
"mm/m": 10 ** 3,
|
|
76
|
+
"cm/km": 10 ** 5,
|
|
77
|
+
"mm/km": 10 ** 6,
|
|
78
|
+
};
|
|
79
|
+
/** Units that convert into one another. */
|
|
80
|
+
const classes = [];
|
|
81
|
+
for (const ratio of Object.keys(conversions)) {
|
|
82
|
+
const [a, b] = ratio.split("/");
|
|
83
|
+
const withA = classes.find((c) => c.has(a));
|
|
84
|
+
const withB = classes.find((c) => c.has(b));
|
|
85
|
+
if (!withA && !withB)
|
|
86
|
+
classes.push(new Set([a, b]));
|
|
87
|
+
else
|
|
88
|
+
(withA ?? withB)?.add(withA ? b : a);
|
|
89
|
+
}
|
|
90
|
+
const sameClass = (a, b) => a === b || classes.some((c) => c.has(a) && c.has(b));
|
|
91
|
+
const removePercentages = (unit) => ({
|
|
92
|
+
numerators: unit.numerators.filter((u) => u !== "%"),
|
|
93
|
+
denominators: unit.denominators.filter((u) => u !== "%"),
|
|
94
|
+
});
|
|
95
|
+
export function simplifyUnit(unit) {
|
|
96
|
+
const { numerators, denominators } = simplify(unit, sameClass);
|
|
97
|
+
if (numerators.length && numerators.every((u) => u === "%")) {
|
|
98
|
+
return { numerators: ["%"], denominators };
|
|
99
|
+
}
|
|
100
|
+
return removePercentages({ numerators, denominators });
|
|
101
|
+
}
|
|
102
|
+
const round = (value) => +value.toFixed(16);
|
|
103
|
+
function factor(from, to) {
|
|
104
|
+
const direct = conversions[`${to}/${from}`];
|
|
105
|
+
if (direct)
|
|
106
|
+
return direct;
|
|
107
|
+
const inverse = conversions[`${from}/${to}`];
|
|
108
|
+
return inverse && 1 / inverse;
|
|
109
|
+
}
|
|
110
|
+
function conversionFactor(from, to) {
|
|
111
|
+
const percents = (units) => units.filter((u) => u === "%").length;
|
|
112
|
+
let result = 100 ** (percents(to) - percents(from));
|
|
113
|
+
for (const unit of from) {
|
|
114
|
+
const target = to.find((t) => factor(unit, t) !== undefined);
|
|
115
|
+
result *= (target !== undefined && factor(unit, target)) || 1;
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
function convertible(a, b) {
|
|
120
|
+
if (a == null || b == null)
|
|
121
|
+
return true;
|
|
122
|
+
const countByClass = (units) => {
|
|
123
|
+
const counts = {};
|
|
124
|
+
for (const unit of units) {
|
|
125
|
+
const index = classes.findIndex((c) => c.has(unit));
|
|
126
|
+
const key = index === -1 ? unit : `${index}`;
|
|
127
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
128
|
+
}
|
|
129
|
+
return counts;
|
|
130
|
+
};
|
|
131
|
+
const [numA, denA, numB, denB] = [
|
|
132
|
+
a.numerators,
|
|
133
|
+
a.denominators,
|
|
134
|
+
b.numerators,
|
|
135
|
+
b.denominators,
|
|
136
|
+
].map(countByClass);
|
|
137
|
+
const keys = new Set([numA, denA, numB, denB].flatMap((counts) => Object.keys(counts)));
|
|
138
|
+
return [...keys].every((key) => key === "%" ||
|
|
139
|
+
(numA[key] ?? 0) - (denA[key] ?? 0) ===
|
|
140
|
+
(numB[key] ?? 0) - (denB[key] ?? 0));
|
|
141
|
+
}
|
|
142
|
+
const equivalents = { "kW.h": "kWh", "mn/h": "noeud" };
|
|
143
|
+
export function convertUnit(from, to, value) {
|
|
144
|
+
const serializedFrom = serializeUnit(from);
|
|
145
|
+
const serializedTo = serializeUnit(to);
|
|
146
|
+
const equivalent = !!serializedFrom &&
|
|
147
|
+
!!serializedTo &&
|
|
148
|
+
(serializedFrom === serializedTo ||
|
|
149
|
+
serializedFrom === equivalents[serializedTo] ||
|
|
150
|
+
serializedTo === equivalents[serializedFrom]);
|
|
151
|
+
if (!equivalent && !convertible(from, to)) {
|
|
152
|
+
throw new PublicodesError("EngineError", `Impossible de convertir l'unité '${serializedFrom}' en '${serializedTo}'`, {});
|
|
153
|
+
}
|
|
154
|
+
if (!value || from === undefined)
|
|
155
|
+
return value;
|
|
156
|
+
const simplified = (unit) => [
|
|
157
|
+
simplify(removePercentages(unit), sameClass),
|
|
158
|
+
round(conversionFactor(unit.numerators, unit.denominators)),
|
|
159
|
+
];
|
|
160
|
+
const [fromSimplified, factorTo] = simplified(from);
|
|
161
|
+
const [toSimplified, factorFrom] = simplified(to ?? { numerators: [], denominators: [] });
|
|
162
|
+
return round(((value * factorTo) / factorFrom) *
|
|
163
|
+
conversionFactor(fromSimplified.numerators, toSimplified.numerators) *
|
|
164
|
+
conversionFactor(toSimplified.denominators, fromSimplified.denominators));
|
|
165
|
+
}
|
package/dist/web.d.ts
ADDED
package/dist/web.js
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|