@roomwise/calc-engine 0.5.0 → 0.6.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/README.md +5 -3
- package/dist/index.cjs +52 -10
- package/dist/index.d.cts +16 -3
- package/dist/index.d.ts +16 -3
- package/dist/index.js +50 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ Lines are produced in this order: base cost, object rules (by `sortOrder`), mate
|
|
|
47
47
|
- Base cost: `OBJECT` rules with `baseComponent: true`, one `BASE` line each. Only in `FULL`; a line that is `OK` with a zero amount is left out.
|
|
48
48
|
- Object rules: the other `OBJECT` level rules, one line each.
|
|
49
49
|
- Materials: for every room selection with a product, the `MATERIAL_TYPE` rule for the category and the product's material type is used if one exists, otherwise the `CATEGORY` rule. Without either, the line is `NOT_CALCULATED` (`RULE_NOT_FOUND`). Unknown or unavailable products give `UNAVAILABLE`. Each line carries [components](#line-components).
|
|
50
|
-
- Engineering and additional: package items marked `includedInBase` become `INCLUDED` lines with a zero amount, and are skipped in `PARTIAL`. Options are priced by unit (`PIECE`, `ROOM_SQM`, `ROOM`, `PROJECT`). An option with `priceCoefficientKeys`
|
|
50
|
+
- Engineering and additional: package items marked `includedInBase` become `INCLUDED` lines with a zero amount, and are skipped in `PARTIAL`. Options are priced by unit (`PIECE`, `ROOM_SQM`, `ROOM`, `PROJECT`). An option has exactly one price source: `priceCents` with no `priceCoefficientKeys`, or `priceCents: null` with keys, in which case the unit price is the sum of those coefficients in dollars (`PricingDataSchema` rejects both and neither; read the price from the line's `unitPriceCents`). A missing coefficient gives an `ERROR` line (`UNKNOWN_COEFFICIENT`). An option whose `scopes` do not include the configuration scope is `UNAVAILABLE` with `OPTION_SCOPE_NOT_ALLOWED`.
|
|
51
51
|
|
|
52
52
|
Result fields:
|
|
53
53
|
|
|
@@ -197,7 +197,7 @@ Every schema is a zod schema with an inferred type of the same name without the
|
|
|
197
197
|
| `totalArea` variable | `totalAreaSqm` | `roomsFloorArea` |
|
|
198
198
|
| Area warnings | `ROOMS_AREA_EXCEEDS_TOTAL`, `ROOMS_AREA_BELOW_TOTAL` | none |
|
|
199
199
|
|
|
200
|
-
The base cost has no per-area rate table. It is built from rules whose formulas use the volumes of the rooms (`roomsFloorArea`, `roomsWallArea`, `roomsPerimeter`, `wetRoomsCount`) together with `finishLevel`, `scope` and coefficients.
|
|
200
|
+
The base cost has no per-area rate table. It is built from rules whose formulas use the volumes of the rooms (`roomsFloorArea`, `roomsWallArea`, `roomsPuttyWallArea`, `roomsPerimeter`, `wetRoomsCount`) together with `finishLevel`, `scope` and coefficients.
|
|
201
201
|
|
|
202
202
|
### Line components
|
|
203
203
|
|
|
@@ -241,18 +241,20 @@ roomType == 'KITCHEN' ? ceil(floorArea / 2) : 0
|
|
|
241
241
|
| object | `totalArea`, `balconyArea` | number; `totalArea` is `roomsFloorArea` in `PARTIAL` |
|
|
242
242
|
| object | `scope` | `FULL`, `PARTIAL` |
|
|
243
243
|
| object | `roomsFloorArea`, `roomsWallArea`, `roomsPerimeter` | number: sums over all rooms of `floorArea`, `wallArea` and `perimeter` |
|
|
244
|
+
| object | `roomsPuttyWallArea` | number: sum of `wallArea` over rooms whose wall selections need putty (rooms without wall selections count) |
|
|
244
245
|
| object | `wetRoomsCount` | number (bathrooms, kitchens and kitchen-living rooms) |
|
|
245
246
|
| object | `balconyTiles` | boolean |
|
|
246
247
|
| object | `finishLevel` | `ROUGH`, `PRE_FINISH`, `LIVED_IN` |
|
|
247
248
|
| object | `roomsCount`, `bathroomsCount` | number (living rooms, bedrooms and kitchen-living rooms count as rooms) |
|
|
248
249
|
| `SURFACE` rules only | `surfaceArea` | number: the area of the surface being prepared |
|
|
250
|
+
| `SURFACE` rules only | `puttyRequired` | boolean: `true` when no category is selected on the surface or at least one selected category has `requiresPutty` (default `true`), taken from the selections, not from line status; booleans do not coerce, write `puttyRequired ? a : 0` |
|
|
249
251
|
| cost and labor formulas of `CATEGORY` and `MATERIAL_TYPE` rules | `quantity` | the result of the quantity formula |
|
|
250
252
|
|
|
251
253
|
Availability by rule level:
|
|
252
254
|
|
|
253
255
|
- `OBJECT`: object variables only. `quantity` is not available.
|
|
254
256
|
- `CATEGORY` and `MATERIAL_TYPE`: room, product and object variables. `quantity` is available in cost and labor formulas, not in the quantity formula. `laborFormula` sees the same variables as `costFormula`.
|
|
255
|
-
- `SURFACE`: room and object variables plus `surfaceArea`. `quantity` and the product variables are not available.
|
|
257
|
+
- `SURFACE`: room and object variables plus `surfaceArea` and `puttyRequired`. `quantity` and the product variables are not available.
|
|
256
258
|
|
|
257
259
|
Coefficients are read as `coef.<key>`; keys come from the pricing data. The cost and labor formulas receive `quantity` unrounded, while the line shows it rounded to 3 decimals.
|
|
258
260
|
|
package/dist/index.cjs
CHANGED
|
@@ -51,6 +51,12 @@ function toCents(dollars) {
|
|
|
51
51
|
return cents;
|
|
52
52
|
}
|
|
53
53
|
//#endregion
|
|
54
|
+
//#region src/variables/putty.ts
|
|
55
|
+
function puttyRequiredOn(room, surface, categories) {
|
|
56
|
+
const selected = categories.filter((category) => category.surface === surface && room.selections.some((selection) => selection.categoryId === category.id));
|
|
57
|
+
return selected.length === 0 || selected.some((category) => category.requiresPutty);
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
54
60
|
//#region src/variables/room.ts
|
|
55
61
|
function roomVariables(room) {
|
|
56
62
|
const perimeter = 2 * (room.lengthM + room.widthM);
|
|
@@ -80,7 +86,7 @@ const WET_ROOMS = [
|
|
|
80
86
|
function totalAreaOf(scope, totalAreaSqm, roomsFloorArea) {
|
|
81
87
|
return scope === "PARTIAL" || totalAreaSqm === null ? roomsFloorArea : totalAreaSqm;
|
|
82
88
|
}
|
|
83
|
-
function objectVariables(configuration) {
|
|
89
|
+
function objectVariables(configuration, categories) {
|
|
84
90
|
const rooms = configuration.rooms.map(roomVariables);
|
|
85
91
|
const sum = (pick) => rooms.reduce((total, room) => total + pick(room), 0);
|
|
86
92
|
const roomsFloorArea = sum((room) => room.floorArea);
|
|
@@ -94,6 +100,7 @@ function objectVariables(configuration) {
|
|
|
94
100
|
bathroomsCount: configuration.rooms.filter((room) => room.roomTypeCode === "BATHROOM").length,
|
|
95
101
|
roomsFloorArea,
|
|
96
102
|
roomsWallArea: sum((room) => room.wallArea),
|
|
103
|
+
roomsPuttyWallArea: configuration.rooms.reduce((total, room) => puttyRequiredOn(room, "WALLS", categories) ? total + roomVariables(room).wallArea : total, 0),
|
|
97
104
|
roomsPerimeter: sum((room) => room.perimeter),
|
|
98
105
|
wetRoomsCount: rooms.filter((room) => WET_ROOMS.includes(room.roomType)).length
|
|
99
106
|
};
|
|
@@ -733,8 +740,11 @@ function applySurfacePrep(data, room, lines, objectScope) {
|
|
|
733
740
|
};
|
|
734
741
|
const surfaces = /* @__PURE__ */ new Map();
|
|
735
742
|
lines.forEach((line, index) => {
|
|
736
|
-
const
|
|
737
|
-
if (line.status === "OK" &&
|
|
743
|
+
const category = data.categories.find((item) => item.id === line.categoryId);
|
|
744
|
+
if (line.status === "OK" && category !== void 0 && category.surface !== "NONE") {
|
|
745
|
+
const surface = category.surface;
|
|
746
|
+
surfaces.set(surface, [...surfaces.get(surface) ?? [], index]);
|
|
747
|
+
}
|
|
738
748
|
});
|
|
739
749
|
for (const [surface, indexes] of surfaces) {
|
|
740
750
|
const rule = surfaceRuleOf(data, surface);
|
|
@@ -742,7 +752,8 @@ function applySurfacePrep(data, room, lines, objectScope) {
|
|
|
742
752
|
const outcome = runNumericFormula(rule.costFormula, {
|
|
743
753
|
variables: {
|
|
744
754
|
...variables,
|
|
745
|
-
surfaceArea: areas[surface]
|
|
755
|
+
surfaceArea: areas[surface],
|
|
756
|
+
puttyRequired: puttyRequiredOn(room, surface, data.categories)
|
|
746
757
|
},
|
|
747
758
|
coefficients: data.coefficients
|
|
748
759
|
});
|
|
@@ -1077,7 +1088,8 @@ const PricingRoomTypeSchema = zod.z.object({
|
|
|
1077
1088
|
const PricingCategorySchema = zod.z.object({
|
|
1078
1089
|
id: zod.z.string(),
|
|
1079
1090
|
wastePercent: zod.z.number().min(0),
|
|
1080
|
-
surface: SurfaceKindSchema
|
|
1091
|
+
surface: SurfaceKindSchema,
|
|
1092
|
+
requiresPutty: zod.z.boolean().default(true)
|
|
1081
1093
|
});
|
|
1082
1094
|
const PricingMaterialTypeSchema = zod.z.object({
|
|
1083
1095
|
id: zod.z.string(),
|
|
@@ -1099,11 +1111,11 @@ const PricingPackageItemSchema = zod.z.object({
|
|
|
1099
1111
|
priceCents: CentsSchema.nullable(),
|
|
1100
1112
|
unit: OptionUnitSchema.nullable()
|
|
1101
1113
|
});
|
|
1102
|
-
const
|
|
1114
|
+
const PricingOptionObjectSchema = zod.z.object({
|
|
1103
1115
|
id: zod.z.string(),
|
|
1104
1116
|
code: OptionCodeSchema.nullable(),
|
|
1105
1117
|
kind: OptionKindSchema,
|
|
1106
|
-
priceCents: CentsSchema,
|
|
1118
|
+
priceCents: CentsSchema.nullable(),
|
|
1107
1119
|
unit: OptionUnitSchema,
|
|
1108
1120
|
roomTypeCodes: zod.z.array(zod.z.string()),
|
|
1109
1121
|
priceCoefficientKeys: zod.z.array(zod.z.string()),
|
|
@@ -1112,6 +1124,26 @@ const PricingOptionSchema = zod.z.object({
|
|
|
1112
1124
|
maxQuantity: zod.z.int().nullable(),
|
|
1113
1125
|
available: zod.z.boolean()
|
|
1114
1126
|
});
|
|
1127
|
+
function findOptionPriceSourceViolation(priceCents, priceCoefficientKeys) {
|
|
1128
|
+
const hasKeys = priceCoefficientKeys.length > 0;
|
|
1129
|
+
if (priceCents !== null && hasKeys) return {
|
|
1130
|
+
path: "priceCents",
|
|
1131
|
+
message: "priceCents must be null when priceCoefficientKeys is set"
|
|
1132
|
+
};
|
|
1133
|
+
if (priceCents === null && !hasKeys) return {
|
|
1134
|
+
path: "priceCoefficientKeys",
|
|
1135
|
+
message: "priceCoefficientKeys is required when priceCents is null"
|
|
1136
|
+
};
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
const PricingOptionSchema = PricingOptionObjectSchema.superRefine((option, context) => {
|
|
1140
|
+
const violation = findOptionPriceSourceViolation(option.priceCents, option.priceCoefficientKeys);
|
|
1141
|
+
if (violation !== null) context.addIssue({
|
|
1142
|
+
code: "custom",
|
|
1143
|
+
path: [violation.path],
|
|
1144
|
+
message: violation.message
|
|
1145
|
+
});
|
|
1146
|
+
});
|
|
1115
1147
|
const PricingDataSchema = zod.z.object({
|
|
1116
1148
|
version: zod.z.string().regex(/^[0-9a-f]{16}$/),
|
|
1117
1149
|
exchangeRate: ExchangeRateSchema.nullable(),
|
|
@@ -1191,6 +1223,7 @@ const RuleSampleSchema = zod.z.object({
|
|
|
1191
1223
|
wastePercent: zod.z.number().min(0)
|
|
1192
1224
|
}).optional(),
|
|
1193
1225
|
surfaceArea: zod.z.number().min(0).optional(),
|
|
1226
|
+
puttyRequired: zod.z.boolean().optional(),
|
|
1194
1227
|
object: zod.z.object({
|
|
1195
1228
|
scope: ScopeSchema,
|
|
1196
1229
|
totalAreaSqm: zod.z.number().positive().nullable(),
|
|
@@ -1201,6 +1234,7 @@ const RuleSampleSchema = zod.z.object({
|
|
|
1201
1234
|
bathroomsCount: zod.z.int().min(0),
|
|
1202
1235
|
roomsFloorArea: zod.z.number().min(0),
|
|
1203
1236
|
roomsWallArea: zod.z.number().min(0),
|
|
1237
|
+
roomsPuttyWallArea: zod.z.number().min(0).optional(),
|
|
1204
1238
|
roomsPerimeter: zod.z.number().min(0),
|
|
1205
1239
|
wetRoomsCount: zod.z.int().min(0)
|
|
1206
1240
|
})
|
|
@@ -1285,7 +1319,7 @@ function withKopecks(line, rate) {
|
|
|
1285
1319
|
}
|
|
1286
1320
|
function estimate(configuration, data) {
|
|
1287
1321
|
const rate = data.exchangeRate?.rate ?? null;
|
|
1288
|
-
const variables = objectVariables(configuration);
|
|
1322
|
+
const variables = objectVariables(configuration, data.categories);
|
|
1289
1323
|
const lines = [
|
|
1290
1324
|
...objectRuleLines(configuration, data, "BASE", variables),
|
|
1291
1325
|
...objectRuleLines(configuration, data, "OBJECT", variables),
|
|
@@ -1381,6 +1415,7 @@ function objectScope({ object }) {
|
|
|
1381
1415
|
bathroomsCount: object.bathroomsCount,
|
|
1382
1416
|
roomsFloorArea: object.roomsFloorArea,
|
|
1383
1417
|
roomsWallArea: object.roomsWallArea,
|
|
1418
|
+
roomsPuttyWallArea: object.roomsPuttyWallArea ?? object.roomsWallArea,
|
|
1384
1419
|
roomsPerimeter: object.roomsPerimeter,
|
|
1385
1420
|
wetRoomsCount: object.wetRoomsCount
|
|
1386
1421
|
};
|
|
@@ -1396,7 +1431,8 @@ function scopeOf(level, sample) {
|
|
|
1396
1431
|
return { scope: {
|
|
1397
1432
|
...roomVariables(room),
|
|
1398
1433
|
...objectScope(sample),
|
|
1399
|
-
surfaceArea
|
|
1434
|
+
surfaceArea,
|
|
1435
|
+
puttyRequired: sample.puttyRequired ?? true
|
|
1400
1436
|
} };
|
|
1401
1437
|
}
|
|
1402
1438
|
if (room === void 0 || product === void 0) return { error: {
|
|
@@ -1538,12 +1574,14 @@ const FORMULA_SCOPE_VARIABLES = {
|
|
|
1538
1574
|
enumeration("scope", ScopeSchema.options),
|
|
1539
1575
|
number("roomsFloorArea"),
|
|
1540
1576
|
number("roomsWallArea"),
|
|
1577
|
+
number("roomsPuttyWallArea"),
|
|
1541
1578
|
number("roomsPerimeter"),
|
|
1542
1579
|
number("wetRoomsCount")
|
|
1543
1580
|
]
|
|
1544
1581
|
};
|
|
1545
1582
|
const FORMULA_QUANTITY_VARIABLE = number("quantity");
|
|
1546
1583
|
const FORMULA_SURFACE_AREA_VARIABLE = number("surfaceArea");
|
|
1584
|
+
const FORMULA_PUTTY_REQUIRED_VARIABLE = boolean("puttyRequired");
|
|
1547
1585
|
const FORMULA_VARIABLES = {
|
|
1548
1586
|
OBJECT: FORMULA_SCOPE_VARIABLES.object,
|
|
1549
1587
|
CATEGORY: [
|
|
@@ -1559,7 +1597,8 @@ const FORMULA_VARIABLES = {
|
|
|
1559
1597
|
SURFACE: [
|
|
1560
1598
|
...FORMULA_SCOPE_VARIABLES.room,
|
|
1561
1599
|
...FORMULA_SCOPE_VARIABLES.object,
|
|
1562
|
-
FORMULA_SURFACE_AREA_VARIABLE
|
|
1600
|
+
FORMULA_SURFACE_AREA_VARIABLE,
|
|
1601
|
+
FORMULA_PUTTY_REQUIRED_VARIABLE
|
|
1563
1602
|
]
|
|
1564
1603
|
};
|
|
1565
1604
|
const ALL_VARIABLE_NAMES = new Set([...FORMULA_VARIABLES.CATEGORY, ...FORMULA_VARIABLES.SURFACE].map((variable) => variable.name));
|
|
@@ -1787,6 +1826,7 @@ exports.ExchangeRateSchema = ExchangeRateSchema;
|
|
|
1787
1826
|
exports.FORMULA_FUNCTIONS = FORMULA_FUNCTIONS;
|
|
1788
1827
|
exports.FORMULA_FUNCTION_TABLE = FORMULA_FUNCTION_TABLE;
|
|
1789
1828
|
exports.FORMULA_LIMITS = FORMULA_LIMITS;
|
|
1829
|
+
exports.FORMULA_PUTTY_REQUIRED_VARIABLE = FORMULA_PUTTY_REQUIRED_VARIABLE;
|
|
1790
1830
|
exports.FORMULA_QUANTITY_VARIABLE = FORMULA_QUANTITY_VARIABLE;
|
|
1791
1831
|
exports.FORMULA_SCOPE_VARIABLES = FORMULA_SCOPE_VARIABLES;
|
|
1792
1832
|
exports.FORMULA_SURFACE_AREA_VARIABLE = FORMULA_SURFACE_AREA_VARIABLE;
|
|
@@ -1826,12 +1866,14 @@ exports.checkLimits = checkLimits;
|
|
|
1826
1866
|
exports.convertCentsToKopecks = convertCentsToKopecks;
|
|
1827
1867
|
exports.estimate = estimate;
|
|
1828
1868
|
exports.evaluate = evaluate;
|
|
1869
|
+
exports.findOptionPriceSourceViolation = findOptionPriceSourceViolation;
|
|
1829
1870
|
exports.levenshtein = levenshtein;
|
|
1830
1871
|
exports.objectVariables = objectVariables;
|
|
1831
1872
|
exports.parse = parse;
|
|
1832
1873
|
exports.previewRule = previewRule;
|
|
1833
1874
|
exports.productVariables = productVariables;
|
|
1834
1875
|
exports.productVariablesOf = productVariablesOf;
|
|
1876
|
+
exports.puttyRequiredOn = puttyRequiredOn;
|
|
1835
1877
|
exports.roomVariables = roomVariables;
|
|
1836
1878
|
exports.suggestNearest = suggestNearest;
|
|
1837
1879
|
exports.toCents = toCents;
|
package/dist/index.d.cts
CHANGED
|
@@ -382,6 +382,7 @@ export declare const RuleSampleSchema: z.ZodObject<{
|
|
|
382
382
|
wastePercent: z.ZodNumber;
|
|
383
383
|
}, z.core.$strip>>;
|
|
384
384
|
surfaceArea: z.ZodOptional<z.ZodNumber>;
|
|
385
|
+
puttyRequired: z.ZodOptional<z.ZodBoolean>;
|
|
385
386
|
object: z.ZodObject<{
|
|
386
387
|
scope: z.ZodEnum<{
|
|
387
388
|
FULL: "FULL";
|
|
@@ -399,6 +400,7 @@ export declare const RuleSampleSchema: z.ZodObject<{
|
|
|
399
400
|
bathroomsCount: z.ZodInt;
|
|
400
401
|
roomsFloorArea: z.ZodNumber;
|
|
401
402
|
roomsWallArea: z.ZodNumber;
|
|
403
|
+
roomsPuttyWallArea: z.ZodOptional<z.ZodNumber>;
|
|
402
404
|
roomsPerimeter: z.ZodNumber;
|
|
403
405
|
wetRoomsCount: z.ZodInt;
|
|
404
406
|
}, z.core.$strip>;
|
|
@@ -487,6 +489,7 @@ export declare const PricingCategorySchema: z.ZodObject<{
|
|
|
487
489
|
CEILING: "CEILING";
|
|
488
490
|
NONE: "NONE";
|
|
489
491
|
}>;
|
|
492
|
+
requiresPutty: z.ZodDefault<z.ZodBoolean>;
|
|
490
493
|
}, z.core.$strip>;
|
|
491
494
|
export type PricingCategory = z.infer<typeof PricingCategorySchema>;
|
|
492
495
|
export declare const PricingMaterialTypeSchema: z.ZodObject<{
|
|
@@ -521,6 +524,10 @@ export declare const PricingPackageItemSchema: z.ZodObject<{
|
|
|
521
524
|
}>>;
|
|
522
525
|
}, z.core.$strip>;
|
|
523
526
|
export type PricingPackageItem = z.infer<typeof PricingPackageItemSchema>;
|
|
527
|
+
export declare function findOptionPriceSourceViolation(priceCents: number | null, priceCoefficientKeys: readonly string[]): {
|
|
528
|
+
path: 'priceCents' | 'priceCoefficientKeys';
|
|
529
|
+
message: string;
|
|
530
|
+
} | null;
|
|
524
531
|
export declare const PricingOptionSchema: z.ZodObject<{
|
|
525
532
|
id: z.ZodString;
|
|
526
533
|
code: z.ZodNullable<z.ZodEnum<{
|
|
@@ -530,7 +537,7 @@ export declare const PricingOptionSchema: z.ZodObject<{
|
|
|
530
537
|
ENGINEERING: "ENGINEERING";
|
|
531
538
|
ADDITIONAL: "ADDITIONAL";
|
|
532
539
|
}>;
|
|
533
|
-
priceCents: z.ZodInt
|
|
540
|
+
priceCents: z.ZodNullable<z.ZodInt>;
|
|
534
541
|
unit: z.ZodEnum<{
|
|
535
542
|
PIECE: "PIECE";
|
|
536
543
|
ROOM_SQM: "ROOM_SQM";
|
|
@@ -616,6 +623,7 @@ export declare const PricingDataSchema: z.ZodObject<{
|
|
|
616
623
|
CEILING: "CEILING";
|
|
617
624
|
NONE: "NONE";
|
|
618
625
|
}>;
|
|
626
|
+
requiresPutty: z.ZodDefault<z.ZodBoolean>;
|
|
619
627
|
}, z.core.$strip>>;
|
|
620
628
|
materialTypes: z.ZodArray<z.ZodObject<{
|
|
621
629
|
id: z.ZodString;
|
|
@@ -655,7 +663,7 @@ export declare const PricingDataSchema: z.ZodObject<{
|
|
|
655
663
|
ENGINEERING: "ENGINEERING";
|
|
656
664
|
ADDITIONAL: "ADDITIONAL";
|
|
657
665
|
}>;
|
|
658
|
-
priceCents: z.ZodInt
|
|
666
|
+
priceCents: z.ZodNullable<z.ZodInt>;
|
|
659
667
|
unit: z.ZodEnum<{
|
|
660
668
|
PIECE: "PIECE";
|
|
661
669
|
ROOM_SQM: "ROOM_SQM";
|
|
@@ -812,6 +820,7 @@ export interface FormulaVariable {
|
|
|
812
820
|
export declare const FORMULA_SCOPE_VARIABLES: Record<FormulaScope, readonly FormulaVariable[]>;
|
|
813
821
|
export declare const FORMULA_QUANTITY_VARIABLE: FormulaVariable;
|
|
814
822
|
export declare const FORMULA_SURFACE_AREA_VARIABLE: FormulaVariable;
|
|
823
|
+
export declare const FORMULA_PUTTY_REQUIRED_VARIABLE: FormulaVariable;
|
|
815
824
|
export declare const FORMULA_VARIABLES: Record<RuleLevel, readonly FormulaVariable[]>;
|
|
816
825
|
export interface ValidateFormulaInput {
|
|
817
826
|
readonly level: RuleLevel;
|
|
@@ -843,11 +852,12 @@ export interface ObjectVariables {
|
|
|
843
852
|
readonly bathroomsCount: number;
|
|
844
853
|
readonly roomsFloorArea: number;
|
|
845
854
|
readonly roomsWallArea: number;
|
|
855
|
+
readonly roomsPuttyWallArea: number;
|
|
846
856
|
readonly roomsPerimeter: number;
|
|
847
857
|
readonly wetRoomsCount: number;
|
|
848
858
|
}
|
|
849
859
|
export declare function totalAreaOf(scope: Scope, totalAreaSqm: number | null, roomsFloorArea: number): number;
|
|
850
|
-
export declare function objectVariables(configuration: Configuration): ObjectVariables;
|
|
860
|
+
export declare function objectVariables(configuration: Configuration, categories: readonly PricingCategory[]): ObjectVariables;
|
|
851
861
|
//#endregion
|
|
852
862
|
//#region src/variables/product.d.ts
|
|
853
863
|
export interface ProductVariables {
|
|
@@ -858,6 +868,9 @@ export interface ProductVariables {
|
|
|
858
868
|
export declare function productVariablesOf(product: Pick<PricingProduct, 'priceCents' | 'unit'>, wastePercent: number): ProductVariables;
|
|
859
869
|
export declare function productVariables(product: PricingProduct, category: PricingCategory): ProductVariables;
|
|
860
870
|
//#endregion
|
|
871
|
+
//#region src/variables/putty.d.ts
|
|
872
|
+
export declare function puttyRequiredOn(room: RoomConfiguration, surface: PricingCategory['surface'], categories: readonly PricingCategory[]): boolean;
|
|
873
|
+
//#endregion
|
|
861
874
|
//#region src/variables/room.d.ts
|
|
862
875
|
export interface RoomVariables {
|
|
863
876
|
readonly length: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -382,6 +382,7 @@ export declare const RuleSampleSchema: z.ZodObject<{
|
|
|
382
382
|
wastePercent: z.ZodNumber;
|
|
383
383
|
}, z.core.$strip>>;
|
|
384
384
|
surfaceArea: z.ZodOptional<z.ZodNumber>;
|
|
385
|
+
puttyRequired: z.ZodOptional<z.ZodBoolean>;
|
|
385
386
|
object: z.ZodObject<{
|
|
386
387
|
scope: z.ZodEnum<{
|
|
387
388
|
FULL: "FULL";
|
|
@@ -399,6 +400,7 @@ export declare const RuleSampleSchema: z.ZodObject<{
|
|
|
399
400
|
bathroomsCount: z.ZodInt;
|
|
400
401
|
roomsFloorArea: z.ZodNumber;
|
|
401
402
|
roomsWallArea: z.ZodNumber;
|
|
403
|
+
roomsPuttyWallArea: z.ZodOptional<z.ZodNumber>;
|
|
402
404
|
roomsPerimeter: z.ZodNumber;
|
|
403
405
|
wetRoomsCount: z.ZodInt;
|
|
404
406
|
}, z.core.$strip>;
|
|
@@ -487,6 +489,7 @@ export declare const PricingCategorySchema: z.ZodObject<{
|
|
|
487
489
|
CEILING: "CEILING";
|
|
488
490
|
NONE: "NONE";
|
|
489
491
|
}>;
|
|
492
|
+
requiresPutty: z.ZodDefault<z.ZodBoolean>;
|
|
490
493
|
}, z.core.$strip>;
|
|
491
494
|
export type PricingCategory = z.infer<typeof PricingCategorySchema>;
|
|
492
495
|
export declare const PricingMaterialTypeSchema: z.ZodObject<{
|
|
@@ -521,6 +524,10 @@ export declare const PricingPackageItemSchema: z.ZodObject<{
|
|
|
521
524
|
}>>;
|
|
522
525
|
}, z.core.$strip>;
|
|
523
526
|
export type PricingPackageItem = z.infer<typeof PricingPackageItemSchema>;
|
|
527
|
+
export declare function findOptionPriceSourceViolation(priceCents: number | null, priceCoefficientKeys: readonly string[]): {
|
|
528
|
+
path: 'priceCents' | 'priceCoefficientKeys';
|
|
529
|
+
message: string;
|
|
530
|
+
} | null;
|
|
524
531
|
export declare const PricingOptionSchema: z.ZodObject<{
|
|
525
532
|
id: z.ZodString;
|
|
526
533
|
code: z.ZodNullable<z.ZodEnum<{
|
|
@@ -530,7 +537,7 @@ export declare const PricingOptionSchema: z.ZodObject<{
|
|
|
530
537
|
ENGINEERING: "ENGINEERING";
|
|
531
538
|
ADDITIONAL: "ADDITIONAL";
|
|
532
539
|
}>;
|
|
533
|
-
priceCents: z.ZodInt
|
|
540
|
+
priceCents: z.ZodNullable<z.ZodInt>;
|
|
534
541
|
unit: z.ZodEnum<{
|
|
535
542
|
PIECE: "PIECE";
|
|
536
543
|
ROOM_SQM: "ROOM_SQM";
|
|
@@ -616,6 +623,7 @@ export declare const PricingDataSchema: z.ZodObject<{
|
|
|
616
623
|
CEILING: "CEILING";
|
|
617
624
|
NONE: "NONE";
|
|
618
625
|
}>;
|
|
626
|
+
requiresPutty: z.ZodDefault<z.ZodBoolean>;
|
|
619
627
|
}, z.core.$strip>>;
|
|
620
628
|
materialTypes: z.ZodArray<z.ZodObject<{
|
|
621
629
|
id: z.ZodString;
|
|
@@ -655,7 +663,7 @@ export declare const PricingDataSchema: z.ZodObject<{
|
|
|
655
663
|
ENGINEERING: "ENGINEERING";
|
|
656
664
|
ADDITIONAL: "ADDITIONAL";
|
|
657
665
|
}>;
|
|
658
|
-
priceCents: z.ZodInt
|
|
666
|
+
priceCents: z.ZodNullable<z.ZodInt>;
|
|
659
667
|
unit: z.ZodEnum<{
|
|
660
668
|
PIECE: "PIECE";
|
|
661
669
|
ROOM_SQM: "ROOM_SQM";
|
|
@@ -812,6 +820,7 @@ export interface FormulaVariable {
|
|
|
812
820
|
export declare const FORMULA_SCOPE_VARIABLES: Record<FormulaScope, readonly FormulaVariable[]>;
|
|
813
821
|
export declare const FORMULA_QUANTITY_VARIABLE: FormulaVariable;
|
|
814
822
|
export declare const FORMULA_SURFACE_AREA_VARIABLE: FormulaVariable;
|
|
823
|
+
export declare const FORMULA_PUTTY_REQUIRED_VARIABLE: FormulaVariable;
|
|
815
824
|
export declare const FORMULA_VARIABLES: Record<RuleLevel, readonly FormulaVariable[]>;
|
|
816
825
|
export interface ValidateFormulaInput {
|
|
817
826
|
readonly level: RuleLevel;
|
|
@@ -843,11 +852,12 @@ export interface ObjectVariables {
|
|
|
843
852
|
readonly bathroomsCount: number;
|
|
844
853
|
readonly roomsFloorArea: number;
|
|
845
854
|
readonly roomsWallArea: number;
|
|
855
|
+
readonly roomsPuttyWallArea: number;
|
|
846
856
|
readonly roomsPerimeter: number;
|
|
847
857
|
readonly wetRoomsCount: number;
|
|
848
858
|
}
|
|
849
859
|
export declare function totalAreaOf(scope: Scope, totalAreaSqm: number | null, roomsFloorArea: number): number;
|
|
850
|
-
export declare function objectVariables(configuration: Configuration): ObjectVariables;
|
|
860
|
+
export declare function objectVariables(configuration: Configuration, categories: readonly PricingCategory[]): ObjectVariables;
|
|
851
861
|
//#endregion
|
|
852
862
|
//#region src/variables/product.d.ts
|
|
853
863
|
export interface ProductVariables {
|
|
@@ -858,6 +868,9 @@ export interface ProductVariables {
|
|
|
858
868
|
export declare function productVariablesOf(product: Pick<PricingProduct, 'priceCents' | 'unit'>, wastePercent: number): ProductVariables;
|
|
859
869
|
export declare function productVariables(product: PricingProduct, category: PricingCategory): ProductVariables;
|
|
860
870
|
//#endregion
|
|
871
|
+
//#region src/variables/putty.d.ts
|
|
872
|
+
export declare function puttyRequiredOn(room: RoomConfiguration, surface: PricingCategory['surface'], categories: readonly PricingCategory[]): boolean;
|
|
873
|
+
//#endregion
|
|
861
874
|
//#region src/variables/room.d.ts
|
|
862
875
|
export interface RoomVariables {
|
|
863
876
|
readonly length: number;
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,12 @@ function toCents(dollars) {
|
|
|
26
26
|
return cents;
|
|
27
27
|
}
|
|
28
28
|
//#endregion
|
|
29
|
+
//#region src/variables/putty.ts
|
|
30
|
+
function puttyRequiredOn(room, surface, categories) {
|
|
31
|
+
const selected = categories.filter((category) => category.surface === surface && room.selections.some((selection) => selection.categoryId === category.id));
|
|
32
|
+
return selected.length === 0 || selected.some((category) => category.requiresPutty);
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
29
35
|
//#region src/variables/room.ts
|
|
30
36
|
function roomVariables(room) {
|
|
31
37
|
const perimeter = 2 * (room.lengthM + room.widthM);
|
|
@@ -55,7 +61,7 @@ const WET_ROOMS = [
|
|
|
55
61
|
function totalAreaOf(scope, totalAreaSqm, roomsFloorArea) {
|
|
56
62
|
return scope === "PARTIAL" || totalAreaSqm === null ? roomsFloorArea : totalAreaSqm;
|
|
57
63
|
}
|
|
58
|
-
function objectVariables(configuration) {
|
|
64
|
+
function objectVariables(configuration, categories) {
|
|
59
65
|
const rooms = configuration.rooms.map(roomVariables);
|
|
60
66
|
const sum = (pick) => rooms.reduce((total, room) => total + pick(room), 0);
|
|
61
67
|
const roomsFloorArea = sum((room) => room.floorArea);
|
|
@@ -69,6 +75,7 @@ function objectVariables(configuration) {
|
|
|
69
75
|
bathroomsCount: configuration.rooms.filter((room) => room.roomTypeCode === "BATHROOM").length,
|
|
70
76
|
roomsFloorArea,
|
|
71
77
|
roomsWallArea: sum((room) => room.wallArea),
|
|
78
|
+
roomsPuttyWallArea: configuration.rooms.reduce((total, room) => puttyRequiredOn(room, "WALLS", categories) ? total + roomVariables(room).wallArea : total, 0),
|
|
72
79
|
roomsPerimeter: sum((room) => room.perimeter),
|
|
73
80
|
wetRoomsCount: rooms.filter((room) => WET_ROOMS.includes(room.roomType)).length
|
|
74
81
|
};
|
|
@@ -708,8 +715,11 @@ function applySurfacePrep(data, room, lines, objectScope) {
|
|
|
708
715
|
};
|
|
709
716
|
const surfaces = /* @__PURE__ */ new Map();
|
|
710
717
|
lines.forEach((line, index) => {
|
|
711
|
-
const
|
|
712
|
-
if (line.status === "OK" &&
|
|
718
|
+
const category = data.categories.find((item) => item.id === line.categoryId);
|
|
719
|
+
if (line.status === "OK" && category !== void 0 && category.surface !== "NONE") {
|
|
720
|
+
const surface = category.surface;
|
|
721
|
+
surfaces.set(surface, [...surfaces.get(surface) ?? [], index]);
|
|
722
|
+
}
|
|
713
723
|
});
|
|
714
724
|
for (const [surface, indexes] of surfaces) {
|
|
715
725
|
const rule = surfaceRuleOf(data, surface);
|
|
@@ -717,7 +727,8 @@ function applySurfacePrep(data, room, lines, objectScope) {
|
|
|
717
727
|
const outcome = runNumericFormula(rule.costFormula, {
|
|
718
728
|
variables: {
|
|
719
729
|
...variables,
|
|
720
|
-
surfaceArea: areas[surface]
|
|
730
|
+
surfaceArea: areas[surface],
|
|
731
|
+
puttyRequired: puttyRequiredOn(room, surface, data.categories)
|
|
721
732
|
},
|
|
722
733
|
coefficients: data.coefficients
|
|
723
734
|
});
|
|
@@ -1052,7 +1063,8 @@ const PricingRoomTypeSchema = z.object({
|
|
|
1052
1063
|
const PricingCategorySchema = z.object({
|
|
1053
1064
|
id: z.string(),
|
|
1054
1065
|
wastePercent: z.number().min(0),
|
|
1055
|
-
surface: SurfaceKindSchema
|
|
1066
|
+
surface: SurfaceKindSchema,
|
|
1067
|
+
requiresPutty: z.boolean().default(true)
|
|
1056
1068
|
});
|
|
1057
1069
|
const PricingMaterialTypeSchema = z.object({
|
|
1058
1070
|
id: z.string(),
|
|
@@ -1074,11 +1086,11 @@ const PricingPackageItemSchema = z.object({
|
|
|
1074
1086
|
priceCents: CentsSchema.nullable(),
|
|
1075
1087
|
unit: OptionUnitSchema.nullable()
|
|
1076
1088
|
});
|
|
1077
|
-
const
|
|
1089
|
+
const PricingOptionObjectSchema = z.object({
|
|
1078
1090
|
id: z.string(),
|
|
1079
1091
|
code: OptionCodeSchema.nullable(),
|
|
1080
1092
|
kind: OptionKindSchema,
|
|
1081
|
-
priceCents: CentsSchema,
|
|
1093
|
+
priceCents: CentsSchema.nullable(),
|
|
1082
1094
|
unit: OptionUnitSchema,
|
|
1083
1095
|
roomTypeCodes: z.array(z.string()),
|
|
1084
1096
|
priceCoefficientKeys: z.array(z.string()),
|
|
@@ -1087,6 +1099,26 @@ const PricingOptionSchema = z.object({
|
|
|
1087
1099
|
maxQuantity: z.int().nullable(),
|
|
1088
1100
|
available: z.boolean()
|
|
1089
1101
|
});
|
|
1102
|
+
function findOptionPriceSourceViolation(priceCents, priceCoefficientKeys) {
|
|
1103
|
+
const hasKeys = priceCoefficientKeys.length > 0;
|
|
1104
|
+
if (priceCents !== null && hasKeys) return {
|
|
1105
|
+
path: "priceCents",
|
|
1106
|
+
message: "priceCents must be null when priceCoefficientKeys is set"
|
|
1107
|
+
};
|
|
1108
|
+
if (priceCents === null && !hasKeys) return {
|
|
1109
|
+
path: "priceCoefficientKeys",
|
|
1110
|
+
message: "priceCoefficientKeys is required when priceCents is null"
|
|
1111
|
+
};
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
const PricingOptionSchema = PricingOptionObjectSchema.superRefine((option, context) => {
|
|
1115
|
+
const violation = findOptionPriceSourceViolation(option.priceCents, option.priceCoefficientKeys);
|
|
1116
|
+
if (violation !== null) context.addIssue({
|
|
1117
|
+
code: "custom",
|
|
1118
|
+
path: [violation.path],
|
|
1119
|
+
message: violation.message
|
|
1120
|
+
});
|
|
1121
|
+
});
|
|
1090
1122
|
const PricingDataSchema = z.object({
|
|
1091
1123
|
version: z.string().regex(/^[0-9a-f]{16}$/),
|
|
1092
1124
|
exchangeRate: ExchangeRateSchema.nullable(),
|
|
@@ -1166,6 +1198,7 @@ const RuleSampleSchema = z.object({
|
|
|
1166
1198
|
wastePercent: z.number().min(0)
|
|
1167
1199
|
}).optional(),
|
|
1168
1200
|
surfaceArea: z.number().min(0).optional(),
|
|
1201
|
+
puttyRequired: z.boolean().optional(),
|
|
1169
1202
|
object: z.object({
|
|
1170
1203
|
scope: ScopeSchema,
|
|
1171
1204
|
totalAreaSqm: z.number().positive().nullable(),
|
|
@@ -1176,6 +1209,7 @@ const RuleSampleSchema = z.object({
|
|
|
1176
1209
|
bathroomsCount: z.int().min(0),
|
|
1177
1210
|
roomsFloorArea: z.number().min(0),
|
|
1178
1211
|
roomsWallArea: z.number().min(0),
|
|
1212
|
+
roomsPuttyWallArea: z.number().min(0).optional(),
|
|
1179
1213
|
roomsPerimeter: z.number().min(0),
|
|
1180
1214
|
wetRoomsCount: z.int().min(0)
|
|
1181
1215
|
})
|
|
@@ -1260,7 +1294,7 @@ function withKopecks(line, rate) {
|
|
|
1260
1294
|
}
|
|
1261
1295
|
function estimate(configuration, data) {
|
|
1262
1296
|
const rate = data.exchangeRate?.rate ?? null;
|
|
1263
|
-
const variables = objectVariables(configuration);
|
|
1297
|
+
const variables = objectVariables(configuration, data.categories);
|
|
1264
1298
|
const lines = [
|
|
1265
1299
|
...objectRuleLines(configuration, data, "BASE", variables),
|
|
1266
1300
|
...objectRuleLines(configuration, data, "OBJECT", variables),
|
|
@@ -1356,6 +1390,7 @@ function objectScope({ object }) {
|
|
|
1356
1390
|
bathroomsCount: object.bathroomsCount,
|
|
1357
1391
|
roomsFloorArea: object.roomsFloorArea,
|
|
1358
1392
|
roomsWallArea: object.roomsWallArea,
|
|
1393
|
+
roomsPuttyWallArea: object.roomsPuttyWallArea ?? object.roomsWallArea,
|
|
1359
1394
|
roomsPerimeter: object.roomsPerimeter,
|
|
1360
1395
|
wetRoomsCount: object.wetRoomsCount
|
|
1361
1396
|
};
|
|
@@ -1371,7 +1406,8 @@ function scopeOf(level, sample) {
|
|
|
1371
1406
|
return { scope: {
|
|
1372
1407
|
...roomVariables(room),
|
|
1373
1408
|
...objectScope(sample),
|
|
1374
|
-
surfaceArea
|
|
1409
|
+
surfaceArea,
|
|
1410
|
+
puttyRequired: sample.puttyRequired ?? true
|
|
1375
1411
|
} };
|
|
1376
1412
|
}
|
|
1377
1413
|
if (room === void 0 || product === void 0) return { error: {
|
|
@@ -1513,12 +1549,14 @@ const FORMULA_SCOPE_VARIABLES = {
|
|
|
1513
1549
|
enumeration("scope", ScopeSchema.options),
|
|
1514
1550
|
number("roomsFloorArea"),
|
|
1515
1551
|
number("roomsWallArea"),
|
|
1552
|
+
number("roomsPuttyWallArea"),
|
|
1516
1553
|
number("roomsPerimeter"),
|
|
1517
1554
|
number("wetRoomsCount")
|
|
1518
1555
|
]
|
|
1519
1556
|
};
|
|
1520
1557
|
const FORMULA_QUANTITY_VARIABLE = number("quantity");
|
|
1521
1558
|
const FORMULA_SURFACE_AREA_VARIABLE = number("surfaceArea");
|
|
1559
|
+
const FORMULA_PUTTY_REQUIRED_VARIABLE = boolean("puttyRequired");
|
|
1522
1560
|
const FORMULA_VARIABLES = {
|
|
1523
1561
|
OBJECT: FORMULA_SCOPE_VARIABLES.object,
|
|
1524
1562
|
CATEGORY: [
|
|
@@ -1534,7 +1572,8 @@ const FORMULA_VARIABLES = {
|
|
|
1534
1572
|
SURFACE: [
|
|
1535
1573
|
...FORMULA_SCOPE_VARIABLES.room,
|
|
1536
1574
|
...FORMULA_SCOPE_VARIABLES.object,
|
|
1537
|
-
FORMULA_SURFACE_AREA_VARIABLE
|
|
1575
|
+
FORMULA_SURFACE_AREA_VARIABLE,
|
|
1576
|
+
FORMULA_PUTTY_REQUIRED_VARIABLE
|
|
1538
1577
|
]
|
|
1539
1578
|
};
|
|
1540
1579
|
const ALL_VARIABLE_NAMES = new Set([...FORMULA_VARIABLES.CATEGORY, ...FORMULA_VARIABLES.SURFACE].map((variable) => variable.name));
|
|
@@ -1749,4 +1788,4 @@ function validateFormula(input) {
|
|
|
1749
1788
|
};
|
|
1750
1789
|
}
|
|
1751
1790
|
//#endregion
|
|
1752
|
-
export { COEFFICIENT_NAMESPACE, CentsSchema, ConfigurationSchema, CurrencySchema, EstimateLineSchema, EstimateMessageSchema, EstimateSchema, EstimateTotalsSchema, EstimateWarningSchema, ExchangeRateSchema, FORMULA_FUNCTIONS, FORMULA_FUNCTION_TABLE, FORMULA_LIMITS, FORMULA_QUANTITY_VARIABLE, FORMULA_SCOPE_VARIABLES, FORMULA_SURFACE_AREA_VARIABLE, FORMULA_VARIABLES, FinishLevelSchema, LineComponentKindSchema, LineComponentSchema, LineStatusSchema, LocalizedTextSchema, OptionCodeSchema, OptionKindSchema, OptionSelectionSchema, OptionUnitSchema, PricingCategorySchema, PricingDataSchema, PricingLimitsSchema, PricingMaterialTypeSchema, PricingOptionSchema, PricingPackageItemSchema, PricingProductSchema, PricingRoomTypeSchema, PricingRuleSchema, ProductUnitSchema, RangeSchema, RateSourceSchema, RoomConfigurationSchema, RoomSelectionSchema, RoomTypeCodeSchema, RuleLevelSchema, RulePreviewSchema, RuleSampleSchema, ScopeSchema, SectionSchema, SectionTotalSchema, SurfaceKindSchema, checkLimits, convertCentsToKopecks, estimate, evaluate, levenshtein, objectVariables, parse, previewRule, productVariables, productVariablesOf, roomVariables, suggestNearest, toCents, totalAreaOf, validateFormula };
|
|
1791
|
+
export { COEFFICIENT_NAMESPACE, CentsSchema, ConfigurationSchema, CurrencySchema, EstimateLineSchema, EstimateMessageSchema, EstimateSchema, EstimateTotalsSchema, EstimateWarningSchema, ExchangeRateSchema, FORMULA_FUNCTIONS, FORMULA_FUNCTION_TABLE, FORMULA_LIMITS, FORMULA_PUTTY_REQUIRED_VARIABLE, FORMULA_QUANTITY_VARIABLE, FORMULA_SCOPE_VARIABLES, FORMULA_SURFACE_AREA_VARIABLE, FORMULA_VARIABLES, FinishLevelSchema, LineComponentKindSchema, LineComponentSchema, LineStatusSchema, LocalizedTextSchema, OptionCodeSchema, OptionKindSchema, OptionSelectionSchema, OptionUnitSchema, PricingCategorySchema, PricingDataSchema, PricingLimitsSchema, PricingMaterialTypeSchema, PricingOptionSchema, PricingPackageItemSchema, PricingProductSchema, PricingRoomTypeSchema, PricingRuleSchema, ProductUnitSchema, RangeSchema, RateSourceSchema, RoomConfigurationSchema, RoomSelectionSchema, RoomTypeCodeSchema, RuleLevelSchema, RulePreviewSchema, RuleSampleSchema, ScopeSchema, SectionSchema, SectionTotalSchema, SurfaceKindSchema, checkLimits, convertCentsToKopecks, estimate, evaluate, findOptionPriceSourceViolation, levenshtein, objectVariables, parse, previewRule, productVariables, productVariablesOf, puttyRequiredOn, roomVariables, suggestNearest, toCents, totalAreaOf, validateFormula };
|