feat: 智能识别支持无数字精油名(默认1滴)、分号/空行分隔多配方
Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Failing after 5s
Test / e2e-test (push) Has been skipped
Test / build-check (push) Successful in 3s
PR Preview / test (pull_request) Failing after 5s
PR Preview / deploy-preview (pull_request) Has been skipped
Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Failing after 5s
Test / e2e-test (push) Has been skipped
Test / build-check (push) Successful in 3s
PR Preview / test (pull_request) Failing after 5s
PR Preview / deploy-preview (pull_request) Has been skipped
- 精油名后不写数字自动识别为1滴 - 分号分隔多配方(两边都有精油时) - 空行分隔多配方 - 混合格式支持(部分有数字部分无数字) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -144,12 +144,16 @@ export function greedyMatchOils(text, oilNames) {
|
||||
/**
|
||||
* Parse text chunk into [{oil, drops}] pairs.
|
||||
* Handles formats like "芳香调理8永久花10" or "薰衣草 3滴 茶树 2ml"
|
||||
* Also handles oil names without numbers, defaulting to 1 drop.
|
||||
*/
|
||||
export function parseOilChunk(text, oilNames) {
|
||||
const results = []
|
||||
// Match: name + optional number+unit
|
||||
const regex = /([^\d]+?)(\d+\.?\d*)\s*(ml|毫升|ML|mL|滴)?/g
|
||||
let match
|
||||
let lastIndex = 0
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
lastIndex = regex.lastIndex
|
||||
const namePart = match[1].trim()
|
||||
let amount = parseFloat(match[2])
|
||||
const unit = match[3] || ''
|
||||
@@ -164,7 +168,7 @@ export function parseOilChunk(text, oilNames) {
|
||||
if (matched.length > 0) {
|
||||
// Last matched oil gets the drops
|
||||
for (let i = 0; i < matched.length - 1; i++) {
|
||||
results.push({ oil: matched[i], drops: 0 })
|
||||
results.push({ oil: matched[i], drops: 1 })
|
||||
}
|
||||
results.push({ oil: matched[matched.length - 1], drops: amount })
|
||||
} else {
|
||||
@@ -177,9 +181,41 @@ export function parseOilChunk(text, oilNames) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lastIndex === 0) {
|
||||
// Regex matched nothing — try the whole text as oil names without numbers
|
||||
_parseNamesOnly(text.trim(), oilNames, results)
|
||||
} else {
|
||||
// Handle trailing text after last number match
|
||||
const trailing = text.substring(lastIndex).trim()
|
||||
if (trailing) {
|
||||
_parseNamesOnly(trailing, oilNames, results)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/** Parse text that contains only oil names (no numbers), default 1 drop each. */
|
||||
function _parseNamesOnly(text, oilNames, results) {
|
||||
// Try greedy match first
|
||||
const matched = greedyMatchOils(text, oilNames)
|
||||
if (matched.length > 0) {
|
||||
for (const oil of matched) {
|
||||
results.push({ oil, drops: 1 })
|
||||
}
|
||||
return
|
||||
}
|
||||
// Fallback: try splitting by common delimiters and fuzzy match
|
||||
const parts = text.split(/[\s+、,,]+/).filter(s => s)
|
||||
for (const part of parts) {
|
||||
const found = findOil(part, oilNames)
|
||||
if (found) {
|
||||
results.push({ oil: found, drops: 1 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Split multi-recipe input by blank lines or semicolons.
|
||||
* Detects recipe boundaries (non-oil text after seeing oils = new recipe).
|
||||
@@ -266,8 +302,23 @@ export function parseSingleBlock(raw, oilNames) {
|
||||
* appears after some oils have been found, it starts a new recipe.
|
||||
*/
|
||||
export function parseMultiRecipes(raw, oilNames) {
|
||||
// Split by blank lines into major blocks
|
||||
const blankLineSplit = raw.split(/\n\s*\n/).map(s => s.trim()).filter(s => s)
|
||||
if (blankLineSplit.length > 1) {
|
||||
return blankLineSplit.flatMap(block => parseMultiRecipes(block, oilNames))
|
||||
}
|
||||
// Split by semicolons only if both sides contain oil names
|
||||
const semiParts = raw.split(/[;;]/).map(s => s.trim()).filter(s => s)
|
||||
if (semiParts.length > 1) {
|
||||
const hasOilInPart = p => oilNames.some(oil => p.includes(oil)) ||
|
||||
Object.keys(OIL_HOMOPHONES).some(a => p.includes(a))
|
||||
if (semiParts.every(hasOilInPart)) {
|
||||
return semiParts.flatMap(block => parseMultiRecipes(block, oilNames))
|
||||
}
|
||||
}
|
||||
|
||||
// First split by lines/commas, then within each part also try space splitting
|
||||
const roughParts = raw.split(/[,,、\n\r]+/).map(s => s.trim()).filter(s => s)
|
||||
const roughParts = raw.split(/[,,、;;\n\r]+/).map(s => s.trim()).filter(s => s)
|
||||
const parts = []
|
||||
for (const rp of roughParts) {
|
||||
// If the part has spaces and contains mixed name+oil, split by spaces too
|
||||
|
||||
Reference in New Issue
Block a user