/** ضریب تبدیل مثقال به گرم — در فرمول با MG */ export const FORMULA_MG = 4.331802 export const DEFAULT_PRICE_FORMULA = '((P)/MG)*W+100000' /** اگر وزن ۰ یا نامعتبر باشد، ۱ در نظر گرفته می‌شود */ export function resolveFormulaWeight(amount: number | null | undefined): number { const w = Number(amount) return !Number.isFinite(w) || w === 0 ? 1 : w } export const FORMULA_VARIABLE_HELP_BASE = [ { symbol: 'P', label: 'قیمت وسط مرجع (فقط مظنه انتخاب‌شده، بدون تفاوت ارائه‌دهنده)' }, { symbol: 'MG', label: `ضریب مثقال به گرم (${FORMULA_MG})` } ] as const export const FORMULA_FUNCTION_HELP = [ { symbol: 'ROUND(x)', label: 'رند به نزدیک‌ترین عدد صحیح' }, { symbol: 'ROUND(x, -3)', label: 'رند به هزار (مثل اکسل؛ -4 ده‌هزار، -5 صدهزار)' }, { symbol: 'FLOOR(x)', label: 'گرد به پایین' }, { symbol: 'CEIL(x)', label: 'گرد به بالا' } ] as const export function getFormulaVariableHelp(productAmount: number) { const w = resolveFormulaWeight(productAmount) return [ FORMULA_VARIABLE_HELP_BASE[0], { symbol: 'W', label: `وزن محصول (${w})` }, FORMULA_VARIABLE_HELP_BASE[1], ...FORMULA_FUNCTION_HELP ] } type FormulaToken = | { type: 'number'; value: string } | { type: 'ident'; value: string } | { type: 'op'; value: string } function tokenize(expression: string): FormulaToken[] { const normalized = expression.replace(/×/g, '*').replace(/÷/g, '/').replace(/\s+/g, '') if (!normalized || !/^[0-9A-Za-z_.,+\-*/()]+$/.test(normalized)) { throw new Error('Invalid formula characters') } const parts = normalized.match(/([0-9]+(?:\.[0-9]+)?|[A-Za-z_][A-Za-z0-9_]*|[+\-*/(),])/g) if (!parts || parts.join('') !== normalized) { throw new Error('Invalid formula syntax') } return parts.map((part) => { if (/^[0-9]/.test(part)) return { type: 'number', value: part } if (/^[A-Za-z_]/.test(part)) return { type: 'ident', value: part.toUpperCase() } return { type: 'op', value: part } }) } function roundPrecision(value: number, precision = 0): number { const p = Number(precision) || 0 const factor = 10 ** p return Math.round(value * factor) / factor } function floorPrecision(value: number, precision = 0): number { const p = Number(precision) || 0 const factor = 10 ** p return Math.floor(value * factor) / factor } function ceilPrecision(value: number, precision = 0): number { const p = Number(precision) || 0 const factor = 10 ** p return Math.ceil(value * factor) / factor } function applyFunction(name: string, args: number[]): number { if (args.length < 1) throw new Error(`Invalid arguments for ${name}`) switch (name) { case 'ROUND': return roundPrecision(args[0], args[1] ?? 0) case 'FLOOR': return floorPrecision(args[0], args[1] ?? 0) case 'CEIL': case 'CEILING': return ceilPrecision(args[0], args[1] ?? 0) default: throw new Error(`Unknown function: ${name}`) } } function parseFactor(tokens: FormulaToken[], position: { i: number }, variables: Record): number { const token = tokens[position.i] if (!token) throw new Error('Unexpected end of formula') if (token.type === 'number') { position.i++ return Number(token.value) } if (token.type === 'ident') { position.i++ const name = token.value const next = tokens[position.i] if (next && next.type === 'op' && next.value === '(') { position.i++ const args: number[] = [] if (!(tokens[position.i] && tokens[position.i].type === 'op' && tokens[position.i].value === ')')) { args.push(parseExpression(tokens, position, variables)) while (tokens[position.i] && tokens[position.i].type === 'op' && tokens[position.i].value === ',') { position.i++ args.push(parseExpression(tokens, position, variables)) } } if (!tokens[position.i] || tokens[position.i].type !== 'op' || tokens[position.i].value !== ')') { throw new Error('Missing closing parenthesis') } position.i++ return applyFunction(name, args) } if (!(name in variables)) throw new Error(`Unknown variable: ${name}`) return variables[name] } if (token.type === 'op' && token.value === '(') { position.i++ const value = parseExpression(tokens, position, variables) if (!tokens[position.i] || tokens[position.i].type !== 'op' || tokens[position.i].value !== ')') { throw new Error('Missing closing parenthesis') } position.i++ return value } if (token.type === 'op' && token.value === '-') { position.i++ return -parseFactor(tokens, position, variables) } if (token.type === 'op' && token.value === '+') { position.i++ return parseFactor(tokens, position, variables) } throw new Error('Invalid formula syntax') } function parseTerm(tokens: FormulaToken[], position: { i: number }, variables: Record): number { let value = parseFactor(tokens, position, variables) while (position.i < tokens.length && tokens[position.i].type === 'op' && ['*', '/'].includes(tokens[position.i].value)) { const operator = tokens[position.i].value position.i++ const right = parseFactor(tokens, position, variables) value = operator === '/' ? value / right : value * right } return value } function parseExpression(tokens: FormulaToken[], position: { i: number }, variables: Record): number { let value = parseTerm(tokens, position, variables) while (position.i < tokens.length && tokens[position.i].type === 'op' && ['+', '-'].includes(tokens[position.i].value)) { const operator = tokens[position.i].value position.i++ const right = parseTerm(tokens, position, variables) value = operator === '+' ? value + right : value - right } return value } export function evaluatePriceFormula(formula: string, p: number, w: number): number | null { const trimmed = (formula || '').trim() if (!trimmed) return null try { const tokens = tokenize(trimmed) const position = { i: 0 } const result = parseExpression(tokens, position, { P: Number(p) || 0, W: Number(w) || 0, MG: FORMULA_MG }) if (position.i < tokens.length || !Number.isFinite(result)) { return null } return Math.round(result) } catch { return null } } /** P = قیمت وسط مرجع (بدون تفاوت ارائه‌دهنده) */ export function computeFormulaProductPrice( referenceMiddlePrice: number, formula: string | undefined | null, weight: number ): number { const expression = (formula || '').trim() || DEFAULT_PRICE_FORMULA return evaluatePriceFormula(expression, referenceMiddlePrice, resolveFormulaWeight(weight)) ?? 0 }