@recipe-scope/quantity-parser 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +95 -0
- package/dist/index.d.mts +181 -0
- package/dist/index.d.ts +181 -0
- package/dist/index.js +416 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +381 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +47 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
// src/strategies/GramsInParensStrategy.ts
|
|
2
|
+
var GramsInParensStrategy = class {
|
|
3
|
+
parse(quantity, options) {
|
|
4
|
+
const gramInParenMatch = quantity.match(/[((](?:[^)\d]*)([\d.]+)\s*(?:g|グラム|cc|ml)[))]/i);
|
|
5
|
+
if (gramInParenMatch && gramInParenMatch[1]) {
|
|
6
|
+
const val = parseFloat(gramInParenMatch[1]);
|
|
7
|
+
if (!isNaN(val)) {
|
|
8
|
+
const unitMatch = gramInParenMatch[0].match(/(g|グラム|cc|ml)/i);
|
|
9
|
+
let unitInParen = unitMatch ? unitMatch[1].toLowerCase() : "g";
|
|
10
|
+
if (unitInParen === "\u30B0\u30E9\u30E0") unitInParen = "g";
|
|
11
|
+
else if (unitInParen === "cc") unitInParen = "ml";
|
|
12
|
+
return { value: val, unit: unitInParen };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// src/strategies/RangeStrategy.ts
|
|
20
|
+
var RangeStrategy = class {
|
|
21
|
+
parse(quantity, options) {
|
|
22
|
+
const rangeMatch = quantity.match(/^([^0-9.\/~]*)([0-9.]+)[~]([0-9.]+)(.*)$/);
|
|
23
|
+
if (rangeMatch) {
|
|
24
|
+
const min = parseFloat(rangeMatch[2]);
|
|
25
|
+
const max = parseFloat(rangeMatch[3]);
|
|
26
|
+
if (!isNaN(min) && !isNaN(max)) {
|
|
27
|
+
let value = min;
|
|
28
|
+
const mode = options?.rangeMode || "min";
|
|
29
|
+
if (mode === "max") {
|
|
30
|
+
value = max;
|
|
31
|
+
} else if (mode === "mean") {
|
|
32
|
+
value = (min + max) / 2;
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
value,
|
|
36
|
+
unit: (rangeMatch[1] || "") + (rangeMatch[4] || "")
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// src/strategies/FractionStrategy.ts
|
|
45
|
+
var FractionStrategy = class {
|
|
46
|
+
parse(quantity, options) {
|
|
47
|
+
const mixedFractionMatch = quantity.match(/^([^0-9と・.\/]*?)(\d+)(?:と|・|・)(\d+)\/(\d+)(.*)$/);
|
|
48
|
+
if (mixedFractionMatch) {
|
|
49
|
+
const integer = parseInt(mixedFractionMatch[2], 10);
|
|
50
|
+
const numerator = parseInt(mixedFractionMatch[3], 10);
|
|
51
|
+
const denominator = parseInt(mixedFractionMatch[4], 10);
|
|
52
|
+
if (!isNaN(integer) && !isNaN(numerator) && !isNaN(denominator) && denominator !== 0) {
|
|
53
|
+
return {
|
|
54
|
+
value: integer + numerator / denominator,
|
|
55
|
+
unit: (mixedFractionMatch[1] || "") + (mixedFractionMatch[5] || "")
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const halfMatch = quantity.match(/^(?:1\/2|半)(.*)$/);
|
|
60
|
+
if (halfMatch) {
|
|
61
|
+
let u = halfMatch[1] || "";
|
|
62
|
+
if (u === "\u5206") u = "";
|
|
63
|
+
if (u.includes("~")) {
|
|
64
|
+
const parts = u.split("~");
|
|
65
|
+
if (parts.length > 1) {
|
|
66
|
+
const afterTilde = parts[1];
|
|
67
|
+
const unitMatch = afterTilde.match(/[\d.]*(.*)/);
|
|
68
|
+
if (unitMatch) {
|
|
69
|
+
u = unitMatch[1];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { value: 0.5, unit: u };
|
|
74
|
+
}
|
|
75
|
+
const fractionMatch = quantity.match(/^([^0-9\/]*?)(\d+)\/(\d+)(.*)$/);
|
|
76
|
+
if (fractionMatch) {
|
|
77
|
+
const numerator = parseInt(fractionMatch[2], 10);
|
|
78
|
+
const denominator = parseInt(fractionMatch[3], 10);
|
|
79
|
+
let suffix = fractionMatch[4] || "";
|
|
80
|
+
if (suffix.includes("~")) {
|
|
81
|
+
const parts = suffix.split("~");
|
|
82
|
+
if (parts.length > 1) {
|
|
83
|
+
const afterTilde = parts[1];
|
|
84
|
+
const unitMatch = afterTilde.match(/[\d.]*(.*)/);
|
|
85
|
+
if (unitMatch) {
|
|
86
|
+
suffix = unitMatch[1];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!isNaN(numerator) && !isNaN(denominator) && denominator !== 0) {
|
|
91
|
+
return {
|
|
92
|
+
value: numerator / denominator,
|
|
93
|
+
unit: (fractionMatch[1] || "") + suffix
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const japaneseFractionMatch = quantity.match(/^([^0-9分]*?)(\d+)分(?:の)?(\d+)(.*)$/);
|
|
98
|
+
if (japaneseFractionMatch) {
|
|
99
|
+
const prefix = japaneseFractionMatch[1] || "";
|
|
100
|
+
const denominator = parseInt(japaneseFractionMatch[2], 10);
|
|
101
|
+
const numerator = parseInt(japaneseFractionMatch[3], 10);
|
|
102
|
+
const suffix = japaneseFractionMatch[4] || "";
|
|
103
|
+
if (!isNaN(numerator) && !isNaN(denominator) && denominator !== 0) {
|
|
104
|
+
return {
|
|
105
|
+
value: numerator / denominator,
|
|
106
|
+
unit: prefix + suffix
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// src/strategies/UnitPrefixStrategy.ts
|
|
115
|
+
var UnitPrefixStrategy = class {
|
|
116
|
+
parse(quantity, options) {
|
|
117
|
+
const daiShoPattern = /^(.*?)(大|小)(さじ)?((?:\d+(?:[と・]\d+\/\d+))|(?:\d+\/\d+)|(?:\d*\.?\d+))(.*)$/;
|
|
118
|
+
const daiShoMatch = quantity.match(daiShoPattern);
|
|
119
|
+
if (daiShoMatch) {
|
|
120
|
+
const sizeChar = daiShoMatch[2];
|
|
121
|
+
const sajiPresent = daiShoMatch[3];
|
|
122
|
+
const valueStr = daiShoMatch[4];
|
|
123
|
+
const suffix = daiShoMatch[5] || "";
|
|
124
|
+
const potentialUnit = (suffix || "").trim();
|
|
125
|
+
const knownUnits = ["\u500B", "\u7F36", "\u679A", "\u672C", "\u675F", "\u682A", "\u5C3E", "\u304B\u3051", "\u7247", "\u4E01", "\u888B", "\u676F", "cm", "\u623F", "\u7BC0", "\u30D1\u30C3\u30AF", "\u7389", "\u81B3", "\u4EBA\u5206", "gr", "g", "ml", "cc"];
|
|
126
|
+
const startsWithKnownUnit = knownUnits.some((u) => potentialUnit.startsWith(u));
|
|
127
|
+
if (!sajiPresent && startsWithKnownUnit) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const parsedSajiUnit = sizeChar === "\u5927" ? "\u5927\u3055\u3058" : "\u5C0F\u3055\u3058";
|
|
131
|
+
let value = NaN;
|
|
132
|
+
const innerMixed = valueStr.match(/^(\d+)[と・](\d+)\/(\d+)$/);
|
|
133
|
+
const innerFraction = valueStr.match(/^(\d+)\/(\d+)$/);
|
|
134
|
+
if (innerMixed) {
|
|
135
|
+
value = parseInt(innerMixed[1], 10) + parseInt(innerMixed[2], 10) / parseInt(innerMixed[3], 10);
|
|
136
|
+
} else if (innerFraction) {
|
|
137
|
+
value = parseInt(innerFraction[1], 10) / parseInt(innerFraction[2], 10);
|
|
138
|
+
} else {
|
|
139
|
+
value = parseFloat(valueStr);
|
|
140
|
+
}
|
|
141
|
+
if (!isNaN(value)) {
|
|
142
|
+
return { value, unit: parsedSajiUnit };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const knownUnitPrefixMatch = quantity.match(/^(大さじ|小さじ|カップ)([0-9.]+)(.*)$/);
|
|
146
|
+
if (knownUnitPrefixMatch) {
|
|
147
|
+
const value = parseFloat(knownUnitPrefixMatch[2]);
|
|
148
|
+
if (!isNaN(value)) {
|
|
149
|
+
return {
|
|
150
|
+
value,
|
|
151
|
+
unit: (knownUnitPrefixMatch[1] + (knownUnitPrefixMatch[3] || "")).trim()
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// src/strategies/SuffixStrategy.ts
|
|
160
|
+
var SuffixStrategy = class {
|
|
161
|
+
parse(quantity, options) {
|
|
162
|
+
const gramOrMlMatch = quantity.match(/([\d.]+)\s*(g|グラム|ml|ミリリットル)/i);
|
|
163
|
+
if (gramOrMlMatch) {
|
|
164
|
+
const val = parseFloat(gramOrMlMatch[1]);
|
|
165
|
+
if (!isNaN(val)) {
|
|
166
|
+
let u = gramOrMlMatch[2].toLowerCase();
|
|
167
|
+
if (u === "\u30B0\u30E9\u30E0") u = "g";
|
|
168
|
+
if (u === "\u30DF\u30EA\u30EA\u30C3\u30C8\u30EB") u = "ml";
|
|
169
|
+
return { value: val, unit: u };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const numberMatch = quantity.match(/^([^0-9.]*)([0-9.]+)(.*)$/);
|
|
173
|
+
if (numberMatch) {
|
|
174
|
+
const p = numberMatch[1] || "";
|
|
175
|
+
const n = parseFloat(numberMatch[2]);
|
|
176
|
+
const s = numberMatch[3] || "";
|
|
177
|
+
if (!isNaN(n)) {
|
|
178
|
+
const unit = (p + s).trim();
|
|
179
|
+
return { value: n, unit };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// src/strategies/TubeStrategy.ts
|
|
187
|
+
var TubeStrategy = class {
|
|
188
|
+
parse(quantity, options) {
|
|
189
|
+
const tubeMatch = quantity.match(/チューブ(?:で)?([\d.]+)(?:cm|センチ)/i);
|
|
190
|
+
if (tubeMatch && tubeMatch[1]) {
|
|
191
|
+
const val = parseFloat(tubeMatch[1]);
|
|
192
|
+
if (!isNaN(val)) {
|
|
193
|
+
return { value: val, unit: "\u30C1\u30E5\u30FC\u30D6cm" };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// src/QuantityParser.ts
|
|
201
|
+
var QuantityParser = class {
|
|
202
|
+
/**
|
|
203
|
+
* Parses a quantity string into a numeric value and a normalized unit.
|
|
204
|
+
* @param quantityStr - The quantity string to parse (e.g., "大さじ1", "100g", "1/2カップ")
|
|
205
|
+
* @param options - Parsing options (e.g. rangeMode)
|
|
206
|
+
* @returns Parsed result with value, unit, and normalized quantity string
|
|
207
|
+
*/
|
|
208
|
+
static parse(quantityStr, options) {
|
|
209
|
+
if (!quantityStr) {
|
|
210
|
+
return { value: 0, unit: "" };
|
|
211
|
+
}
|
|
212
|
+
const normalizedQuantity = this.preprocess(quantityStr);
|
|
213
|
+
let result = null;
|
|
214
|
+
const quantityWithoutParens = normalizedQuantity.replace(/[((][^))]*[))]/g, "");
|
|
215
|
+
for (const strategy of this.strategies) {
|
|
216
|
+
let input = quantityWithoutParens;
|
|
217
|
+
if (strategy instanceof TubeStrategy || strategy instanceof GramsInParensStrategy) {
|
|
218
|
+
input = normalizedQuantity;
|
|
219
|
+
}
|
|
220
|
+
result = strategy.parse(input, options);
|
|
221
|
+
if (result) break;
|
|
222
|
+
}
|
|
223
|
+
if (result) {
|
|
224
|
+
const normalizedUnit = this.postprocessUnit(result.unit, result.value, quantityWithoutParens);
|
|
225
|
+
return {
|
|
226
|
+
value: result.value,
|
|
227
|
+
unit: normalizedUnit,
|
|
228
|
+
normalizedQuantity
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
value: 0,
|
|
233
|
+
unit: quantityWithoutParens.trim(),
|
|
234
|
+
normalizedQuantity
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
static preprocess(quantityStr) {
|
|
238
|
+
let normalized = quantityStr.normalize("NFKC").replace(/[ ]/g, "").replace(/[~〜]/g, "~").replace(/ふたつまみ/g, "2\u3072\u3068\u3064\u307E\u307F").replace(/(?:大|中|小)サイズ/g, "").replace(/(\d+)([^0-9/.\s]*)(?:半|半分)$/g, "$1.5$2").replace(/^半分/, "0.5").replace(/([^0-9/.\s~]+)半分$/g, "0.5$1").replace(/(?:ほど|前後|程度|くらい|強|弱|半分)$/g, "");
|
|
239
|
+
normalized = normalized.replace(/cc/gi, "ml").replace(/[0-9.]+(?:mm|cm|ミリ|センチ)(?:幅|厚|角|長さ|切り|みじん|乱切り|いちょう|千切り|輪切り)/gi, "").replace(/(?:斜め|薄切り|幅|角|切り|みじん|乱切り|いちょう|千切り|輪切り|半月|小口|ざく|ぶつ|そぎ|拍子木|さいの目|くし形|ささがき|おろし|すり|つぶし|刻み|の)/g, "");
|
|
240
|
+
return normalized;
|
|
241
|
+
}
|
|
242
|
+
static postprocessUnit(unit, value, originalCleaned) {
|
|
243
|
+
let normalizedUnit = unit;
|
|
244
|
+
if (unit) {
|
|
245
|
+
normalizedUnit = normalizedUnit.replace(/コ|コ/g, "\u500B").replace(/ワ|ワ|把/g, "\u675F").replace(/パイ|パイ/g, "\u676F");
|
|
246
|
+
normalizedUnit = normalizedUnit.trim();
|
|
247
|
+
if (normalizedUnit === "\u3064\u307E\u307F") normalizedUnit = "\u3072\u3068\u3064\u307E\u307F";
|
|
248
|
+
if (normalizedUnit.includes("\u30C6\u30A3\u30FC\u30B9\u30D7\u30FC\u30F3")) normalizedUnit = "\u5C0F\u3055\u3058";
|
|
249
|
+
if (normalizedUnit.length > 1 && normalizedUnit.endsWith("\u5206") && !normalizedUnit.startsWith("\u4EBA")) {
|
|
250
|
+
normalizedUnit = normalizedUnit.slice(0, -1);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (!isNaN(value) && normalizedUnit === "" && value >= 50) {
|
|
254
|
+
normalizedUnit = "g";
|
|
255
|
+
}
|
|
256
|
+
return normalizedUnit;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
QuantityParser.strategies = [
|
|
260
|
+
new TubeStrategy(),
|
|
261
|
+
// 1b
|
|
262
|
+
new GramsInParensStrategy(),
|
|
263
|
+
// 2
|
|
264
|
+
new RangeStrategy(),
|
|
265
|
+
// 3
|
|
266
|
+
new FractionStrategy(),
|
|
267
|
+
// 4, 5, 6, 6b
|
|
268
|
+
new UnitPrefixStrategy(),
|
|
269
|
+
// 7, 9a
|
|
270
|
+
new SuffixStrategy()
|
|
271
|
+
// 8, 9b
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
// src/config.ts
|
|
275
|
+
var UNITS_CONFIG = {
|
|
276
|
+
/**
|
|
277
|
+
* Standard volume mapping in Milliliters (ml).
|
|
278
|
+
* Used for density calculations.
|
|
279
|
+
*/
|
|
280
|
+
STANDARD_VOLUME_ML: {
|
|
281
|
+
"\u30AB\u30C3\u30D7": 200,
|
|
282
|
+
"\u5927\u3055\u3058": 15,
|
|
283
|
+
"\u5C0F\u3055\u3058": 5,
|
|
284
|
+
"ml": 1,
|
|
285
|
+
"cc": 1,
|
|
286
|
+
"\u30DF\u30EA\u30EA\u30C3\u30C8\u30EB": 1,
|
|
287
|
+
"L": 1e3,
|
|
288
|
+
"\u30EA\u30C3\u30C8\u30EB": 1e3,
|
|
289
|
+
"\u5C11\u3005": 0.8,
|
|
290
|
+
"\u3072\u3068\u3064\u307E\u307F": 1,
|
|
291
|
+
"\u5408": 180,
|
|
292
|
+
"\u5C0F": 5,
|
|
293
|
+
"\u5927": 15,
|
|
294
|
+
"\u9069\u91CF": 0,
|
|
295
|
+
"\u9069\u5B9C": 0
|
|
296
|
+
},
|
|
297
|
+
/**
|
|
298
|
+
* Units that represent a "single entity" of a countable food.
|
|
299
|
+
* If a food has any of these in unitWeights, they can be treated as interchangeable
|
|
300
|
+
* fallbacks (1 entity = unitWeight).
|
|
301
|
+
*/
|
|
302
|
+
UNIT_SINGLE_ALIASES: ["\u500B", "\u672C", "\u5339", "\u5C3E", "\u7389", "\u7C92", "\u682A"],
|
|
303
|
+
/**
|
|
304
|
+
* Ambiguous measurement expressions.
|
|
305
|
+
*/
|
|
306
|
+
AMBIGUOUS: ["\u5C11\u3005", "\u9069\u91CF", "\u3072\u3068\u3064\u307E\u307F", "\u9069\u5B9C", "\u3072\u3068\u3064\u304B\u307F"],
|
|
307
|
+
/**
|
|
308
|
+
* Special units with fixed weights or behaviors.
|
|
309
|
+
*/
|
|
310
|
+
SPECIAL_FIXED: ["\u5C11\u3005", "\u3072\u3068\u3064\u307E\u307F"]
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// src/QuantityFormatter.ts
|
|
314
|
+
var QuantityFormatter = class {
|
|
315
|
+
/**
|
|
316
|
+
* Formats a numeric value with its unit for display.
|
|
317
|
+
* @param value - The numeric value
|
|
318
|
+
* @param unit - The unit string
|
|
319
|
+
* @returns Formatted string for display
|
|
320
|
+
*/
|
|
321
|
+
static format(value, unit) {
|
|
322
|
+
if (isNaN(value) || value === void 0 || value === null) {
|
|
323
|
+
return unit?.trim() ? unit : "";
|
|
324
|
+
}
|
|
325
|
+
if (value === 0 && UNITS_CONFIG.AMBIGUOUS.includes(unit)) return unit;
|
|
326
|
+
if (value === 0) return unit && !UNITS_CONFIG.AMBIGUOUS.includes(unit) ? `0 ${unit}` : "";
|
|
327
|
+
if (unit === "\u5927\u3055\u3058" || unit === "\u5C0F\u3055\u3058") {
|
|
328
|
+
return this.formatSpoon(value, unit);
|
|
329
|
+
}
|
|
330
|
+
const formattedValue = parseFloat(value.toFixed(3)).toString();
|
|
331
|
+
if (unit.startsWith("\u5927\u3055\u3058") || unit.startsWith("\u5C0F\u3055\u3058")) {
|
|
332
|
+
return unit ? `${formattedValue} ${unit}` : formattedValue;
|
|
333
|
+
}
|
|
334
|
+
const sizePrefixMatch = unit.match(/^(大|中|小|特大|極小|厚切り|薄切り)(.*)$/);
|
|
335
|
+
if (sizePrefixMatch) {
|
|
336
|
+
return `${sizePrefixMatch[1]}${formattedValue}${sizePrefixMatch[2]}`;
|
|
337
|
+
}
|
|
338
|
+
if (unit === "\u30C1\u30E5\u30FC\u30D6cm") {
|
|
339
|
+
return `\u30C1\u30E5\u30FC\u30D6${formattedValue}cm`;
|
|
340
|
+
}
|
|
341
|
+
return unit ? `${formattedValue} ${unit}` : formattedValue;
|
|
342
|
+
}
|
|
343
|
+
static formatSpoon(value, unit) {
|
|
344
|
+
const tolerance = 0.01;
|
|
345
|
+
const integerPart = Math.floor(value);
|
|
346
|
+
const fractionalPart = value - integerPart;
|
|
347
|
+
let fractionStr = "";
|
|
348
|
+
if (Math.abs(fractionalPart - 0.5) < tolerance) fractionStr = "1/2";
|
|
349
|
+
else if (Math.abs(fractionalPart - 0.25) < tolerance) fractionStr = "1/4";
|
|
350
|
+
else if (Math.abs(fractionalPart - 0.75) < tolerance) fractionStr = "3/4";
|
|
351
|
+
else if (Math.abs(fractionalPart - 1 / 3) < tolerance) fractionStr = "1/3";
|
|
352
|
+
else if (Math.abs(fractionalPart - 2 / 3) < tolerance) fractionStr = "2/3";
|
|
353
|
+
else if (fractionalPart > tolerance) {
|
|
354
|
+
fractionStr = parseFloat(fractionalPart.toFixed(2)).toString().substring(1);
|
|
355
|
+
if (fractionStr === ".0") fractionStr = "";
|
|
356
|
+
}
|
|
357
|
+
let result = unit;
|
|
358
|
+
if (integerPart > 0) {
|
|
359
|
+
result += ` ${integerPart}`;
|
|
360
|
+
if (fractionStr && !fractionStr.startsWith(".")) result += `\u3068`;
|
|
361
|
+
result += fractionStr;
|
|
362
|
+
} else if (fractionStr) {
|
|
363
|
+
result += ` ${fractionStr}`;
|
|
364
|
+
} else {
|
|
365
|
+
result += ` ${parseFloat(value.toFixed(2)).toString()}`;
|
|
366
|
+
}
|
|
367
|
+
return result.replace(/\s+/g, " ").trim();
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
export {
|
|
371
|
+
FractionStrategy,
|
|
372
|
+
GramsInParensStrategy,
|
|
373
|
+
QuantityFormatter,
|
|
374
|
+
QuantityParser,
|
|
375
|
+
RangeStrategy,
|
|
376
|
+
SuffixStrategy,
|
|
377
|
+
TubeStrategy,
|
|
378
|
+
UNITS_CONFIG,
|
|
379
|
+
UnitPrefixStrategy
|
|
380
|
+
};
|
|
381
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/strategies/GramsInParensStrategy.ts","../src/strategies/RangeStrategy.ts","../src/strategies/FractionStrategy.ts","../src/strategies/UnitPrefixStrategy.ts","../src/strategies/SuffixStrategy.ts","../src/strategies/TubeStrategy.ts","../src/QuantityParser.ts","../src/config.ts","../src/QuantityFormatter.ts"],"sourcesContent":["import type { ParseStrategy, ParsedResult, ParseOptions } from '../types';\r\n\r\n/**\r\n * Parses grams or milliliters in parentheses like \"(100g)\" or \"(200ml)\".\r\n */\r\nexport class GramsInParensStrategy implements ParseStrategy {\r\n parse(quantity: string, options?: ParseOptions): ParsedResult | null {\r\n const gramInParenMatch = quantity.match(/[((](?:[^)\\d]*)([\\d.]+)\\s*(?:g|グラム|cc|ml)[))]/i);\r\n if (gramInParenMatch && gramInParenMatch[1]) {\r\n const val = parseFloat(gramInParenMatch[1]);\r\n if (!isNaN(val)) {\r\n const unitMatch = gramInParenMatch[0].match(/(g|グラム|cc|ml)/i);\r\n let unitInParen = unitMatch ? unitMatch[1].toLowerCase() : 'g';\r\n if (unitInParen === 'グラム') unitInParen = 'g';\r\n else if (unitInParen === 'cc') unitInParen = 'ml';\r\n\r\n return { value: val, unit: unitInParen };\r\n }\r\n }\r\n return null;\r\n }\r\n}\r\n","import type { ParseStrategy, ParsedResult, ParseOptions } from '../types';\r\n\r\n/**\r\n * Parses range expressions like \"1~2個\".\r\n * Behavior depends on options.rangeMode (default: min).\r\n */\r\nexport class RangeStrategy implements ParseStrategy {\r\n parse(quantity: string, options?: ParseOptions): ParsedResult | null {\r\n // e.g. \"1~2個\" -> value=1, unit=\"個\" (takes minimum)\r\n const rangeMatch = quantity.match(/^([^0-9.\\/~]*)([0-9.]+)[~]([0-9.]+)(.*)$/);\r\n if (rangeMatch) {\r\n const min = parseFloat(rangeMatch[2]);\r\n const max = parseFloat(rangeMatch[3]);\r\n if (!isNaN(min) && !isNaN(max)) {\r\n let value = min;\r\n const mode = options?.rangeMode || 'min';\r\n\r\n if (mode === 'max') {\r\n value = max;\r\n } else if (mode === 'mean') {\r\n value = (min + max) / 2;\r\n }\r\n\r\n // Default is min\r\n return {\r\n value: value,\r\n unit: (rangeMatch[1] || '') + (rangeMatch[4] || '')\r\n };\r\n }\r\n }\r\n return null;\r\n }\r\n}\r\n","import type { ParseStrategy, ParsedResult, ParseOptions } from '../types';\r\n\r\n/**\r\n * Parses various fraction formats:\r\n * - Mixed fractions: \"1と1/2\", \"1・1/2\"\r\n * - Half: \"1/2\", \"半\"\r\n * - Simple fractions: \"1/4\"\r\n * - Japanese fractions: \"8分の1\"\r\n */\r\nexport class FractionStrategy implements ParseStrategy {\r\n parse(quantity: string, options?: ParseOptions): ParsedResult | null {\r\n // 1. Mixed Fraction (e.g. 1と1/2)\r\n const mixedFractionMatch = quantity.match(/^([^0-9と・.\\/]*?)(\\d+)(?:と|・|・)(\\d+)\\/(\\d+)(.*)$/);\r\n if (mixedFractionMatch) {\r\n const integer = parseInt(mixedFractionMatch[2], 10);\r\n const numerator = parseInt(mixedFractionMatch[3], 10);\r\n const denominator = parseInt(mixedFractionMatch[4], 10);\r\n if (!isNaN(integer) && !isNaN(numerator) && !isNaN(denominator) && denominator !== 0) {\r\n return {\r\n value: integer + numerator / denominator,\r\n unit: (mixedFractionMatch[1] || '') + (mixedFractionMatch[5] || '')\r\n };\r\n }\r\n }\r\n\r\n // 2. Half (1/2 or 半)\r\n const halfMatch = quantity.match(/^(?:1\\/2|半)(.*)$/);\r\n if (halfMatch) {\r\n let u = halfMatch[1] || '';\r\n // \"半分\" means 0.5. If suffix is \"分\" only, it's likely \"半分\" -> value=0.5, unit=\"\"\r\n // But \"半分個\" -> value=0.5, unit=\"分個\"? No, usually \"半分\" is used as standalone or \"半分個\".\r\n // If \"半分\", u=\"分\".\r\n if (u === '分') u = '';\r\n\r\n // Handle range suffix \"1/2~1\" logic\r\n if (u.includes('~')) {\r\n const parts = u.split('~');\r\n if (parts.length > 1) {\r\n const afterTilde = parts[1];\r\n const unitMatch = afterTilde.match(/[\\d.]*(.*)/);\r\n if (unitMatch) {\r\n u = unitMatch[1];\r\n }\r\n }\r\n }\r\n\r\n return { value: 0.5, unit: u };\r\n }\r\n\r\n // 3. Simple Fraction (1/4)\r\n const fractionMatch = quantity.match(/^([^0-9\\/]*?)(\\d+)\\/(\\d+)(.*)$/);\r\n if (fractionMatch) {\r\n const numerator = parseInt(fractionMatch[2], 10);\r\n const denominator = parseInt(fractionMatch[3], 10);\r\n let suffix = fractionMatch[4] || '';\r\n\r\n // Handle range suffix \"1/2~1\" logic\r\n if (suffix.includes('~')) {\r\n // Suffix is like \"~1個\" or \"~1\"\r\n // Split by tilde\r\n const parts = suffix.split('~');\r\n if (parts.length > 1) {\r\n const afterTilde = parts[1];\r\n // Remove leading numbers/dots to get unit\r\n const unitMatch = afterTilde.match(/[\\d.]*(.*)/);\r\n if (unitMatch) {\r\n suffix = unitMatch[1];\r\n }\r\n }\r\n }\r\n\r\n if (!isNaN(numerator) && !isNaN(denominator) && denominator !== 0) {\r\n return {\r\n value: numerator / denominator,\r\n unit: (fractionMatch[1] || '') + suffix\r\n };\r\n }\r\n }\r\n\r\n // 4. Japanese Fraction (e.g. 8分の1)\r\n const japaneseFractionMatch = quantity.match(/^([^0-9分]*?)(\\d+)分(?:の)?(\\d+)(.*)$/);\r\n if (japaneseFractionMatch) {\r\n const prefix = japaneseFractionMatch[1] || '';\r\n const denominator = parseInt(japaneseFractionMatch[2], 10);\r\n const numerator = parseInt(japaneseFractionMatch[3], 10);\r\n const suffix = japaneseFractionMatch[4] || '';\r\n if (!isNaN(numerator) && !isNaN(denominator) && denominator !== 0) {\r\n return {\r\n value: numerator / denominator,\r\n unit: prefix + suffix\r\n };\r\n }\r\n }\r\n\r\n return null;\r\n }\r\n}\r\n","import type { ParseStrategy, ParsedResult, ParseOptions } from '../types';\r\n\r\n/**\r\n * Parses unit prefix patterns like \"大さじ1\", \"小1\", \"大1/2\", \"カップ1\".\r\n */\r\nexport class UnitPrefixStrategy implements ParseStrategy {\r\n parse(quantity: string, options?: ParseOptions): ParsedResult | null {\r\n // e.g. 大さじ1, 小1, 大1/2\r\n const daiShoPattern = /^(.*?)(大|小)(さじ)?((?:\\d+(?:[と・]\\d+\\/\\d+))|(?:\\d+\\/\\d+)|(?:\\d*\\.?\\d+))(.*)$/;\r\n const daiShoMatch = quantity.match(daiShoPattern);\r\n\r\n if (daiShoMatch) {\r\n const sizeChar = daiShoMatch[2]; // \"大\" or \"小\"\r\n const sajiPresent = daiShoMatch[3]; // \"さじ\"\r\n const valueStr = daiShoMatch[4];\r\n const suffix = daiShoMatch[5] || '';\r\n\r\n const potentialUnit = (suffix || '').trim();\r\n const knownUnits = ['個', '缶', '枚', '本', '束', '株', '尾', 'かけ', '片', '丁', '袋', '杯', 'cm', '房', '節', 'パック', '玉', '膳', '人分', 'gr', 'g', 'ml', 'cc'];\r\n const startsWithKnownUnit = knownUnits.some(u => potentialUnit.startsWith(u));\r\n\r\n // Heuristic: If \"saji\" is missing and suffix looks like a known unit (e.g., \"小2個\"),\r\n // treat \"小\" as modifier and fallback to general parser (return null here).\r\n if (!sajiPresent && startsWithKnownUnit) {\r\n return null;\r\n }\r\n\r\n const parsedSajiUnit = sizeChar === '大' ? '大さじ' : '小さじ';\r\n let value = NaN;\r\n\r\n const innerMixed = valueStr.match(/^(\\d+)[と・](\\d+)\\/(\\d+)$/);\r\n const innerFraction = valueStr.match(/^(\\d+)\\/(\\d+)$/);\r\n\r\n if (innerMixed) {\r\n value = parseInt(innerMixed[1], 10) + parseInt(innerMixed[2], 10) / parseInt(innerMixed[3], 10);\r\n } else if (innerFraction) {\r\n value = parseInt(innerFraction[1], 10) / parseInt(innerFraction[2], 10);\r\n } else {\r\n value = parseFloat(valueStr);\r\n }\r\n\r\n if (!isNaN(value)) {\r\n return { value, unit: parsedSajiUnit };\r\n }\r\n }\r\n\r\n // Also handle explicit prefix match (Step 9 part 1 in original)\r\n // e.g. \"大さじ2\", \"カップ1\"\r\n const knownUnitPrefixMatch = quantity.match(/^(大さじ|小さじ|カップ)([0-9.]+)(.*)$/);\r\n if (knownUnitPrefixMatch) {\r\n const value = parseFloat(knownUnitPrefixMatch[2]);\r\n if (!isNaN(value)) {\r\n return {\r\n value,\r\n unit: (knownUnitPrefixMatch[1] + (knownUnitPrefixMatch[3] || '')).trim()\r\n };\r\n }\r\n }\r\n\r\n return null;\r\n }\r\n}\r\n","import type { ParseStrategy, ParsedResult, ParseOptions } from '../types';\r\n\r\n/**\r\n * Fallback strategy for parsing:\r\n * - Explicit grams/ml trailing patterns\r\n * - General number + suffix patterns\r\n */\r\nexport class SuffixStrategy implements ParseStrategy {\r\n parse(quantity: string, options?: ParseOptions): ParsedResult | null {\r\n // 1. Explicit Grams/ML (trailing)\r\n const gramOrMlMatch = quantity.match(/([\\d.]+)\\s*(g|グラム|ml|ミリリットル)/i);\r\n if (gramOrMlMatch) {\r\n const val = parseFloat(gramOrMlMatch[1]);\r\n if (!isNaN(val)) {\r\n let u = gramOrMlMatch[2].toLowerCase();\r\n if (u === 'グラム') u = 'g';\r\n if (u === 'ミリリットル') u = 'ml';\r\n return { value: val, unit: u };\r\n }\r\n }\r\n\r\n // 2. General Number Match\r\n const numberMatch = quantity.match(/^([^0-9.]*)([0-9.]+)(.*)$/);\r\n if (numberMatch) {\r\n const p = numberMatch[1] || '';\r\n const n = parseFloat(numberMatch[2]);\r\n const s = numberMatch[3] || '';\r\n if (!isNaN(n)) {\r\n const unit = (p + s).trim();\r\n return { value: n, unit };\r\n }\r\n }\r\n\r\n return null;\r\n }\r\n}\r\n","import type { ParseStrategy, ParsedResult, ParseOptions } from '../types';\r\n\r\n/**\r\n * Parses tube measurements like \"チューブ3cm\" or \"チューブで2センチ\".\r\n */\r\nexport class TubeStrategy implements ParseStrategy {\r\n parse(quantity: string, options?: ParseOptions): ParsedResult | null {\r\n // \"チューブXcm\" -> value=X, unit=\"チューブcm\"\r\n const tubeMatch = quantity.match(/チューブ(?:で)?([\\d.]+)(?:cm|センチ)/i);\r\n if (tubeMatch && tubeMatch[1]) {\r\n const val = parseFloat(tubeMatch[1]);\r\n if (!isNaN(val)) {\r\n return { value: val, unit: 'チューブcm' };\r\n }\r\n }\r\n return null;\r\n }\r\n}\r\n","import { GramsInParensStrategy } from './strategies/GramsInParensStrategy';\r\nimport { RangeStrategy } from './strategies/RangeStrategy';\r\nimport { FractionStrategy } from './strategies/FractionStrategy';\r\nimport { UnitPrefixStrategy } from './strategies/UnitPrefixStrategy';\r\nimport { SuffixStrategy } from './strategies/SuffixStrategy';\r\nimport { TubeStrategy } from './strategies/TubeStrategy';\r\nimport type { ParsedResult, ParseStrategy, ParsedQuantityResult } from './types';\r\n\r\n/**\r\n * Parses Japanese quantity strings into numeric values and normalized units.\r\n *\r\n * @example\r\n * ```typescript\r\n * QuantityParser.parse('大さじ1'); // { value: 1, unit: '大さじ' }\r\n * QuantityParser.parse('1/2カップ'); // { value: 0.5, unit: 'カップ' }\r\n * QuantityParser.parse('100g'); // { value: 100, unit: 'g' }\r\n * ```\r\n */\r\nexport class QuantityParser {\r\n private static strategies: ParseStrategy[] = [\r\n new TubeStrategy(), // 1b\r\n new GramsInParensStrategy(), // 2\r\n new RangeStrategy(), // 3\r\n new FractionStrategy(), // 4, 5, 6, 6b\r\n new UnitPrefixStrategy(), // 7, 9a\r\n new SuffixStrategy() // 8, 9b\r\n ];\r\n\r\n /**\r\n * Parses a quantity string into a numeric value and a normalized unit.\r\n * @param quantityStr - The quantity string to parse (e.g., \"大さじ1\", \"100g\", \"1/2カップ\")\r\n * @param options - Parsing options (e.g. rangeMode)\r\n * @returns Parsed result with value, unit, and normalized quantity string\r\n */\r\n static parse(quantityStr: string, options?: import('./types').ParseOptions): ParsedQuantityResult {\r\n if (!quantityStr) {\r\n return { value: 0, unit: '' };\r\n }\r\n\r\n const normalizedQuantity = this.preprocess(quantityStr);\r\n let result: ParsedResult | null = null;\r\n\r\n // Remove parens for most strategies (except GramsInParens which uses raw normalized string)\r\n const quantityWithoutParens = normalizedQuantity.replace(/[((][^))]*[))]/g, '');\r\n\r\n for (const strategy of this.strategies) {\r\n // Some strategies need the full string (Tube, GramsInParens), others need stripped\r\n // TubeStrategy uses explicit match on \"チューブ...\", so passed normalizedQuantity is fine\r\n // GramsInParens needs parens, so passed normalizedQuantity\r\n // Range/Fraction/Prefix/Suffix operate on the \"value\" part, usually without parens instructions.\r\n // However, the original code computed `quantityWithoutParens` at step 2.\r\n // Tube (1b) used normalizedQuantity.\r\n // GramsInParens (2) used normalizedQuantity.\r\n // Others used quantityWithoutParens.\r\n\r\n let input = quantityWithoutParens;\r\n if (strategy instanceof TubeStrategy || strategy instanceof GramsInParensStrategy) {\r\n input = normalizedQuantity;\r\n }\r\n\r\n result = strategy.parse(input, options);\r\n if (result) break;\r\n }\r\n\r\n if (result) {\r\n const normalizedUnit = this.postprocessUnit(result.unit, result.value, quantityWithoutParens);\r\n return {\r\n value: result.value,\r\n unit: normalizedUnit,\r\n normalizedQuantity\r\n };\r\n }\r\n\r\n // Fallback (Step 12)\r\n return {\r\n value: 0,\r\n unit: quantityWithoutParens.trim(),\r\n normalizedQuantity\r\n };\r\n }\r\n\r\n private static preprocess(quantityStr: string): string {\r\n let normalized = quantityStr\r\n .normalize('NFKC')\r\n .replace(/[ ]/g, '')\r\n .replace(/[~〜]/g, '~')\r\n .replace(/ふたつまみ/g, '2ひとつまみ')\r\n .replace(/(?:大|中|小)サイズ/g, '')\r\n .replace(/(\\d+)([^0-9/.\\s]*)(?:半|半分)$/g, '$1.5$2')\r\n .replace(/^半分/, '0.5')\r\n .replace(/([^0-9/.\\s~]+)半分$/g, '0.5$1')\r\n .replace(/(?:ほど|前後|程度|くらい|強|弱|半分)$/g, '');\r\n\r\n normalized = normalized\r\n .replace(/cc/gi, 'ml')\r\n .replace(/[0-9.]+(?:mm|cm|ミリ|センチ)(?:幅|厚|角|長さ|切り|みじん|乱切り|いちょう|千切り|輪切り)/gi, '')\r\n .replace(/(?:斜め|薄切り|幅|角|切り|みじん|乱切り|いちょう|千切り|輪切り|半月|小口|ざく|ぶつ|そぎ|拍子木|さいの目|くし形|ささがき|おろし|すり|つぶし|刻み|の)/g, '');\r\n\r\n return normalized;\r\n }\r\n\r\n private static postprocessUnit(unit: string, value: number, originalCleaned: string): string {\r\n let normalizedUnit = unit;\r\n if (unit) {\r\n normalizedUnit = normalizedUnit\r\n .replace(/コ|コ/g, '個')\r\n .replace(/ワ|ワ|把/g, '束')\r\n .replace(/パイ|パイ/g, '杯');\r\n\r\n normalizedUnit = normalizedUnit.trim();\r\n\r\n if (normalizedUnit === 'つまみ') normalizedUnit = 'ひとつまみ';\r\n if (normalizedUnit.includes('ティースプーン')) normalizedUnit = '小さじ';\r\n\r\n if (normalizedUnit.length > 1 && normalizedUnit.endsWith('分') && !normalizedUnit.startsWith('人')) {\r\n normalizedUnit = normalizedUnit.slice(0, -1);\r\n }\r\n }\r\n\r\n // Step 11: Infer 'g'\r\n if (!isNaN(value) && normalizedUnit === '' && value >= 50) {\r\n normalizedUnit = 'g';\r\n }\r\n\r\n return normalizedUnit;\r\n }\r\n}\r\n","/**\r\n * Unit definitions and measurement mappings for quantity parsing.\r\n */\r\nexport const UNITS_CONFIG = {\r\n /**\r\n * Standard volume mapping in Milliliters (ml).\r\n * Used for density calculations.\r\n */\r\n STANDARD_VOLUME_ML: {\r\n \"カップ\": 200,\r\n \"大さじ\": 15,\r\n \"小さじ\": 5,\r\n \"ml\": 1,\r\n \"cc\": 1,\r\n \"ミリリットル\": 1,\r\n \"L\": 1000,\r\n \"リットル\": 1000,\r\n \"少々\": 0.8,\r\n \"ひとつまみ\": 1,\r\n \"合\": 180,\r\n \"小\": 5,\r\n \"大\": 15,\r\n \"適量\": 0,\r\n \"適宜\": 0,\r\n } as const,\r\n\r\n /**\r\n * Units that represent a \"single entity\" of a countable food.\r\n * If a food has any of these in unitWeights, they can be treated as interchangeable\r\n * fallbacks (1 entity = unitWeight).\r\n */\r\n UNIT_SINGLE_ALIASES: ['個', '本', '匹', '尾', '玉', '粒', '株'] as const,\r\n\r\n /**\r\n * Ambiguous measurement expressions.\r\n */\r\n AMBIGUOUS: ['少々', '適量', 'ひとつまみ', '適宜', 'ひとつかみ'] as const,\r\n\r\n /**\r\n * Special units with fixed weights or behaviors.\r\n */\r\n SPECIAL_FIXED: ['少々', 'ひとつまみ'] as const,\r\n} as const;\r\n\r\nexport type StandardVolumeUnit = keyof typeof UNITS_CONFIG.STANDARD_VOLUME_ML;\r\nexport type UnitSingleAlias = (typeof UNITS_CONFIG.UNIT_SINGLE_ALIASES)[number];\r\nexport type AmbiguousUnit = (typeof UNITS_CONFIG.AMBIGUOUS)[number];\r\nexport type SpecialFixedUnit = (typeof UNITS_CONFIG.SPECIAL_FIXED)[number];\r\n","import { UNITS_CONFIG } from './config';\r\n\r\n/**\r\n * Formats quantity values with units for display.\r\n *\r\n * @example\r\n * ```typescript\r\n * QuantityFormatter.format(1.5, '大さじ'); // '大さじ 1と1/2'\r\n * QuantityFormatter.format(100, 'g'); // '100 g'\r\n * QuantityFormatter.format(3, 'チューブcm'); // 'チューブ3cm'\r\n * ```\r\n */\r\nexport class QuantityFormatter {\r\n /**\r\n * Formats a numeric value with its unit for display.\r\n * @param value - The numeric value\r\n * @param unit - The unit string\r\n * @returns Formatted string for display\r\n */\r\n static format(value: number, unit: string): string {\r\n if (isNaN(value) || value === undefined || value === null) {\r\n return unit?.trim() ? unit : '';\r\n }\r\n\r\n if (value === 0 && UNITS_CONFIG.AMBIGUOUS.includes(unit as any)) return unit;\r\n if (value === 0) return unit && !UNITS_CONFIG.AMBIGUOUS.includes(unit as any) ? `0 ${unit}` : \"\";\r\n\r\n if (unit === '大さじ' || unit === '小さじ') {\r\n return this.formatSpoon(value, unit);\r\n }\r\n\r\n const formattedValue = parseFloat(value.toFixed(3)).toString();\r\n\r\n if (unit.startsWith('大さじ') || unit.startsWith('小さじ')) {\r\n return unit ? `${formattedValue} ${unit}` : formattedValue;\r\n }\r\n\r\n const sizePrefixMatch = unit.match(/^(大|中|小|特大|極小|厚切り|薄切り)(.*)$/);\r\n if (sizePrefixMatch) {\r\n return `${sizePrefixMatch[1]}${formattedValue}${sizePrefixMatch[2]}`;\r\n }\r\n\r\n if (unit === 'チューブcm') {\r\n return `チューブ${formattedValue}cm`;\r\n }\r\n\r\n return unit ? `${formattedValue} ${unit}` : formattedValue;\r\n }\r\n\r\n private static formatSpoon(value: number, unit: string): string {\r\n const tolerance = 0.01;\r\n const integerPart = Math.floor(value);\r\n const fractionalPart = value - integerPart;\r\n let fractionStr = '';\r\n\r\n if (Math.abs(fractionalPart - 0.5) < tolerance) fractionStr = '1/2';\r\n else if (Math.abs(fractionalPart - 0.25) < tolerance) fractionStr = '1/4';\r\n else if (Math.abs(fractionalPart - 0.75) < tolerance) fractionStr = '3/4';\r\n else if (Math.abs(fractionalPart - 1 / 3) < tolerance) fractionStr = '1/3';\r\n else if (Math.abs(fractionalPart - 2 / 3) < tolerance) fractionStr = '2/3';\r\n else if (fractionalPart > tolerance) {\r\n fractionStr = parseFloat(fractionalPart.toFixed(2)).toString().substring(1);\r\n if (fractionStr === '.0') fractionStr = '';\r\n }\r\n\r\n let result = unit;\r\n if (integerPart > 0) {\r\n result += ` ${integerPart}`;\r\n if (fractionStr && !fractionStr.startsWith('.')) result += `と`;\r\n result += fractionStr;\r\n } else if (fractionStr) {\r\n result += ` ${fractionStr}`;\r\n } else {\r\n result += ` ${parseFloat(value.toFixed(2)).toString()}`;\r\n }\r\n return result.replace(/\\s+/g, ' ').trim();\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@recipe-scope/quantity-parser",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Zero-dependency Japanese quantity string parser for recipe ingredients",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"recipe",
|
|
7
|
+
"parser",
|
|
8
|
+
"quantity",
|
|
9
|
+
"japanese",
|
|
10
|
+
"ingredient",
|
|
11
|
+
"cooking"
|
|
12
|
+
],
|
|
13
|
+
"author": "recipe-scope",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"module": "./dist/index.mjs",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"import": {
|
|
24
|
+
"types": "./dist/index.d.mts",
|
|
25
|
+
"default": "./dist/index.mjs"
|
|
26
|
+
},
|
|
27
|
+
"require": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:watch": "vitest",
|
|
40
|
+
"prepublishOnly": "npm run build"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"tsup": "^8.0.0",
|
|
44
|
+
"typescript": "^5.0.0",
|
|
45
|
+
"vitest": "^1.0.0"
|
|
46
|
+
}
|
|
47
|
+
}
|