GuDingZiChan/backend/scripts/hardwareQuantitySplit.js
2026-03-24 14:21:09 +08:00

535 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const normalizeText = (value) =>
String(value == null ? '' : value)
.replace(/\r/g, ' ')
.replace(/\n/g, ' ')
.replace(/\s+/g, ' ')
.trim()
const cleanModelText = (value) =>
normalizeText(value)
.replace(/\s+([,,/])/g, '$1')
.replace(/([((])\s+/g, '$1')
.replace(/\s+([))])/g, '$1')
.replace(/\s+/g, ' ')
.trim()
const MULTI_UNIT_HINT_PATTERNS = [
{ pattern: 'cpu_x_multiplier', regex: /[((]\s*[xX]\s*[2-9]\d*\s*[))]\s*$/ },
{ pattern: 'suffix_quantity_label', regex: /\*\s*[2-9]\d*\s*(个|条|根|块|张|颗)\s*$/ },
{ pattern: 'bracket_capacity_multiplier', regex: /[((][^()()]*\d+\s*[A-Za-z]{0,3}\s*\*\s*[2-9]\d*[^()()]*[))]/i },
{ pattern: 'inline_capacity_multiplier', regex: /(?:^|\s)\d+\s*[A-Za-z]{0,3}\s*\*\s*[2-9]\d*(?:\s|$)/i },
{ pattern: 'suffix_multiplier_with_paren_unit', regex: /\*\s*[2-9]\d*\s*[((]\s*\d+\s*[A-Za-z]{1,3}\s*[))]\s*$/i },
{ pattern: 'plus_capacity_pair', regex: /\d+(?:\.\d+)?\s*[A-Za-z]{0,3}\s*\+\s*\d+(?:\.\d+)?\s*[A-Za-z]{0,3}/i }
]
const CAPACITY_TOKEN_REGEX = /(\d+(?:\.\d+)?)\s*([A-Za-z]{1,3})/g
const normalizeCapacityUnit = (value) => {
const unit = String(value || '').trim().toUpperCase()
if (unit === 'GB') return 'G'
if (unit === 'TB') return 'T'
return unit
}
const parseCapacityToken = (value) => {
const matched = normalizeText(value).match(/^(\d+(?:\.\d+)?)\s*([A-Za-z]{0,3})$/)
if (!matched) {
return null
}
return {
raw: matched[0],
value: Number(matched[1]),
unit: matched[2] || '',
normalizedUnit: normalizeCapacityUnit(matched[2] || '')
}
}
const formatCapacityToken = (value, unit) => {
const numeric = Number(value)
const rendered = Number.isInteger(numeric) ? String(numeric) : String(numeric)
return `${rendered}${unit || ''}`
}
const findLastCapacityTokenWithUnit = (value) => {
const text = String(value || '')
let matched = null
let current = null
while ((current = CAPACITY_TOKEN_REGEX.exec(text)) !== null) {
matched = {
raw: current[0],
value: Number(current[1]),
unit: current[2],
normalizedUnit: normalizeCapacityUnit(current[2]),
index: current.index,
length: current[0].length
}
}
CAPACITY_TOKEN_REGEX.lastIndex = 0
return matched
}
const findFirstCapacityTokenWithUnit = (value) => {
const text = String(value || '')
const matched = CAPACITY_TOKEN_REGEX.exec(text)
CAPACITY_TOKEN_REGEX.lastIndex = 0
if (!matched) {
return null
}
return {
raw: matched[0],
value: Number(matched[1]),
unit: matched[2],
normalizedUnit: normalizeCapacityUnit(matched[2]),
index: matched.index,
length: matched[0].length
}
}
const countCapacityTokensWithUnit = (value) => {
const text = String(value || '')
const matched = text.match(/(\d+(?:\.\d+)?)\s*([A-Za-z]{1,3})/g)
return matched ? matched.length : 0
}
const splitAroundFirstCapacityToken = (value) => {
const text = normalizeText(value)
const token = findFirstCapacityTokenWithUnit(text)
if (!token) {
return null
}
return {
prefix: normalizeText(text.slice(0, token.index)),
token,
suffix: normalizeText(text.slice(token.index + token.length))
}
}
const buildModelText = (prefix, token, suffix) =>
cleanModelText(`${prefix || ''}${formatCapacityToken(token.value, token.unit)}${suffix ? ` ${suffix}` : ''}`)
const isMemoryChildType = (childType) => normalizeText(childType) === '内存条'
const isStorageChildType = (childType) => ['固态硬盘', '机械硬盘'].includes(normalizeText(childType))
const canShareMemoryPrefix = (value) => {
const text = normalizeText(value)
return Boolean(text) && !/[+/]/.test(text) && !/\d/.test(text)
}
const canShareMemorySuffix = (value) => {
const text = normalizeText(value)
return Boolean(text) && !/[+/]/.test(text) && !/[\\/]/.test(text)
}
const replaceTokenByIndex = (value, token, replacement) =>
`${value.slice(0, token.index)}${replacement}${value.slice(token.index + token.length)}`
const removeBundleKeyword = (value) =>
normalizeText(value)
.replace(/^套装/i, '')
.replace(/套装/gi, ' ')
.replace(/\s+/g, ' ')
.trim()
const detectMultiUnitHint = (value) => {
const text = normalizeText(value)
if (!text) {
return null
}
return MULTI_UNIT_HINT_PATTERNS.find((item) => item.regex.test(text))?.pattern || null
}
const resolveBracketCapacitySplit = (value) => {
const text = normalizeText(value)
const matched = text.match(/^(.*?)[((]\s*(\d+\s*[A-Za-z]{0,3}|\d+)\s*\*\s*([2-9]\d*)\s*[))](.*)$/i)
if (!matched) {
return null
}
const prefix = matched[1] || ''
const rawInner = matched[2] || ''
const quantity = Number(matched[3] || 0)
const suffix = matched[4] || ''
const inner = parseCapacityToken(rawInner)
const outer = findLastCapacityTokenWithUnit(prefix)
if (!inner || !outer || quantity < 2) {
return {
isMultiUnit: true,
isAutoSplittable: false,
reason: 'missing_outer_or_inner_capacity_token',
pattern: 'bracket_capacity_multiplier',
originalModel: text
}
}
const unitToken = formatCapacityToken(inner.value, inner.unit || outer.unit)
if (outer.value !== inner.value * quantity) {
return {
isMultiUnit: true,
isAutoSplittable: false,
reason: 'outer_capacity_not_equal_to_inner_times_quantity',
pattern: 'bracket_capacity_multiplier',
originalModel: text,
quantity
}
}
const updatedPrefix = replaceTokenByIndex(prefix, outer, unitToken)
const unitModel = cleanModelText(`${updatedPrefix}${removeBundleKeyword(suffix) ? ` ${removeBundleKeyword(suffix)}` : ''}`)
return {
isMultiUnit: true,
isAutoSplittable: true,
quantity,
unitModel,
pattern: 'bracket_capacity_multiplier',
originalModel: text
}
}
const resolveInlineCapacitySplit = (value) => {
const text = normalizeText(value)
const matched = text.match(/^(.*?)(\d+\s*[A-Za-z]{0,3}|\d+)\s*\*\s*([2-9]\d*)\s*$/i)
if (!matched) {
return null
}
const prefix = matched[1] || ''
const rawInner = matched[2] || ''
const quantity = Number(matched[3] || 0)
const inner = parseCapacityToken(rawInner)
const outer = findLastCapacityTokenWithUnit(prefix)
if (!inner || !outer || quantity < 2) {
return {
isMultiUnit: true,
isAutoSplittable: false,
reason: 'missing_outer_or_inner_capacity_token',
pattern: 'inline_capacity_multiplier',
originalModel: text
}
}
const unitToken = formatCapacityToken(inner.value, inner.unit || outer.unit)
if (outer.value !== inner.value * quantity) {
return {
isMultiUnit: true,
isAutoSplittable: false,
reason: 'outer_capacity_not_equal_to_inner_times_quantity',
pattern: 'inline_capacity_multiplier',
originalModel: text,
quantity
}
}
const updatedPrefix = replaceTokenByIndex(prefix, outer, unitToken)
const unitModel = cleanModelText(updatedPrefix)
return {
isMultiUnit: true,
isAutoSplittable: true,
quantity,
unitModel,
pattern: 'inline_capacity_multiplier',
originalModel: text
}
}
const resolveSuffixMultiplierWithParenUnit = (value) => {
const text = normalizeText(value)
const matched = text.match(/^(.*)\*\s*([2-9]\d*)\s*[((]\s*(\d+\s*[A-Za-z]{1,3})\s*[))]\s*$/i)
if (!matched) {
return null
}
const prefix = normalizeText(matched[1] || '')
const quantity = Number(matched[2] || 0)
const rawInner = normalizeText(matched[3] || '')
if (quantity < 2 || !prefix || !rawInner) {
return {
isMultiUnit: true,
isAutoSplittable: false,
reason: 'invalid_suffix_multiplier_with_paren_unit',
pattern: 'suffix_multiplier_with_paren_unit',
originalModel: text
}
}
if (!prefix.includes(rawInner)) {
return {
isMultiUnit: true,
isAutoSplittable: false,
reason: 'paren_unit_not_present_in_prefix',
pattern: 'suffix_multiplier_with_paren_unit',
originalModel: text,
quantity
}
}
return {
isMultiUnit: true,
isAutoSplittable: true,
quantity,
unitModel: cleanModelText(prefix),
pattern: 'suffix_multiplier_with_paren_unit',
originalModel: text
}
}
const resolveCpuXMultiplier = (value) => {
const text = normalizeText(value)
const matched = text.match(/^(.*?)[((]\s*[xX]\s*([2-9]\d*)\s*[))]\s*$/)
if (!matched) {
return null
}
return {
isMultiUnit: true,
isAutoSplittable: true,
quantity: Number(matched[2] || 0),
unitModel: cleanModelText(matched[1]),
pattern: 'cpu_x_multiplier',
originalModel: text
}
}
const resolveSuffixQuantityLabel = (value) => {
const text = normalizeText(value)
const matched = text.match(/^(.*?)\s*\*\s*([2-9]\d*)\s*(个|条|根|块|张|颗)\s*$/)
if (!matched) {
return null
}
return {
isMultiUnit: true,
isAutoSplittable: true,
quantity: Number(matched[2] || 0),
unitModel: cleanModelText(matched[1]),
pattern: 'suffix_quantity_label',
originalModel: text
}
}
const resolveMemoryPlusSplit = (childType, value) => {
if (!isMemoryChildType(childType)) {
return null
}
const text = normalizeText(value)
const parts = text.split('+')
if (parts.length !== 2) {
return null
}
const left = splitAroundFirstCapacityToken(parts[0])
const right = splitAroundFirstCapacityToken(parts[1])
if (!left || !right || countCapacityTokensWithUnit(parts[0]) !== 1 || countCapacityTokensWithUnit(parts[1]) !== 1) {
return {
isMultiUnit: true,
isAutoSplittable: false,
quantity: 0,
unitModel: '',
reason: 'memory_plus_pattern_is_not_single_capacity_pair',
pattern: 'plus_capacity_pair',
originalModel: text
}
}
const leftPrefix = left.prefix || (canShareMemoryPrefix(right.prefix) ? right.prefix : '')
const rightPrefix = right.prefix || (canShareMemoryPrefix(left.prefix) ? left.prefix : '')
const leftSuffix = left.suffix || (canShareMemorySuffix(right.suffix) ? right.suffix : '')
const rightSuffix = right.suffix || (canShareMemorySuffix(left.suffix) ? left.suffix : '')
const models = [
buildModelText(leftPrefix, left.token, leftSuffix),
buildModelText(rightPrefix, right.token, rightSuffix)
]
return {
isMultiUnit: true,
isAutoSplittable: true,
quantity: models.length,
unitModel: models[0] === models[1] ? models[0] : '',
unitModels: models,
pattern: 'plus_capacity_pair',
originalModel: text
}
}
const resolveStorageSimplePlusSplit = (childType, value) => {
if (!isStorageChildType(childType)) {
return null
}
const text = normalizeText(value)
const parts = text.split('+')
if (parts.length !== 2) {
return null
}
const left = splitAroundFirstCapacityToken(parts[0])
const right = splitAroundFirstCapacityToken(parts[1])
if (!left || !right || countCapacityTokensWithUnit(parts[0]) !== 1 || countCapacityTokensWithUnit(parts[1]) !== 1) {
return {
isMultiUnit: true,
isAutoSplittable: false,
quantity: 0,
unitModel: '',
reason: 'storage_plus_pattern_is_not_single_capacity_pair',
pattern: 'plus_capacity_pair',
originalModel: text
}
}
if (left.prefix || left.suffix || right.prefix || right.suffix) {
return {
isMultiUnit: true,
isAutoSplittable: false,
quantity: 0,
unitModel: '',
reason: 'storage_plus_pattern_contains_extra_model_details',
pattern: 'plus_capacity_pair',
originalModel: text
}
}
const models = [
formatCapacityToken(left.token.value, left.token.unit),
formatCapacityToken(right.token.value, right.token.unit)
]
return {
isMultiUnit: true,
isAutoSplittable: true,
quantity: models.length,
unitModel: models[0] === models[1] ? models[0] : '',
unitModels: models,
pattern: 'plus_capacity_pair',
originalModel: text
}
}
const isTypeAndModelConsistent = (childType, model) => {
const normalizedType = normalizeText(childType)
const normalizedModel = normalizeText(model).toUpperCase()
if (!normalizedType || !normalizedModel) {
return true
}
if ((normalizedType === '固态硬盘' || normalizedType === '机械硬盘') && /(DDR\d|MHZ|内存|套装)/i.test(normalizedModel)) {
return false
}
return true
}
const splitHardwareQuantity = (childType, value) => {
const text = normalizeText(value)
if (!text) {
return {
isMultiUnit: false,
isAutoSplittable: false,
quantity: 1,
unitModel: ''
}
}
const hintPattern = detectMultiUnitHint(text)
if (!hintPattern) {
return {
isMultiUnit: false,
isAutoSplittable: false,
quantity: 1,
unitModel: cleanModelText(text),
pattern: 'single_unit',
originalModel: text
}
}
const strategies = [
(type, text) => resolveCpuXMultiplier(text),
(type, text) => resolveSuffixQuantityLabel(text),
(type, text) => resolveBracketCapacitySplit(text),
(type, text) => resolveInlineCapacitySplit(text),
(type, text) => resolveSuffixMultiplierWithParenUnit(text),
resolveMemoryPlusSplit,
resolveStorageSimplePlusSplit
]
for (const strategy of strategies) {
const result = strategy(childType, text)
if (!result) {
continue
}
if (result.isAutoSplittable && !isTypeAndModelConsistent(childType, result.unitModel)) {
return {
...result,
isAutoSplittable: false,
reason: 'child_type_and_model_are_inconsistent'
}
}
return result
}
return {
isMultiUnit: true,
isAutoSplittable: false,
quantity: 0,
unitModel: '',
reason: 'pattern_detected_but_no_safe_split_rule',
pattern: hintPattern,
originalModel: text
}
}
const expandHardwareModels = (childType, value) => {
const result = splitHardwareQuantity(childType, value)
if (!result.isMultiUnit) {
return {
...result,
models: result.unitModel ? [result.unitModel] : []
}
}
if (!result.isAutoSplittable || !result.unitModel || !result.quantity) {
if (Array.isArray(result.unitModels) && result.unitModels.length > 0) {
return {
...result,
models: result.unitModels.map((item) => cleanModelText(item)).filter(Boolean)
}
}
return {
...result,
models: []
}
}
if (Array.isArray(result.unitModels) && result.unitModels.length > 0) {
return {
...result,
models: result.unitModels.map((item) => cleanModelText(item)).filter(Boolean)
}
}
return {
...result,
models: Array.from({ length: result.quantity }, () => result.unitModel)
}
}
module.exports = {
normalizeText,
cleanModelText,
detectMultiUnitHint,
splitHardwareQuantity,
expandHardwareModels
}