@tmlmt/cooklang-parser 3.0.0-alpha.10 → 3.0.0-alpha.11
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/dist/index.cjs +704 -106
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +109 -5
- package/dist/index.d.ts +109 -5
- package/dist/index.js +701 -105
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -40,6 +40,7 @@ __export(index_exports, {
|
|
|
40
40
|
Section: () => Section,
|
|
41
41
|
ShoppingCart: () => ShoppingCart,
|
|
42
42
|
ShoppingList: () => ShoppingList,
|
|
43
|
+
convertQuantityToSystem: () => convertQuantityToSystem,
|
|
43
44
|
formatExtendedQuantity: () => formatExtendedQuantity,
|
|
44
45
|
formatItemQuantity: () => formatItemQuantity,
|
|
45
46
|
formatNumericValue: () => formatNumericValue,
|
|
@@ -51,7 +52,8 @@ __export(index_exports, {
|
|
|
51
52
|
isAlternativeSelected: () => isAlternativeSelected,
|
|
52
53
|
isAndGroup: () => isAndGroup,
|
|
53
54
|
isGroupedItem: () => isGroupedItem,
|
|
54
|
-
isSimpleGroup: () => isSimpleGroup
|
|
55
|
+
isSimpleGroup: () => isSimpleGroup,
|
|
56
|
+
renderFractionAsVulgar: () => renderFractionAsVulgar
|
|
55
57
|
});
|
|
56
58
|
module.exports = __toCommonJS(index_exports);
|
|
57
59
|
|
|
@@ -355,7 +357,8 @@ var units = [
|
|
|
355
357
|
type: "mass",
|
|
356
358
|
system: "metric",
|
|
357
359
|
aliases: ["gram", "grams", "grammes"],
|
|
358
|
-
toBase: 1
|
|
360
|
+
toBase: 1,
|
|
361
|
+
maxValue: 999
|
|
359
362
|
},
|
|
360
363
|
{
|
|
361
364
|
name: "kg",
|
|
@@ -364,20 +367,28 @@ var units = [
|
|
|
364
367
|
aliases: ["kilogram", "kilograms", "kilogrammes", "kilos", "kilo"],
|
|
365
368
|
toBase: 1e3
|
|
366
369
|
},
|
|
367
|
-
// Mass (
|
|
370
|
+
// Mass (US/UK - identical in both systems)
|
|
368
371
|
{
|
|
369
372
|
name: "oz",
|
|
370
373
|
type: "mass",
|
|
371
|
-
system: "
|
|
374
|
+
system: "ambiguous",
|
|
372
375
|
aliases: ["ounce", "ounces"],
|
|
373
|
-
toBase: 28.3495
|
|
376
|
+
toBase: 28.3495,
|
|
377
|
+
// default: US (same as UK)
|
|
378
|
+
toBaseBySystem: { US: 28.3495, UK: 28.3495 },
|
|
379
|
+
maxValue: 31,
|
|
380
|
+
// 16 oz = 1 lb, allow a bit more
|
|
381
|
+
fractions: { enabled: true, denominators: [2] }
|
|
374
382
|
},
|
|
375
383
|
{
|
|
376
384
|
name: "lb",
|
|
377
385
|
type: "mass",
|
|
378
|
-
system: "
|
|
386
|
+
system: "ambiguous",
|
|
379
387
|
aliases: ["pound", "pounds"],
|
|
380
|
-
toBase: 453.592
|
|
388
|
+
toBase: 453.592,
|
|
389
|
+
// default: US (same as UK)
|
|
390
|
+
toBaseBySystem: { US: 453.592, UK: 453.592 },
|
|
391
|
+
fractions: { enabled: true, denominators: [2, 4] }
|
|
381
392
|
},
|
|
382
393
|
// Volume (Metric)
|
|
383
394
|
{
|
|
@@ -385,21 +396,26 @@ var units = [
|
|
|
385
396
|
type: "volume",
|
|
386
397
|
system: "metric",
|
|
387
398
|
aliases: ["milliliter", "milliliters", "millilitre", "millilitres", "cc"],
|
|
388
|
-
toBase: 1
|
|
399
|
+
toBase: 1,
|
|
400
|
+
maxValue: 999
|
|
389
401
|
},
|
|
390
402
|
{
|
|
391
403
|
name: "cl",
|
|
392
404
|
type: "volume",
|
|
393
405
|
system: "metric",
|
|
394
406
|
aliases: ["centiliter", "centiliters", "centilitre", "centilitres"],
|
|
395
|
-
toBase: 10
|
|
407
|
+
toBase: 10,
|
|
408
|
+
isBestUnit: false
|
|
409
|
+
// exists but not a "best" candidate
|
|
396
410
|
},
|
|
397
411
|
{
|
|
398
412
|
name: "dl",
|
|
399
413
|
type: "volume",
|
|
400
414
|
system: "metric",
|
|
401
415
|
aliases: ["deciliter", "deciliters", "decilitre", "decilitres"],
|
|
402
|
-
toBase: 100
|
|
416
|
+
toBase: 100,
|
|
417
|
+
isBestUnit: false
|
|
418
|
+
// exists but not a "best" candidate
|
|
403
419
|
},
|
|
404
420
|
{
|
|
405
421
|
name: "l",
|
|
@@ -408,55 +424,102 @@ var units = [
|
|
|
408
424
|
aliases: ["liter", "liters", "litre", "litres"],
|
|
409
425
|
toBase: 1e3
|
|
410
426
|
},
|
|
427
|
+
// Volume (JP)
|
|
428
|
+
{
|
|
429
|
+
name: "go",
|
|
430
|
+
type: "volume",
|
|
431
|
+
system: "JP",
|
|
432
|
+
aliases: ["gou", "goo", "\u5408", "rice cup"],
|
|
433
|
+
toBase: 180,
|
|
434
|
+
maxValue: 10
|
|
435
|
+
},
|
|
436
|
+
// Volume (Ambiguous: metric/US/UK)
|
|
411
437
|
{
|
|
412
438
|
name: "tsp",
|
|
413
439
|
type: "volume",
|
|
414
|
-
system: "
|
|
440
|
+
system: "ambiguous",
|
|
415
441
|
aliases: ["teaspoon", "teaspoons"],
|
|
416
|
-
toBase: 5
|
|
442
|
+
toBase: 5,
|
|
443
|
+
// default: metric
|
|
444
|
+
toBaseBySystem: { metric: 5, US: 4.929, UK: 5.919, JP: 5 },
|
|
445
|
+
maxValue: 5,
|
|
446
|
+
// 3 tsp = 1 tbsp (but allow a bit more)
|
|
447
|
+
fractions: { enabled: true, denominators: [2, 3, 4, 8] }
|
|
417
448
|
},
|
|
418
449
|
{
|
|
419
450
|
name: "tbsp",
|
|
420
451
|
type: "volume",
|
|
421
|
-
system: "
|
|
452
|
+
system: "ambiguous",
|
|
422
453
|
aliases: ["tablespoon", "tablespoons"],
|
|
423
|
-
toBase: 15
|
|
454
|
+
toBase: 15,
|
|
455
|
+
// default: metric
|
|
456
|
+
toBaseBySystem: { metric: 15, US: 14.787, UK: 17.758, JP: 15 },
|
|
457
|
+
maxValue: 4,
|
|
458
|
+
// ~16 tbsp = 1 cup
|
|
459
|
+
fractions: { enabled: true }
|
|
424
460
|
},
|
|
425
|
-
// Volume (
|
|
461
|
+
// Volume (Ambiguous: US/UK only)
|
|
426
462
|
{
|
|
427
463
|
name: "fl-oz",
|
|
428
464
|
type: "volume",
|
|
429
|
-
system: "
|
|
465
|
+
system: "ambiguous",
|
|
430
466
|
aliases: ["fluid ounce", "fluid ounces"],
|
|
431
|
-
toBase: 29.5735
|
|
467
|
+
toBase: 29.5735,
|
|
468
|
+
// default: US
|
|
469
|
+
toBaseBySystem: { US: 29.5735, UK: 28.4131 },
|
|
470
|
+
maxValue: 15,
|
|
471
|
+
// 8 fl-oz ~ 1 cup, allow more
|
|
472
|
+
fractions: { enabled: true, denominators: [2] }
|
|
432
473
|
},
|
|
433
474
|
{
|
|
434
475
|
name: "cup",
|
|
435
476
|
type: "volume",
|
|
436
|
-
system: "
|
|
477
|
+
system: "ambiguous",
|
|
437
478
|
aliases: ["cups"],
|
|
438
|
-
toBase: 236.588
|
|
479
|
+
toBase: 236.588,
|
|
480
|
+
// default: US
|
|
481
|
+
toBaseBySystem: { US: 236.588, UK: 284.131 },
|
|
482
|
+
maxValue: 15,
|
|
483
|
+
// upgrade to gallons above 15 cups
|
|
484
|
+
fractions: { enabled: true }
|
|
439
485
|
},
|
|
440
486
|
{
|
|
441
487
|
name: "pint",
|
|
442
488
|
type: "volume",
|
|
443
|
-
system: "
|
|
489
|
+
system: "ambiguous",
|
|
444
490
|
aliases: ["pints"],
|
|
445
|
-
toBase: 473.176
|
|
491
|
+
toBase: 473.176,
|
|
492
|
+
// default: US
|
|
493
|
+
toBaseBySystem: { US: 473.176, UK: 568.261 },
|
|
494
|
+
maxValue: 3,
|
|
495
|
+
// 2 pints = 1 quart
|
|
496
|
+
fractions: { enabled: true, denominators: [2] },
|
|
497
|
+
isBestUnit: false
|
|
498
|
+
// exists but not a "best" candidate
|
|
446
499
|
},
|
|
447
500
|
{
|
|
448
501
|
name: "quart",
|
|
449
502
|
type: "volume",
|
|
450
|
-
system: "
|
|
503
|
+
system: "ambiguous",
|
|
451
504
|
aliases: ["quarts"],
|
|
452
|
-
toBase: 946.353
|
|
505
|
+
toBase: 946.353,
|
|
506
|
+
// default: US
|
|
507
|
+
toBaseBySystem: { US: 946.353, UK: 1136.52 },
|
|
508
|
+
maxValue: 3,
|
|
509
|
+
// 4 quarts = 1 gallon
|
|
510
|
+
fractions: { enabled: true, denominators: [2] },
|
|
511
|
+
isBestUnit: false
|
|
512
|
+
// exists but not a "best" candidate
|
|
453
513
|
},
|
|
454
514
|
{
|
|
455
515
|
name: "gallon",
|
|
456
516
|
type: "volume",
|
|
457
|
-
system: "
|
|
517
|
+
system: "ambiguous",
|
|
458
518
|
aliases: ["gallons"],
|
|
459
|
-
toBase: 3785.41
|
|
519
|
+
toBase: 3785.41,
|
|
520
|
+
// default: US
|
|
521
|
+
toBaseBySystem: { US: 3785.41, UK: 4546.09 },
|
|
522
|
+
fractions: { enabled: true, denominators: [2] }
|
|
460
523
|
},
|
|
461
524
|
// Count units (no conversion, but recognized as a type)
|
|
462
525
|
{
|
|
@@ -464,7 +527,8 @@ var units = [
|
|
|
464
527
|
type: "count",
|
|
465
528
|
system: "metric",
|
|
466
529
|
aliases: ["pieces", "pc"],
|
|
467
|
-
toBase: 1
|
|
530
|
+
toBase: 1,
|
|
531
|
+
maxValue: 999
|
|
468
532
|
}
|
|
469
533
|
];
|
|
470
534
|
var unitMap = /* @__PURE__ */ new Map();
|
|
@@ -488,8 +552,14 @@ function isNoUnit(unit) {
|
|
|
488
552
|
return resolveUnit(unit.name).name === NO_UNIT;
|
|
489
553
|
}
|
|
490
554
|
|
|
555
|
+
// src/units/conversion.ts
|
|
556
|
+
var import_big2 = __toESM(require("big.js"), 1);
|
|
557
|
+
|
|
491
558
|
// src/quantities/numeric.ts
|
|
492
559
|
var import_big = __toESM(require("big.js"), 1);
|
|
560
|
+
var DEFAULT_DENOMINATORS = [2, 3, 4];
|
|
561
|
+
var DEFAULT_FRACTION_ACCURACY = 0.05;
|
|
562
|
+
var DEFAULT_MAX_WHOLE = 4;
|
|
493
563
|
function gcd(a2, b) {
|
|
494
564
|
return b === 0 ? a2 : gcd(b, a2 % b);
|
|
495
565
|
}
|
|
@@ -510,6 +580,41 @@ function simplifyFraction(num, den) {
|
|
|
510
580
|
return { type: "fraction", num: simplifiedNum, den: simplifiedDen };
|
|
511
581
|
}
|
|
512
582
|
}
|
|
583
|
+
function approximateFraction(value, denominators = DEFAULT_DENOMINATORS, accuracy = DEFAULT_FRACTION_ACCURACY, maxWhole = DEFAULT_MAX_WHOLE) {
|
|
584
|
+
if (value <= 0 || !Number.isFinite(value)) {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
const wholePart = Math.floor(value);
|
|
588
|
+
if (wholePart > maxWhole) {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
const fractionalPart = value - wholePart;
|
|
592
|
+
if (fractionalPart < 1e-4) {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
let bestFraction = null;
|
|
596
|
+
for (const den of denominators) {
|
|
597
|
+
const exactNum = value * den;
|
|
598
|
+
const roundedNum = Math.round(exactNum);
|
|
599
|
+
if (roundedNum === 0) continue;
|
|
600
|
+
const approximatedValue = roundedNum / den;
|
|
601
|
+
const relativeError = Math.abs(approximatedValue - value) / value;
|
|
602
|
+
if (relativeError <= accuracy) {
|
|
603
|
+
if (!bestFraction || relativeError < bestFraction.error) {
|
|
604
|
+
bestFraction = { num: roundedNum, den, error: relativeError };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (!bestFraction) {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
const commonDivisor = gcd(bestFraction.num, bestFraction.den);
|
|
612
|
+
return {
|
|
613
|
+
type: "fraction",
|
|
614
|
+
num: bestFraction.num / commonDivisor,
|
|
615
|
+
den: bestFraction.den / commonDivisor
|
|
616
|
+
};
|
|
617
|
+
}
|
|
513
618
|
function getNumericValue(v) {
|
|
514
619
|
if (v.type === "decimal") {
|
|
515
620
|
return v.decimal;
|
|
@@ -558,9 +663,35 @@ function addNumericValues(val1, val2) {
|
|
|
558
663
|
};
|
|
559
664
|
}
|
|
560
665
|
}
|
|
561
|
-
var toRoundedDecimal = (v) => {
|
|
666
|
+
var toRoundedDecimal = (v, precision = 3) => {
|
|
562
667
|
const value = v.type === "decimal" ? v.decimal : v.num / v.den;
|
|
563
|
-
|
|
668
|
+
if (value === 0) {
|
|
669
|
+
return { type: "decimal", decimal: 0 };
|
|
670
|
+
}
|
|
671
|
+
const absValue = Math.abs(value);
|
|
672
|
+
if (absValue >= 1e3) {
|
|
673
|
+
return { type: "decimal", decimal: Math.round(value) };
|
|
674
|
+
}
|
|
675
|
+
const magnitude = Math.floor(Math.log10(absValue));
|
|
676
|
+
const scale = Math.pow(10, precision - 1 - magnitude);
|
|
677
|
+
const rounded = Math.round(value * scale) / scale;
|
|
678
|
+
return { type: "decimal", decimal: rounded };
|
|
679
|
+
};
|
|
680
|
+
var formatOutputValue = (value, unitDef, precision = 3) => {
|
|
681
|
+
if (unitDef.fractions?.enabled) {
|
|
682
|
+
const denominators = unitDef.fractions.denominators ?? DEFAULT_DENOMINATORS;
|
|
683
|
+
const maxWhole = unitDef.fractions.maxWhole ?? DEFAULT_MAX_WHOLE;
|
|
684
|
+
const fraction = approximateFraction(
|
|
685
|
+
value,
|
|
686
|
+
denominators,
|
|
687
|
+
DEFAULT_FRACTION_ACCURACY,
|
|
688
|
+
maxWhole
|
|
689
|
+
);
|
|
690
|
+
if (fraction) {
|
|
691
|
+
return fraction;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return toRoundedDecimal({ type: "decimal", decimal: value }, precision);
|
|
564
695
|
};
|
|
565
696
|
function multiplyQuantityValue(value, factor) {
|
|
566
697
|
if (value.type === "fixed") {
|
|
@@ -594,6 +725,143 @@ function getAverageValue(q) {
|
|
|
594
725
|
}
|
|
595
726
|
}
|
|
596
727
|
|
|
728
|
+
// src/units/compatibility.ts
|
|
729
|
+
function areUnitsGroupable(u1, u2) {
|
|
730
|
+
if (u1.name === u2.name) {
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
if (u1.type === "other" || u2.type === "other") {
|
|
734
|
+
return false;
|
|
735
|
+
}
|
|
736
|
+
if (u1.type === u2.type && u1.system === u2.system) {
|
|
737
|
+
return true;
|
|
738
|
+
}
|
|
739
|
+
if (u1.type === u2.type) {
|
|
740
|
+
if (u1.system === "ambiguous" && u2.system === "metric" && u1.toBaseBySystem?.metric !== void 0) {
|
|
741
|
+
return true;
|
|
742
|
+
}
|
|
743
|
+
if (u2.system === "ambiguous" && u1.system === "metric" && u2.toBaseBySystem?.metric !== void 0) {
|
|
744
|
+
return true;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
function areUnitsConvertible(u1, u2) {
|
|
750
|
+
if (u1.name === u2.name) return true;
|
|
751
|
+
if (u1.type === "other" || u2.type === "other") return false;
|
|
752
|
+
return u1.type === u2.type;
|
|
753
|
+
}
|
|
754
|
+
function isUnitCompatibleWithSystem(unit, system) {
|
|
755
|
+
if (unit.system === system) return true;
|
|
756
|
+
if (unit.system === "ambiguous") {
|
|
757
|
+
if (unit.toBaseBySystem) {
|
|
758
|
+
return system in unit.toBaseBySystem;
|
|
759
|
+
}
|
|
760
|
+
if (system === "metric") return true;
|
|
761
|
+
}
|
|
762
|
+
if (unit.system === "metric" && system === "JP") {
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// src/units/conversion.ts
|
|
769
|
+
var EPSILON = 0.01;
|
|
770
|
+
var DEFAULT_MAX_VALUE = 999;
|
|
771
|
+
function isCloseToInteger(value) {
|
|
772
|
+
return Math.abs(value - Math.round(value)) < EPSILON;
|
|
773
|
+
}
|
|
774
|
+
function getMaxValue(unit) {
|
|
775
|
+
return unit.maxValue ?? DEFAULT_MAX_VALUE;
|
|
776
|
+
}
|
|
777
|
+
function isValueInRange(value, unit) {
|
|
778
|
+
const maxValue = getMaxValue(unit);
|
|
779
|
+
if (value >= 1 && value <= maxValue) {
|
|
780
|
+
return true;
|
|
781
|
+
}
|
|
782
|
+
if (value > 0 && value < 1 && unit.fractions?.enabled) {
|
|
783
|
+
const denominators = unit.fractions.denominators ?? DEFAULT_DENOMINATORS;
|
|
784
|
+
const maxWhole = unit.fractions.maxWhole ?? DEFAULT_MAX_WHOLE;
|
|
785
|
+
const fraction = approximateFraction(
|
|
786
|
+
value,
|
|
787
|
+
denominators,
|
|
788
|
+
DEFAULT_FRACTION_ACCURACY,
|
|
789
|
+
maxWhole
|
|
790
|
+
);
|
|
791
|
+
return fraction !== null;
|
|
792
|
+
}
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
function findBestUnit(valueInBase, unitType, system, inputUnits) {
|
|
796
|
+
const inputUnitNames = new Set(inputUnits.map((u) => u.name));
|
|
797
|
+
const candidates = units.filter(
|
|
798
|
+
(u) => u.type === unitType && isUnitCompatibleWithSystem(u, system) && (u.isBestUnit !== false || inputUnitNames.has(u.name))
|
|
799
|
+
);
|
|
800
|
+
if (candidates.length === 0) {
|
|
801
|
+
const fallbackUnit = inputUnits[0];
|
|
802
|
+
return {
|
|
803
|
+
unit: fallbackUnit,
|
|
804
|
+
value: valueInBase / getToBase(fallbackUnit, system)
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
const candidatesWithValues = candidates.map((unit) => ({
|
|
808
|
+
unit,
|
|
809
|
+
value: valueInBase / getToBase(unit, system)
|
|
810
|
+
}));
|
|
811
|
+
const inRange = candidatesWithValues.filter(
|
|
812
|
+
(c) => isValueInRange(c.value, c.unit)
|
|
813
|
+
);
|
|
814
|
+
if (inRange.length > 0) {
|
|
815
|
+
const integersInInputFamily = inRange.filter(
|
|
816
|
+
(c) => isCloseToInteger(c.value) && inputUnitNames.has(c.unit.name)
|
|
817
|
+
);
|
|
818
|
+
if (integersInInputFamily.length > 0) {
|
|
819
|
+
return integersInInputFamily.sort((a2, b) => a2.value - b.value)[0];
|
|
820
|
+
}
|
|
821
|
+
const integersAny = inRange.filter((c) => isCloseToInteger(c.value));
|
|
822
|
+
if (integersAny.length > 0) {
|
|
823
|
+
return integersAny.sort((a2, b) => a2.value - b.value)[0];
|
|
824
|
+
}
|
|
825
|
+
return inRange.sort((a2, b) => {
|
|
826
|
+
const aInFamily = inputUnitNames.has(a2.unit.name) ? 0 : 1;
|
|
827
|
+
const bInFamily = inputUnitNames.has(b.unit.name) ? 0 : 1;
|
|
828
|
+
if (aInFamily !== bInFamily) return aInFamily - bInFamily;
|
|
829
|
+
return a2.value - b.value;
|
|
830
|
+
})[0];
|
|
831
|
+
}
|
|
832
|
+
return candidatesWithValues.sort((a2, b) => {
|
|
833
|
+
const aMaxValue = getMaxValue(a2.unit);
|
|
834
|
+
const bMaxValue = getMaxValue(b.unit);
|
|
835
|
+
const aDistance = a2.value < 1 ? 1 - a2.value : a2.value - aMaxValue;
|
|
836
|
+
const bDistance = b.value < 1 ? 1 - b.value : b.value - bMaxValue;
|
|
837
|
+
return aDistance - bDistance;
|
|
838
|
+
})[0];
|
|
839
|
+
}
|
|
840
|
+
function getUnitRatio(q1, q2) {
|
|
841
|
+
const q1Value = getAverageValue(q1.quantity);
|
|
842
|
+
const q2Value = getAverageValue(q2.quantity);
|
|
843
|
+
const factor = "toBase" in q1.unit && "toBase" in q2.unit ? q1.unit.toBase / q2.unit.toBase : 1;
|
|
844
|
+
if (typeof q1Value !== "number" || typeof q2Value !== "number") {
|
|
845
|
+
throw Error(
|
|
846
|
+
"One of both values is not a number, so a ratio cannot be computed"
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
return (0, import_big2.default)(q1Value).times(factor).div(q2Value);
|
|
850
|
+
}
|
|
851
|
+
function getBaseUnitRatio(q, qRef) {
|
|
852
|
+
if ("toBase" in q.unit && "toBase" in qRef.unit) {
|
|
853
|
+
return q.unit.toBase / qRef.unit.toBase;
|
|
854
|
+
} else {
|
|
855
|
+
return 1;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
function getToBase(unit, system) {
|
|
859
|
+
if (unit.system === "ambiguous" && system && unit.toBaseBySystem) {
|
|
860
|
+
return unit.toBaseBySystem[system] ?? unit.toBase;
|
|
861
|
+
}
|
|
862
|
+
return unit.toBase;
|
|
863
|
+
}
|
|
864
|
+
|
|
597
865
|
// src/errors.ts
|
|
598
866
|
var ReferencedItemCannotBeRedefinedError = class extends Error {
|
|
599
867
|
constructor(item_type, item_name, new_modifier) {
|
|
@@ -732,11 +1000,6 @@ function normalizeAllUnits(q) {
|
|
|
732
1000
|
return newQ;
|
|
733
1001
|
}
|
|
734
1002
|
}
|
|
735
|
-
var convertQuantityValue = (value, def, targetDef) => {
|
|
736
|
-
if (def.name === targetDef.name) return value;
|
|
737
|
-
const factor = def.toBase / targetDef.toBase;
|
|
738
|
-
return multiplyQuantityValue(value, factor);
|
|
739
|
-
};
|
|
740
1003
|
function getDefaultQuantityValue() {
|
|
741
1004
|
return { type: "fixed", value: { type: "decimal", decimal: 0 } };
|
|
742
1005
|
}
|
|
@@ -763,7 +1026,7 @@ function addQuantityValues(v1, v2) {
|
|
|
763
1026
|
);
|
|
764
1027
|
return { type: "range", min: newMin, max: newMax };
|
|
765
1028
|
}
|
|
766
|
-
function addQuantities(q1, q2) {
|
|
1029
|
+
function addQuantities(q1, q2, system) {
|
|
767
1030
|
const v1 = q1.quantity;
|
|
768
1031
|
const v2 = q2.quantity;
|
|
769
1032
|
if (v1.type === "fixed" && v1.value.type === "text" || v2.type === "fixed" && v2.value.type === "text") {
|
|
@@ -781,35 +1044,129 @@ function addQuantities(q1, q2) {
|
|
|
781
1044
|
if ((q2.unit?.name === "" || q2.unit === void 0) && q1.unit !== void 0) {
|
|
782
1045
|
return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
|
|
783
1046
|
}
|
|
784
|
-
if (!q1.unit && !q2.unit
|
|
1047
|
+
if (!q1.unit && !q2.unit) {
|
|
1048
|
+
return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
|
|
1049
|
+
}
|
|
1050
|
+
if (q1.unit && q2.unit && q1.unit.name.toLowerCase() === q2.unit.name.toLowerCase()) {
|
|
1051
|
+
if (unit1Def) {
|
|
1052
|
+
const effectiveSystem = system ?? (["metric", "JP"].includes(unit1Def.system) ? unit1Def.system : "US");
|
|
1053
|
+
return addAndFindBestUnit(v1, v2, unit1Def, unit1Def, effectiveSystem, [
|
|
1054
|
+
unit1Def
|
|
1055
|
+
]);
|
|
1056
|
+
}
|
|
785
1057
|
return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
|
|
786
1058
|
}
|
|
787
1059
|
if (unit1Def && unit2Def) {
|
|
788
|
-
if (unit1Def
|
|
1060
|
+
if (!areUnitsConvertible(unit1Def, unit2Def)) {
|
|
789
1061
|
throw new IncompatibleUnitsError(
|
|
790
1062
|
`${unit1Def.type} (${q1.unit?.name})`,
|
|
791
1063
|
`${unit2Def.type} (${q2.unit?.name})`
|
|
792
1064
|
);
|
|
793
1065
|
}
|
|
794
|
-
let
|
|
795
|
-
if (
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1066
|
+
let effectiveSystem = system;
|
|
1067
|
+
if (!effectiveSystem) {
|
|
1068
|
+
if (unit1Def.system === "metric" || unit2Def.system === "metric") {
|
|
1069
|
+
effectiveSystem = "metric";
|
|
1070
|
+
} else {
|
|
1071
|
+
if (unit1Def.system === "JP" && unit2Def.system === "JP") {
|
|
1072
|
+
effectiveSystem = "JP";
|
|
1073
|
+
} else {
|
|
1074
|
+
const unit1SupportsUS = unit1Def.system === "US" || unit1Def.system === "ambiguous" && unit1Def.toBaseBySystem && "US" in unit1Def.toBaseBySystem;
|
|
1075
|
+
const unit2SupportsUS = unit2Def.system === "US" || unit2Def.system === "ambiguous" && unit2Def.toBaseBySystem && "US" in unit2Def.toBaseBySystem;
|
|
1076
|
+
effectiveSystem = unit1SupportsUS && unit2SupportsUS ? "US" : "metric";
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
802
1079
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1080
|
+
return addAndFindBestUnit(v1, v2, unit1Def, unit2Def, effectiveSystem, [
|
|
1081
|
+
unit1Def,
|
|
1082
|
+
unit2Def
|
|
1083
|
+
]);
|
|
807
1084
|
}
|
|
808
1085
|
throw new IncompatibleUnitsError(
|
|
809
1086
|
q1.unit?.name,
|
|
810
1087
|
q2.unit?.name
|
|
811
1088
|
);
|
|
812
1089
|
}
|
|
1090
|
+
function addAndFindBestUnit(v1, v2, unit1Def, unit2Def, system, inputUnits) {
|
|
1091
|
+
const toBase1 = getToBase(unit1Def, system);
|
|
1092
|
+
const toBase2 = getToBase(unit2Def, system);
|
|
1093
|
+
let sumInBase;
|
|
1094
|
+
if (v1.type === "fixed" && v2.type === "fixed") {
|
|
1095
|
+
const val1 = getNumericValue(v1.value);
|
|
1096
|
+
const val2 = getNumericValue(v2.value);
|
|
1097
|
+
sumInBase = val1 * toBase1 + val2 * toBase2;
|
|
1098
|
+
} else {
|
|
1099
|
+
const avg1 = getAverageValue(v1);
|
|
1100
|
+
const avg2 = getAverageValue(v2);
|
|
1101
|
+
sumInBase = avg1 * toBase1 + avg2 * toBase2;
|
|
1102
|
+
}
|
|
1103
|
+
const { unit: bestUnit, value: bestValue } = findBestUnit(
|
|
1104
|
+
sumInBase,
|
|
1105
|
+
unit1Def.type,
|
|
1106
|
+
system,
|
|
1107
|
+
inputUnits
|
|
1108
|
+
);
|
|
1109
|
+
const formattedValue = formatOutputValue(bestValue, bestUnit);
|
|
1110
|
+
if (v1.type === "range" || v2.type === "range") {
|
|
1111
|
+
const r1 = v1.type === "range" ? v1 : { type: "range", min: v1.value, max: v1.value };
|
|
1112
|
+
const r2 = v2.type === "range" ? v2 : { type: "range", min: v2.value, max: v2.value };
|
|
1113
|
+
const minInBase = getNumericValue(r1.min) * toBase1 + getNumericValue(r2.min) * toBase2;
|
|
1114
|
+
const maxInBase = getNumericValue(r1.max) * toBase1 + getNumericValue(r2.max) * toBase2;
|
|
1115
|
+
const bestToBase = getToBase(bestUnit, system);
|
|
1116
|
+
const minValue = minInBase / bestToBase;
|
|
1117
|
+
const maxValue = maxInBase / bestToBase;
|
|
1118
|
+
return {
|
|
1119
|
+
quantity: {
|
|
1120
|
+
type: "range",
|
|
1121
|
+
min: formatOutputValue(minValue, bestUnit),
|
|
1122
|
+
max: formatOutputValue(maxValue, bestUnit)
|
|
1123
|
+
},
|
|
1124
|
+
unit: { name: bestUnit.name }
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
return {
|
|
1128
|
+
quantity: { type: "fixed", value: formattedValue },
|
|
1129
|
+
unit: { name: bestUnit.name }
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
function convertQuantityToSystem(quantity, system) {
|
|
1133
|
+
const unitDef = resolveUnit(
|
|
1134
|
+
typeof quantity.unit === "string" ? quantity.unit : quantity.unit?.name
|
|
1135
|
+
);
|
|
1136
|
+
if (unitDef.type === "other" || !("toBase" in unitDef)) {
|
|
1137
|
+
return void 0;
|
|
1138
|
+
}
|
|
1139
|
+
const avgValue = getAverageValue(quantity.quantity);
|
|
1140
|
+
if (typeof avgValue !== "number") {
|
|
1141
|
+
return void 0;
|
|
1142
|
+
}
|
|
1143
|
+
const toBase = getToBase(unitDef, system);
|
|
1144
|
+
const valueInBase = avgValue * toBase;
|
|
1145
|
+
const { unit: bestUnit, value: bestValue } = findBestUnit(
|
|
1146
|
+
valueInBase,
|
|
1147
|
+
unitDef.type,
|
|
1148
|
+
system,
|
|
1149
|
+
[unitDef]
|
|
1150
|
+
);
|
|
1151
|
+
const formattedValue = formatOutputValue(bestValue, bestUnit);
|
|
1152
|
+
if (quantity.quantity.type === "range") {
|
|
1153
|
+
const bestToBase = getToBase(bestUnit, system);
|
|
1154
|
+
const minValue = getNumericValue(quantity.quantity.min) * toBase / bestToBase;
|
|
1155
|
+
const maxValue = getNumericValue(quantity.quantity.max) * toBase / bestToBase;
|
|
1156
|
+
return {
|
|
1157
|
+
quantity: {
|
|
1158
|
+
type: "range",
|
|
1159
|
+
min: formatOutputValue(minValue, bestUnit),
|
|
1160
|
+
max: formatOutputValue(maxValue, bestUnit)
|
|
1161
|
+
},
|
|
1162
|
+
unit: { name: bestUnit.name }
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
return {
|
|
1166
|
+
quantity: { type: "fixed", value: formattedValue },
|
|
1167
|
+
unit: { name: bestUnit.name }
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
813
1170
|
function toPlainUnit(quantity) {
|
|
814
1171
|
if (isQuantity(quantity))
|
|
815
1172
|
return quantity.unit ? { ...quantity, unit: quantity.unit.name } : quantity;
|
|
@@ -916,6 +1273,53 @@ var flattenPlainUnitGroup = (summed) => {
|
|
|
916
1273
|
];
|
|
917
1274
|
}
|
|
918
1275
|
};
|
|
1276
|
+
function applyBestUnit(q, system) {
|
|
1277
|
+
if (!q.unit?.name) {
|
|
1278
|
+
return q;
|
|
1279
|
+
}
|
|
1280
|
+
const unitDef = resolveUnit(q.unit.name);
|
|
1281
|
+
if (unitDef.type === "other") {
|
|
1282
|
+
return q;
|
|
1283
|
+
}
|
|
1284
|
+
if (q.quantity.type === "fixed" && q.quantity.value.type === "text") {
|
|
1285
|
+
return q;
|
|
1286
|
+
}
|
|
1287
|
+
const avgValue = getAverageValue(q.quantity);
|
|
1288
|
+
const effectiveSystem = system ?? (["metric", "JP"].includes(unitDef.system) ? unitDef.system : "US");
|
|
1289
|
+
const toBase = getToBase(unitDef, effectiveSystem);
|
|
1290
|
+
const valueInBase = avgValue * toBase;
|
|
1291
|
+
const { unit: bestUnit, value: bestValue } = findBestUnit(
|
|
1292
|
+
valueInBase,
|
|
1293
|
+
unitDef.type,
|
|
1294
|
+
effectiveSystem,
|
|
1295
|
+
[unitDef]
|
|
1296
|
+
);
|
|
1297
|
+
const originalCanonicalName = normalizeUnit(q.unit.name)?.name;
|
|
1298
|
+
if (bestUnit.name === originalCanonicalName) {
|
|
1299
|
+
return q;
|
|
1300
|
+
}
|
|
1301
|
+
const formattedValue = formatOutputValue(bestValue, bestUnit);
|
|
1302
|
+
if (q.quantity.type === "range") {
|
|
1303
|
+
const bestToBase = getToBase(bestUnit, effectiveSystem);
|
|
1304
|
+
const minValue = getNumericValue(q.quantity.min) * toBase / bestToBase;
|
|
1305
|
+
const maxValue = getNumericValue(q.quantity.max) * toBase / bestToBase;
|
|
1306
|
+
return {
|
|
1307
|
+
quantity: {
|
|
1308
|
+
type: "range",
|
|
1309
|
+
min: formatOutputValue(minValue, bestUnit),
|
|
1310
|
+
max: formatOutputValue(maxValue, bestUnit)
|
|
1311
|
+
},
|
|
1312
|
+
unit: { name: bestUnit.name }
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
return {
|
|
1316
|
+
quantity: {
|
|
1317
|
+
type: "fixed",
|
|
1318
|
+
value: formattedValue
|
|
1319
|
+
},
|
|
1320
|
+
unit: { name: bestUnit.name }
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
919
1323
|
|
|
920
1324
|
// src/utils/parser_helpers.ts
|
|
921
1325
|
function flushPendingNote(section, noteItems) {
|
|
@@ -1119,6 +1523,18 @@ function extractMetadata(content) {
|
|
|
1119
1523
|
const stringMetaValue = parseSimpleMetaVar(metadataContent, metaVar);
|
|
1120
1524
|
if (stringMetaValue) metadata[metaVar] = stringMetaValue;
|
|
1121
1525
|
}
|
|
1526
|
+
let unitSystem;
|
|
1527
|
+
const unitSystemRaw = parseSimpleMetaVar(metadataContent, "unit system");
|
|
1528
|
+
if (unitSystemRaw) {
|
|
1529
|
+
metadata["unit system"] = unitSystemRaw;
|
|
1530
|
+
const unitSystemMap = {
|
|
1531
|
+
metric: "metric",
|
|
1532
|
+
us: "US",
|
|
1533
|
+
uk: "UK",
|
|
1534
|
+
jp: "JP"
|
|
1535
|
+
};
|
|
1536
|
+
unitSystem = unitSystemMap[unitSystemRaw.toLowerCase()];
|
|
1537
|
+
}
|
|
1122
1538
|
for (const metaVar of ["serves", "yield", "servings"]) {
|
|
1123
1539
|
const scalingMetaValue = parseScalingMetaVar(metadataContent, metaVar);
|
|
1124
1540
|
if (scalingMetaValue && scalingMetaValue[1]) {
|
|
@@ -1130,7 +1546,7 @@ function extractMetadata(content) {
|
|
|
1130
1546
|
const listMetaValue = parseListMetaVar(metadataContent, metaVar);
|
|
1131
1547
|
if (listMetaValue) metadata[metaVar] = listMetaValue;
|
|
1132
1548
|
}
|
|
1133
|
-
return { metadata, servings };
|
|
1549
|
+
return { metadata, servings, unitSystem };
|
|
1134
1550
|
}
|
|
1135
1551
|
function isPositiveIntegerString(str) {
|
|
1136
1552
|
return /^\d+$/.test(str);
|
|
@@ -1313,44 +1729,14 @@ var Section = class {
|
|
|
1313
1729
|
// src/quantities/alternatives.ts
|
|
1314
1730
|
var import_big3 = __toESM(require("big.js"), 1);
|
|
1315
1731
|
|
|
1316
|
-
// src/units/conversion.ts
|
|
1317
|
-
var import_big2 = __toESM(require("big.js"), 1);
|
|
1318
|
-
function getUnitRatio(q1, q2) {
|
|
1319
|
-
const q1Value = getAverageValue(q1.quantity);
|
|
1320
|
-
const q2Value = getAverageValue(q2.quantity);
|
|
1321
|
-
const factor = "toBase" in q1.unit && "toBase" in q2.unit ? q1.unit.toBase / q2.unit.toBase : 1;
|
|
1322
|
-
if (typeof q1Value !== "number" || typeof q2Value !== "number") {
|
|
1323
|
-
throw Error(
|
|
1324
|
-
"One of both values is not a number, so a ratio cannot be computed"
|
|
1325
|
-
);
|
|
1326
|
-
}
|
|
1327
|
-
return (0, import_big2.default)(q1Value).times(factor).div(q2Value);
|
|
1328
|
-
}
|
|
1329
|
-
function getBaseUnitRatio(q, qRef) {
|
|
1330
|
-
if ("toBase" in q.unit && "toBase" in qRef.unit) {
|
|
1331
|
-
return q.unit.toBase / qRef.unit.toBase;
|
|
1332
|
-
} else {
|
|
1333
|
-
return 1;
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
1732
|
// src/units/lookup.ts
|
|
1338
|
-
function areUnitsCompatible(u1, u2) {
|
|
1339
|
-
if (u1.name === u2.name) {
|
|
1340
|
-
return true;
|
|
1341
|
-
}
|
|
1342
|
-
if (u1.type !== "other" && u1.type === u2.type && u1.system === u2.system) {
|
|
1343
|
-
return true;
|
|
1344
|
-
}
|
|
1345
|
-
return false;
|
|
1346
|
-
}
|
|
1347
1733
|
function findListWithCompatibleQuantity(list, quantity) {
|
|
1348
1734
|
const quantityWithUnitDef = {
|
|
1349
1735
|
...quantity,
|
|
1350
1736
|
unit: resolveUnit(quantity.unit?.name)
|
|
1351
1737
|
};
|
|
1352
1738
|
return list.find(
|
|
1353
|
-
(l) => l.some((lq) =>
|
|
1739
|
+
(l) => l.some((lq) => areUnitsGroupable(lq.unit, quantityWithUnitDef.unit))
|
|
1354
1740
|
);
|
|
1355
1741
|
}
|
|
1356
1742
|
function findCompatibleQuantityWithinList(list, quantity) {
|
|
@@ -1407,11 +1793,9 @@ function getEquivalentUnitsLists(...quantities) {
|
|
|
1407
1793
|
});
|
|
1408
1794
|
function findLinkIndexForUnits(lists, unitsToCheck) {
|
|
1409
1795
|
return lists.findIndex((l) => {
|
|
1410
|
-
const
|
|
1796
|
+
const listItems = l.map((q) => resolveUnit(q.unit?.name));
|
|
1411
1797
|
return unitsToCheck.some(
|
|
1412
|
-
(u) =>
|
|
1413
|
-
(lu) => lu.name === u?.name || lu.system === u?.system && lu.type === u?.type && lu.type !== "other"
|
|
1414
|
-
)
|
|
1798
|
+
(u) => u && listItems.some((lu) => areUnitsGroupable(lu, u))
|
|
1415
1799
|
);
|
|
1416
1800
|
});
|
|
1417
1801
|
}
|
|
@@ -1423,16 +1807,18 @@ function getEquivalentUnitsLists(...quantities) {
|
|
|
1423
1807
|
unit: resolveUnit(v.unit?.name, v.unit?.integerProtected)
|
|
1424
1808
|
};
|
|
1425
1809
|
const commonQuantity = og.or.find(
|
|
1426
|
-
(q) => isQuantity(q) &&
|
|
1810
|
+
(q) => isQuantity(q) && areUnitsGroupable(q.unit, normalizedV.unit)
|
|
1427
1811
|
);
|
|
1428
1812
|
if (commonQuantity) {
|
|
1429
1813
|
acc.push(normalizedV);
|
|
1430
|
-
|
|
1814
|
+
if (!unitRatio) {
|
|
1815
|
+
unitRatio = getUnitRatio(normalizedV, commonQuantity);
|
|
1816
|
+
}
|
|
1431
1817
|
}
|
|
1432
1818
|
return acc;
|
|
1433
1819
|
}, []);
|
|
1434
1820
|
for (const newQ of og.or) {
|
|
1435
|
-
if (commonUnitList.some((q) =>
|
|
1821
|
+
if (commonUnitList.some((q) => areUnitsGroupable(q.unit, newQ.unit))) {
|
|
1436
1822
|
continue;
|
|
1437
1823
|
} else {
|
|
1438
1824
|
const scaledQuantity = multiplyQuantityValue(newQ.quantity, unitRatio);
|
|
@@ -1549,7 +1935,7 @@ function reduceOrsToFirstEquivalent(unitList, quantities) {
|
|
|
1549
1935
|
return reduceToQuantity(qListModified[0]);
|
|
1550
1936
|
});
|
|
1551
1937
|
}
|
|
1552
|
-
function addQuantitiesOrGroups(
|
|
1938
|
+
function addQuantitiesOrGroups(quantities, system) {
|
|
1553
1939
|
if (quantities.length === 0)
|
|
1554
1940
|
return {
|
|
1555
1941
|
sum: {
|
|
@@ -1579,7 +1965,7 @@ function addQuantitiesOrGroups(...quantities) {
|
|
|
1579
1965
|
unit: resolveUnit(nextQ.unit?.name)
|
|
1580
1966
|
});
|
|
1581
1967
|
} else {
|
|
1582
|
-
const sumQ = addQuantities(existingQ, nextQ);
|
|
1968
|
+
const sumQ = addQuantities(existingQ, nextQ, system);
|
|
1583
1969
|
existingQ.quantity = sumQ.quantity;
|
|
1584
1970
|
existingQ.unit = resolveUnit(sumQ.unit?.name);
|
|
1585
1971
|
}
|
|
@@ -1589,7 +1975,7 @@ function addQuantitiesOrGroups(...quantities) {
|
|
|
1589
1975
|
}
|
|
1590
1976
|
return { sum: { and: sum }, unitsLists };
|
|
1591
1977
|
}
|
|
1592
|
-
function regroupQuantitiesAndExpandEquivalents(sum, unitsLists) {
|
|
1978
|
+
function regroupQuantitiesAndExpandEquivalents(sum, unitsLists, system) {
|
|
1593
1979
|
const sumQuantities = isAndGroup(sum) ? sum.and : [sum];
|
|
1594
1980
|
const result = [];
|
|
1595
1981
|
const processedQuantities = /* @__PURE__ */ new Set();
|
|
@@ -1617,9 +2003,19 @@ function regroupQuantitiesAndExpandEquivalents(sum, unitsLists) {
|
|
|
1617
2003
|
}
|
|
1618
2004
|
return main.reduce((acc, v) => {
|
|
1619
2005
|
const mainInList = findCompatibleQuantityWithinList(list, v);
|
|
2006
|
+
const conversionRatio = getBaseUnitRatio(v, mainInList);
|
|
2007
|
+
const valueInOriginalUnit = (0, import_big3.default)(getAverageValue(v.quantity)).times(
|
|
2008
|
+
conversionRatio
|
|
2009
|
+
);
|
|
1620
2010
|
const newValue = {
|
|
1621
2011
|
quantity: multiplyQuantityValue(
|
|
1622
|
-
|
|
2012
|
+
{
|
|
2013
|
+
type: "fixed",
|
|
2014
|
+
value: {
|
|
2015
|
+
type: "decimal",
|
|
2016
|
+
decimal: valueInOriginalUnit.toNumber()
|
|
2017
|
+
}
|
|
2018
|
+
},
|
|
1623
2019
|
(0, import_big3.default)(getAverageValue(equiv.quantity)).div(
|
|
1624
2020
|
getAverageValue(mainInList.quantity)
|
|
1625
2021
|
)
|
|
@@ -1628,7 +2024,7 @@ function regroupQuantitiesAndExpandEquivalents(sum, unitsLists) {
|
|
|
1628
2024
|
if (equiv.unit && !isNoUnit(equiv.unit)) {
|
|
1629
2025
|
newValue.unit = { name: equiv.unit.name };
|
|
1630
2026
|
}
|
|
1631
|
-
return addQuantities(acc, newValue);
|
|
2027
|
+
return addQuantities(acc, newValue, system);
|
|
1632
2028
|
}, initialValue);
|
|
1633
2029
|
});
|
|
1634
2030
|
if (main.length + equivalents.length > 1) {
|
|
@@ -1645,12 +2041,16 @@ function regroupQuantitiesAndExpandEquivalents(sum, unitsLists) {
|
|
|
1645
2041
|
sumQuantities.filter((q) => !processedQuantities.has(q)).forEach((q) => result.push(deNormalizeQuantity(q)));
|
|
1646
2042
|
return result;
|
|
1647
2043
|
}
|
|
1648
|
-
function addEquivalentsAndSimplify(
|
|
2044
|
+
function addEquivalentsAndSimplify(quantities, system) {
|
|
1649
2045
|
if (quantities.length === 1) {
|
|
1650
2046
|
return toPlainUnit(quantities[0]);
|
|
1651
2047
|
}
|
|
1652
|
-
const { sum, unitsLists } = addQuantitiesOrGroups(
|
|
1653
|
-
const regrouped = regroupQuantitiesAndExpandEquivalents(
|
|
2048
|
+
const { sum, unitsLists } = addQuantitiesOrGroups(quantities, system);
|
|
2049
|
+
const regrouped = regroupQuantitiesAndExpandEquivalents(
|
|
2050
|
+
sum,
|
|
2051
|
+
unitsLists,
|
|
2052
|
+
system
|
|
2053
|
+
);
|
|
1654
2054
|
if (regrouped.length === 1) {
|
|
1655
2055
|
return toPlainUnit(regrouped[0]);
|
|
1656
2056
|
} else {
|
|
@@ -1710,6 +2110,15 @@ var _Recipe = class _Recipe {
|
|
|
1710
2110
|
this.parse(content);
|
|
1711
2111
|
}
|
|
1712
2112
|
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Gets the unit system specified in the recipe metadata.
|
|
2115
|
+
* Used for resolving ambiguous units like tsp, tbsp, cup, etc.
|
|
2116
|
+
*
|
|
2117
|
+
* @returns The unit system if specified, or undefined to use defaults
|
|
2118
|
+
*/
|
|
2119
|
+
get unitSystem() {
|
|
2120
|
+
return _Recipe.unitSystems.get(this);
|
|
2121
|
+
}
|
|
1713
2122
|
/**
|
|
1714
2123
|
* Gets the current item count for this recipe.
|
|
1715
2124
|
*/
|
|
@@ -2222,13 +2631,16 @@ var _Recipe = class _Recipe {
|
|
|
2222
2631
|
if (groupsForIng) {
|
|
2223
2632
|
const quantityGroups = [];
|
|
2224
2633
|
for (const [, group] of groupsForIng) {
|
|
2225
|
-
const summed = addEquivalentsAndSimplify(
|
|
2634
|
+
const summed = addEquivalentsAndSimplify(
|
|
2635
|
+
group.quantities,
|
|
2636
|
+
this.unitSystem
|
|
2637
|
+
);
|
|
2226
2638
|
const flattened = flattenPlainUnitGroup(summed);
|
|
2227
2639
|
const alternatives = group.alternativeQuantities.size > 0 ? [...group.alternativeQuantities].map(([altIdx, altQtys]) => ({
|
|
2228
2640
|
index: altIdx,
|
|
2229
2641
|
...altQtys.length > 0 && {
|
|
2230
2642
|
quantities: flattenPlainUnitGroup(
|
|
2231
|
-
addEquivalentsAndSimplify(
|
|
2643
|
+
addEquivalentsAndSimplify(altQtys, this.unitSystem)
|
|
2232
2644
|
).flatMap(
|
|
2233
2645
|
/* v8 ignore next -- item.and branch requires complex nested AND-with-equivalents structure */
|
|
2234
2646
|
(item) => "quantity" in item ? [item] : item.and
|
|
@@ -2267,9 +2679,10 @@ var _Recipe = class _Recipe {
|
|
|
2267
2679
|
*/
|
|
2268
2680
|
parse(content) {
|
|
2269
2681
|
const cleanContent = content.replace(metadataRegex, "").replace(commentRegex, "").replace(blockCommentRegex, "").trim().split(/\r\n?|\n/);
|
|
2270
|
-
const { metadata, servings } = extractMetadata(content);
|
|
2682
|
+
const { metadata, servings, unitSystem } = extractMetadata(content);
|
|
2271
2683
|
this.metadata = metadata;
|
|
2272
2684
|
this.servings = servings;
|
|
2685
|
+
if (unitSystem) _Recipe.unitSystems.set(this, unitSystem);
|
|
2273
2686
|
let blankLineBefore = true;
|
|
2274
2687
|
let section = new Section();
|
|
2275
2688
|
const items = [];
|
|
@@ -2427,6 +2840,7 @@ var _Recipe = class _Recipe {
|
|
|
2427
2840
|
if (originalServings === void 0 || originalServings === 0) {
|
|
2428
2841
|
originalServings = 1;
|
|
2429
2842
|
}
|
|
2843
|
+
const unitSystem = this.unitSystem;
|
|
2430
2844
|
function scaleAlternativesBy(alternatives, factor2) {
|
|
2431
2845
|
for (const alternative of alternatives) {
|
|
2432
2846
|
if (alternative.itemQuantity) {
|
|
@@ -2454,6 +2868,20 @@ var _Recipe = class _Recipe {
|
|
|
2454
2868
|
}
|
|
2455
2869
|
);
|
|
2456
2870
|
}
|
|
2871
|
+
const optimizedPrimary = applyBestUnit(
|
|
2872
|
+
{
|
|
2873
|
+
quantity: alternative.itemQuantity.quantity,
|
|
2874
|
+
unit: alternative.itemQuantity.unit
|
|
2875
|
+
},
|
|
2876
|
+
unitSystem
|
|
2877
|
+
);
|
|
2878
|
+
alternative.itemQuantity.quantity = optimizedPrimary.quantity;
|
|
2879
|
+
alternative.itemQuantity.unit = optimizedPrimary.unit;
|
|
2880
|
+
if (alternative.itemQuantity.equivalents) {
|
|
2881
|
+
alternative.itemQuantity.equivalents = alternative.itemQuantity.equivalents.map(
|
|
2882
|
+
(eq) => applyBestUnit(eq, unitSystem)
|
|
2883
|
+
);
|
|
2884
|
+
}
|
|
2457
2885
|
}
|
|
2458
2886
|
}
|
|
2459
2887
|
}
|
|
@@ -2514,6 +2942,139 @@ var _Recipe = class _Recipe {
|
|
|
2514
2942
|
}
|
|
2515
2943
|
return newRecipe;
|
|
2516
2944
|
}
|
|
2945
|
+
/**
|
|
2946
|
+
* Converts all ingredient quantities in the recipe to a target unit system.
|
|
2947
|
+
*
|
|
2948
|
+
* @param system - The target unit system to convert to (metric, US, UK, JP)
|
|
2949
|
+
* @param method - How to handle existing quantities:
|
|
2950
|
+
* - "keep": Keep all existing equivalents (swap if needed, or add converted)
|
|
2951
|
+
* - "replace": Replace primary with target system quantity, discard equivalent used for conversion
|
|
2952
|
+
* - "remove": Only keep target system quantity, delete all equivalents
|
|
2953
|
+
* @returns A new Recipe instance with converted quantities
|
|
2954
|
+
*
|
|
2955
|
+
* @example
|
|
2956
|
+
* ```typescript
|
|
2957
|
+
* // Convert a recipe to metric, keeping original units as equivalents
|
|
2958
|
+
* const metricRecipe = recipe.convertTo("metric", "keep");
|
|
2959
|
+
*
|
|
2960
|
+
* // Convert to US units, removing all other equivalents
|
|
2961
|
+
* const usRecipe = recipe.convertTo("US", "remove");
|
|
2962
|
+
* ```
|
|
2963
|
+
*/
|
|
2964
|
+
convertTo(system, method) {
|
|
2965
|
+
const newRecipe = this.clone();
|
|
2966
|
+
function buildNewPrimary(convertedQty, oldPrimary, remainingEquivalents, scalable, integerProtected, source) {
|
|
2967
|
+
const newUnit = integerProtected && convertedQty.unit ? { name: convertedQty.unit.name, integerProtected: true } : convertedQty.unit;
|
|
2968
|
+
const newPrimary = {
|
|
2969
|
+
quantity: convertedQty.quantity,
|
|
2970
|
+
unit: newUnit,
|
|
2971
|
+
scalable
|
|
2972
|
+
};
|
|
2973
|
+
if (method === "remove") {
|
|
2974
|
+
return newPrimary;
|
|
2975
|
+
} else if (method === "replace") {
|
|
2976
|
+
if (remainingEquivalents.length > 0) {
|
|
2977
|
+
newPrimary.equivalents = remainingEquivalents;
|
|
2978
|
+
if (source === "converted") newPrimary.equivalents.push(oldPrimary);
|
|
2979
|
+
}
|
|
2980
|
+
} else {
|
|
2981
|
+
newPrimary.equivalents = [oldPrimary, ...remainingEquivalents];
|
|
2982
|
+
}
|
|
2983
|
+
return newPrimary;
|
|
2984
|
+
}
|
|
2985
|
+
function convertItemQuantity(itemQuantity) {
|
|
2986
|
+
const primaryUnit = resolveUnit(itemQuantity.unit?.name);
|
|
2987
|
+
const equivalents = itemQuantity.equivalents ?? [];
|
|
2988
|
+
const oldPrimary = {
|
|
2989
|
+
quantity: itemQuantity.quantity,
|
|
2990
|
+
unit: itemQuantity.unit
|
|
2991
|
+
};
|
|
2992
|
+
if (primaryUnit.type !== "other" && isUnitCompatibleWithSystem(primaryUnit, system)) {
|
|
2993
|
+
if (method === "remove") {
|
|
2994
|
+
return { ...itemQuantity, equivalents: void 0 };
|
|
2995
|
+
}
|
|
2996
|
+
return itemQuantity;
|
|
2997
|
+
}
|
|
2998
|
+
const targetEquivIndex = equivalents.findIndex((eq) => {
|
|
2999
|
+
const eqUnit = resolveUnit(eq.unit?.name);
|
|
3000
|
+
return eqUnit.type !== "other" && isUnitCompatibleWithSystem(eqUnit, system);
|
|
3001
|
+
});
|
|
3002
|
+
if (targetEquivIndex !== -1) {
|
|
3003
|
+
const targetEquiv = equivalents[targetEquivIndex];
|
|
3004
|
+
const remainingEquivalents = equivalents.filter(
|
|
3005
|
+
(_, i2) => i2 !== targetEquivIndex
|
|
3006
|
+
);
|
|
3007
|
+
return buildNewPrimary(
|
|
3008
|
+
targetEquiv,
|
|
3009
|
+
oldPrimary,
|
|
3010
|
+
remainingEquivalents,
|
|
3011
|
+
itemQuantity.scalable,
|
|
3012
|
+
targetEquiv.unit?.integerProtected,
|
|
3013
|
+
"swapped"
|
|
3014
|
+
);
|
|
3015
|
+
}
|
|
3016
|
+
const converted = convertQuantityToSystem(oldPrimary, system);
|
|
3017
|
+
if (converted && converted.unit) {
|
|
3018
|
+
return buildNewPrimary(
|
|
3019
|
+
converted,
|
|
3020
|
+
oldPrimary,
|
|
3021
|
+
equivalents,
|
|
3022
|
+
itemQuantity.scalable,
|
|
3023
|
+
itemQuantity.unit?.integerProtected,
|
|
3024
|
+
"swapped"
|
|
3025
|
+
);
|
|
3026
|
+
}
|
|
3027
|
+
for (let i2 = 0; i2 < equivalents.length; i2++) {
|
|
3028
|
+
const equiv = equivalents[i2];
|
|
3029
|
+
const convertedEquiv = convertQuantityToSystem(equiv, system);
|
|
3030
|
+
if (convertedEquiv && convertedEquiv.unit) {
|
|
3031
|
+
const remainingEquivalents = method === "keep" ? equivalents : equivalents.filter((_, idx) => idx !== i2);
|
|
3032
|
+
return buildNewPrimary(
|
|
3033
|
+
convertedEquiv,
|
|
3034
|
+
oldPrimary,
|
|
3035
|
+
remainingEquivalents,
|
|
3036
|
+
itemQuantity.scalable,
|
|
3037
|
+
equiv.unit?.integerProtected,
|
|
3038
|
+
"converted"
|
|
3039
|
+
);
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
if (method === "remove") {
|
|
3043
|
+
return { ...itemQuantity, equivalents: void 0 };
|
|
3044
|
+
} else {
|
|
3045
|
+
return itemQuantity;
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
function convertAlternatives(alternatives) {
|
|
3049
|
+
for (const alternative of alternatives) {
|
|
3050
|
+
if (alternative.itemQuantity) {
|
|
3051
|
+
alternative.itemQuantity = convertItemQuantity(
|
|
3052
|
+
alternative.itemQuantity
|
|
3053
|
+
);
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
for (const section of newRecipe.sections) {
|
|
3058
|
+
for (const step of section.content.filter(
|
|
3059
|
+
(item) => item.type === "step"
|
|
3060
|
+
)) {
|
|
3061
|
+
for (const item of step.items.filter(
|
|
3062
|
+
(item2) => item2.type === "ingredient"
|
|
3063
|
+
)) {
|
|
3064
|
+
convertAlternatives(item.alternatives);
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
for (const alternatives of newRecipe.choices.ingredientGroups.values()) {
|
|
3069
|
+
convertAlternatives(alternatives);
|
|
3070
|
+
}
|
|
3071
|
+
for (const alternatives of newRecipe.choices.ingredientItems.values()) {
|
|
3072
|
+
convertAlternatives(alternatives);
|
|
3073
|
+
}
|
|
3074
|
+
newRecipe._populate_ingredient_quantities();
|
|
3075
|
+
if (method !== "keep") _Recipe.unitSystems.set(newRecipe, system);
|
|
3076
|
+
return newRecipe;
|
|
3077
|
+
}
|
|
2517
3078
|
/**
|
|
2518
3079
|
* Gets the number of servings for the recipe.
|
|
2519
3080
|
* @private
|
|
@@ -2547,6 +3108,11 @@ var _Recipe = class _Recipe {
|
|
|
2547
3108
|
return newRecipe;
|
|
2548
3109
|
}
|
|
2549
3110
|
};
|
|
3111
|
+
/**
|
|
3112
|
+
* External storage for unit system (not a property on instances).
|
|
3113
|
+
* Used for resolving ambiguous units during quantity addition.
|
|
3114
|
+
*/
|
|
3115
|
+
__publicField(_Recipe, "unitSystems", /* @__PURE__ */ new WeakMap());
|
|
2550
3116
|
/**
|
|
2551
3117
|
* External storage for item count (not a property on instances).
|
|
2552
3118
|
* Used for giving ID numbers to items during parsing.
|
|
@@ -2598,10 +3164,10 @@ var ShoppingList = class {
|
|
|
2598
3164
|
existing.quantityTotal
|
|
2599
3165
|
);
|
|
2600
3166
|
const existingQuantities = isAndGroup(existingQuantityTotalExtended) ? existingQuantityTotalExtended.and : [existingQuantityTotalExtended];
|
|
2601
|
-
existing.quantityTotal = addEquivalentsAndSimplify(
|
|
3167
|
+
existing.quantityTotal = addEquivalentsAndSimplify([
|
|
2602
3168
|
...existingQuantities,
|
|
2603
3169
|
...newQuantities
|
|
2604
|
-
);
|
|
3170
|
+
]);
|
|
2605
3171
|
return;
|
|
2606
3172
|
} catch {
|
|
2607
3173
|
}
|
|
@@ -2652,7 +3218,7 @@ var ShoppingList = class {
|
|
|
2652
3218
|
(q) => extendAllUnits(q)
|
|
2653
3219
|
);
|
|
2654
3220
|
const totalQuantity = addEquivalentsAndSimplify(
|
|
2655
|
-
|
|
3221
|
+
extendedQuantities
|
|
2656
3222
|
);
|
|
2657
3223
|
addIngredientQuantity(ingredient.name, totalQuantity);
|
|
2658
3224
|
}
|
|
@@ -2948,12 +3514,12 @@ var ShoppingCart = class {
|
|
|
2948
3514
|
alternative.quantity = scaledQuantity;
|
|
2949
3515
|
const matchOptions = normalizedOptions2.filter(
|
|
2950
3516
|
(option) => option.sizes.some(
|
|
2951
|
-
(s) =>
|
|
3517
|
+
(s) => areUnitsGroupable(alternative.unit, s.unit)
|
|
2952
3518
|
)
|
|
2953
3519
|
);
|
|
2954
3520
|
if (matchOptions.length > 0) {
|
|
2955
3521
|
const findCompatibleSize = (option) => option.sizes.find(
|
|
2956
|
-
(s) =>
|
|
3522
|
+
(s) => areUnitsGroupable(alternative.unit, s.unit)
|
|
2957
3523
|
);
|
|
2958
3524
|
if (matchOptions.length == 1) {
|
|
2959
3525
|
const matchedOption = matchOptions[0];
|
|
@@ -3055,10 +3621,37 @@ var ShoppingCart = class {
|
|
|
3055
3621
|
};
|
|
3056
3622
|
|
|
3057
3623
|
// src/utils/render_helpers.ts
|
|
3058
|
-
|
|
3624
|
+
var VULGAR_FRACTIONS = {
|
|
3625
|
+
"1/2": "\xBD",
|
|
3626
|
+
"1/3": "\u2153",
|
|
3627
|
+
"2/3": "\u2154",
|
|
3628
|
+
"1/4": "\xBC",
|
|
3629
|
+
"3/4": "\xBE",
|
|
3630
|
+
"1/8": "\u215B",
|
|
3631
|
+
"3/8": "\u215C",
|
|
3632
|
+
"5/8": "\u215D",
|
|
3633
|
+
"7/8": "\u215E"
|
|
3634
|
+
};
|
|
3635
|
+
function renderFractionAsVulgar(num, den) {
|
|
3636
|
+
const wholePart = Math.floor(num / den);
|
|
3637
|
+
const remainder = num % den;
|
|
3638
|
+
if (remainder === 0) {
|
|
3639
|
+
return String(wholePart);
|
|
3640
|
+
}
|
|
3641
|
+
const fractionKey = `${remainder}/${den}`;
|
|
3642
|
+
const vulgar = VULGAR_FRACTIONS[fractionKey];
|
|
3643
|
+
if (wholePart > 0) {
|
|
3644
|
+
return vulgar ? `${wholePart}${vulgar}` : `${wholePart} ${remainder}/${den}`;
|
|
3645
|
+
}
|
|
3646
|
+
return vulgar ?? `${num}/${den}`;
|
|
3647
|
+
}
|
|
3648
|
+
function formatNumericValue(value, useVulgar = true) {
|
|
3059
3649
|
if (value.type === "decimal") {
|
|
3060
3650
|
return String(value.decimal);
|
|
3061
3651
|
}
|
|
3652
|
+
if (useVulgar) {
|
|
3653
|
+
return renderFractionAsVulgar(value.num, value.den);
|
|
3654
|
+
}
|
|
3062
3655
|
return `${value.num}/${value.den}`;
|
|
3063
3656
|
}
|
|
3064
3657
|
function formatSingleValue(value) {
|
|
@@ -3125,6 +3718,7 @@ function isAlternativeSelected(recipe, choices, item, alternativeIndex) {
|
|
|
3125
3718
|
Section,
|
|
3126
3719
|
ShoppingCart,
|
|
3127
3720
|
ShoppingList,
|
|
3721
|
+
convertQuantityToSystem,
|
|
3128
3722
|
formatExtendedQuantity,
|
|
3129
3723
|
formatItemQuantity,
|
|
3130
3724
|
formatNumericValue,
|
|
@@ -3136,11 +3730,15 @@ function isAlternativeSelected(recipe, choices, item, alternativeIndex) {
|
|
|
3136
3730
|
isAlternativeSelected,
|
|
3137
3731
|
isAndGroup,
|
|
3138
3732
|
isGroupedItem,
|
|
3139
|
-
isSimpleGroup
|
|
3733
|
+
isSimpleGroup,
|
|
3734
|
+
renderFractionAsVulgar
|
|
3140
3735
|
});
|
|
3141
3736
|
/* v8 ignore else -- @preserve */
|
|
3737
|
+
/* v8 ignore next -- @preserve: defensive fallback for ambiguous units without toBaseBySystem */
|
|
3738
|
+
/* v8 ignore start -- @preserve: defensive fallback that shouldn't happen with valid inputs */
|
|
3142
3739
|
// v8 ignore else -- @preserve
|
|
3143
|
-
/* v8 ignore else -- expliciting error type -- @preserve */
|
|
3144
3740
|
// v8 ignore if -- @preserve
|
|
3741
|
+
/* v8 ignore else -- expliciting error type -- @preserve */
|
|
3145
3742
|
/* v8 ignore if -- @preserve */
|
|
3743
|
+
// v8 ignore next -- @preserve
|
|
3146
3744
|
//# sourceMappingURL=index.cjs.map
|