(function installLTJTTeamBatchHelpers(root, factory) { const api = factory(); if (typeof module !== 'undefined' && module.exports) module.exports = api; if (root) root.LTJTTeamBatchHelpers = api; }(typeof self !== 'undefined' ? self : globalThis, () => { function normalizeText(value) { return String(value ?? '') .normalize('NFKC') .toLowerCase() .replace(/[\s\p{P}\p{S}]+/gu, '') .trim(); } function chineseOrLatinSegments(value) { const normalized = String(value ?? '').normalize('NFKC').toLowerCase(); return normalized.match(/[\p{Script=Han}]+|[a-z0-9]+/giu) || []; } function tokenizeKeyword(value) { const tokens = []; for (const segment of chineseOrLatinSegments(value)) { if (/^[\p{Script=Han}]+$/u.test(segment)) { if (segment.length <= 2) { tokens.push(segment); continue; } for (let index = 0; index < segment.length; index += 2) { tokens.push(segment.slice(index, index + 2)); } } else if (segment) { tokens.push(segment); } } return [...new Set(tokens.map(normalizeText).filter(Boolean))]; } function rowColumns(row) { return String(row ?? '').split('◆').map((value) => value.trim()); } function parseSelectBoxRows(serialized) { return String(serialized ?? '') .split(String(serialized ?? '').includes('◇') ? '◇' : /\r?\n/) .map((raw, rowIndex) => ({ raw: raw.trim(), rowIndex, columns: rowColumns(raw) })) .filter((row) => row.raw); } function searchableText(candidate, columns = []) { if (Array.isArray(candidate)) return candidate.join(' '); if (candidate && Array.isArray(candidate.columns)) { return columns.length ? columns.map((index) => candidate.columns[index] || '').join(' ') : candidate.columns.join(' '); } return String(candidate ?? ''); } function classifyCandidate(candidate, keyword, columns = []) { const haystack = normalizeText(searchableText(candidate, columns)); const needle = normalizeText(keyword); const tokens = tokenizeKeyword(keyword); const exact = Array.isArray(candidate?.columns) && candidate.columns.some((value) => normalizeText(value) === needle && needle); const contiguous = Boolean(needle && haystack.includes(needle)); const tokenMatch = Boolean(tokens.length && tokens.every((token) => haystack.includes(token))); return { candidate, exact, contiguous, tokenMatch, score: exact ? 300 : (contiguous ? 200 : (tokenMatch ? 100 : 0)) }; } function resolveUniqueSelectBoxRow(serialized, keyword, options = {}) { const rows = Array.isArray(serialized) ? serialized : parseSelectBoxRows(serialized); const columns = Array.isArray(options.searchableColumns) ? options.searchableColumns : []; const classified = rows.map((row) => classifyCandidate(row, keyword, columns)); const choose = (rule, list) => { if (list.length !== 1) return null; return { ok: true, row: list[0].candidate, rule, exact_count: classified.filter((item) => item.exact).length, contains_count: classified.filter((item) => item.contiguous).length, token_match_count: classified.filter((item) => item.tokenMatch).length, candidate_count: list.length }; }; const exact = classified.filter((item) => item.exact); const contiguous = classified.filter((item) => item.contiguous); const token = classified.filter((item) => item.tokenMatch); const match = choose('exact', exact) || choose('unique_contains', contiguous) || choose('unique_tokenized_contains', token); if (match) return match; const candidateCount = exact.length > 1 ? exact.length : (contiguous.length > 1 ? contiguous.length : token.length); return { ok: false, row: null, rule: exact.length > 1 ? 'ambiguous_exact' : (contiguous.length > 1 ? 'ambiguous_contains' : (token.length > 1 ? 'ambiguous_tokenized_contains' : 'not_found')), exact_count: exact.length, contains_count: contiguous.length, token_match_count: token.length, candidate_count: candidateCount, blocker: `${options.label || 'lookup'}: expected exactly one deterministic match, found ${candidateCount}` }; } function resolveUniqueTextCandidate(candidates, keyword, options = {}) { const source = Array.isArray(candidates) ? candidates : []; const wrapped = source.map((candidate) => ({ ...classifyCandidate({ columns: [candidate.text || candidate.label || ''] }, keyword, [0]), candidate })); const exact = wrapped.filter((item) => normalizeText(item.candidate.label || item.candidate.text || '') === normalizeText(keyword)); const contiguous = wrapped.filter((item) => item.contiguous); const token = wrapped.filter((item) => item.tokenMatch); const selected = exact.length === 1 ? { list: exact, rule: 'exact' } : (contiguous.length === 1 ? { list: contiguous, rule: 'unique_contains' } : (token.length === 1 ? { list: token, rule: 'unique_tokenized_contains' } : null)); if (selected) { return { ok: true, candidate: selected.list[0].candidate, rule: selected.rule, exact_count: exact.length, contains_count: contiguous.length, token_match_count: token.length, candidate_count: selected.list.length }; } const count = exact.length > 1 ? exact.length : (contiguous.length > 1 ? contiguous.length : token.length); return { ok: false, candidate: null, rule: exact.length > 1 ? 'ambiguous_exact' : (contiguous.length > 1 ? 'ambiguous_contains' : (token.length > 1 ? 'ambiguous_tokenized_contains' : 'not_found')), exact_count: exact.length, contains_count: contiguous.length, token_match_count: token.length, candidate_count: count, blocker: `${options.label || 'lookup'}: expected exactly one deterministic match, found ${count}` }; } function canonicalDate(value) { const match = String(value ?? '').trim().match(/^(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})$/); if (!match) return ''; const year = Number(match[1]); const month = Number(match[2]); const day = Number(match[3]); const date = new Date(Date.UTC(year, month - 1, day)); if (date.getUTCFullYear() !== year || date.getUTCMonth() + 1 !== month || date.getUTCDate() !== day) return ''; return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; } function erpDate(value) { const canonical = canonicalDate(value); if (!canonical) return ''; const [year, month, day] = canonical.split('-'); return `${year}-${Number(month)}-${Number(day)}`; } function dateNumber(value) { const canonical = canonicalDate(value); if (!canonical) return NaN; return Date.parse(`${canonical}T00:00:00Z`); } function dateInRange(value, start, end) { const current = dateNumber(value); const from = dateNumber(start); const to = dateNumber(end); return Number.isFinite(current) && Number.isFinite(from) && Number.isFinite(to) && current >= from && current <= to; } function cycleShortcutValues(pattern, weekdays = []) { const value = String(pattern || '').trim().toLowerCase(); if (value === 'daily' || value === 'all' || value === '天天') return ['All']; if (value === 'odd_days' || value === 'odd' || value === '单日') return ['d1d', 'd3d', 'd5d', 'd7d', 'd9d']; if (value === 'even_days' || value === 'even' || value === '双日') return ['d2d', 'd4d', 'd6d', 'd8d', 'd0d']; if (value === 'weekly' || value === 'weekdays' || value === '星期') { return [...new Set((Array.isArray(weekdays) ? weekdays : [weekdays]) .map((day) => Number(day)) .filter((day) => day >= 1 && day <= 7) .map((day) => String(day === 7 ? 0 : day)))]; } return []; } function uniqueCanonicalDates(values = []) { return [...new Set((Array.isArray(values) ? values : []) .map(canonicalDate) .filter(Boolean))]; } function addCanonicalDays(value, days) { const canonical = canonicalDate(value); if (!canonical) return ''; const [year, month, day] = canonical.split('-').map(Number); const date = new Date(Date.UTC(year, month - 1, day + Number(days || 0))); return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}-${String(date.getUTCDate()).padStart(2, '0')}`; } function weekdayOneToSeven(value) { const canonical = canonicalDate(value); if (!canonical) return 0; const day = new Date(`${canonical}T00:00:00Z`).getUTCDay(); return day === 0 ? 7 : day; } function expandCycleDates(data = {}) { const explicitDates = uniqueCanonicalDates(data.departure_dates); const recurrence = data.recurrence || {}; const pattern = String(recurrence.pattern || data.cycle || '').trim().toLowerCase(); if (explicitDates.length) return explicitDates; const start = canonicalDate(recurrence.start_date); const end = canonicalDate(recurrence.end_date); if (!start || !end || dateNumber(start) > dateNumber(end)) return []; const weekdays = new Set((Array.isArray(recurrence.weekday) ? recurrence.weekday : []) .map((value) => Number(value)) .filter((value) => value >= 1 && value <= 7)); const selected = []; for (let cursor = start; dateNumber(cursor) <= dateNumber(end); cursor = addCanonicalDays(cursor, 1)) { const [, , calendarDay] = cursor.split('-').map(Number); const weekday = weekdayOneToSeven(cursor); const keep = pattern === 'daily' ? true : pattern === 'weekly' ? weekdays.has(weekday) : pattern === 'odd_days' ? calendarDay % 2 === 1 : pattern === 'even_days' ? calendarDay % 2 === 0 : false; if (keep) selected.push(cursor); } return selected; } function cycleSelectionPlan(data = {}) { const explicitDates = uniqueCanonicalDates(data.departure_dates); if (explicitDates.length) { return { ok: true, mode: 'explicit_dates', explicit_dates: explicitDates, expanded_dates: explicitDates, shortcut_values: [] }; } const recurrence = data.recurrence || {}; const shortcutValues = cycleShortcutValues(recurrence.pattern || data.cycle, recurrence.weekday || []); if (!shortcutValues.length) { return { ok: false, mode: 'invalid', explicit_dates: [], expanded_dates: [], shortcut_values: [], blocker: '发团周期未能映射到 ERP 的日期枚举或快捷标签。' }; } return { ok: true, mode: 'shortcut', explicit_dates: [], expanded_dates: expandCycleDates(data), shortcut_values: shortcutValues }; } return { cycleSelectionPlan, canonicalDate, chineseOrLatinSegments, classifyCandidate, cycleShortcutValues, dateInRange, dateNumber, erpDate, expandCycleDates, normalizeText, parseSelectBoxRows, resolveUniqueSelectBoxRow, resolveUniqueTextCandidate, tokenizeKeyword }; }));