UNPKG

calculate-items

Version:

Items calculation template

263 lines (234 loc) 11 kB
import NumberFormatter from "./NumberFormatter"; import { compact, gt, gte, isEmpty, lte, map, range, replace, size, split, trim } from "lodash"; import { LINE_SPLITTER, MEMBER_INDEXES_PATTERN, MEMBER_NAMES_PATTERN, NOT_ARITHMETIC_CHARS_PATTERN, VALUE_SPLITTER } from "../constants/patterns"; import { CalculatedMembers, Item, ItemsResult, Summary } from "../types/types"; import MembersParser from "./MembersParser"; import {splitLineText} from "./itemSplitter"; export default class ItemsParser { public static getItemsResult(exp: string): ItemsResult { return ItemsParser.calculateResultFromExpression(exp); } public static getExpItems(exp: string): Item[] { if (!isEmpty(trim(exp))) { return ItemsParser.getSplitItems(exp) .map(trim) .filter(l => l.match(/\d/g)) .map((l, ind) => { const members = MembersParser.getUserIndexes(l); const cleanValue = trim(replace(l, MEMBER_INDEXES_PATTERN, '')); return { name: `Item ${ind + 1}`, order: ind + 1, members, ...ItemsParser.getValueAndType(cleanValue, members), }; }); } return []; } public static getTemplateItems(templateExp?: string): Item[] { if (templateExp && !isEmpty(trim(templateExp))) { // const error = getInvalidMessage(text); // if(error){ // throw new Error(error) // } templateExp = templateExp.replace(MEMBER_NAMES_PATTERN, '') if (!templateExp.match(/[a-zA-Z]/)) { return ItemsParser.getExpItems(templateExp); } return split(templateExp, LINE_SPLITTER) .map(trim) .map(splitLineText) .filter(split => { return gte(size(split), 2); }) .map(([name, value], ind): Item => { const obj = ItemsParser.getMembersFromName(name); return { order: ind + 1, ...obj, ...ItemsParser.getValueAndType(value, obj.members), }; }); } return []; } public static calculateResultFromExpression(exp?: string): ItemsResult { const items = ItemsParser.getTemplateItems(exp) const usernames = MembersParser.getUsernames(exp) const cache: CalculatedMembers = { membersMap: {}, membersCount: size(usernames) || 0, }; if (!cache.membersCount) { cache.membersCount = items.reduce((max, product) => { return Math.max(max, ...product.members, 1); }, 0); if (cache.membersCount > 20) cache.membersCount = 20; } items .map(p => ({ product: p, pMembersCount: size(p.members), })) .filter(obj => obj.product.valueType === 'currency' && gt(obj.pMembersCount, 0)) .forEach(obj => { obj.product.members.forEach(memberIndex => { const bracketValueObj = Object.keys(obj.product.values || {}).length && obj.product.values?.[memberIndex] || undefined; const pTotal = bracketValueObj?.total || NumberFormatter.round(obj.product.value / obj.pMembersCount); if (!cache.membersMap[memberIndex]) { cache.membersMap[memberIndex] = { index: memberIndex, total: 0, items: [{ ...obj.product, value: pTotal }], productNames: [], totalExpression: '', }; } else { cache.membersMap[memberIndex].items?.push(obj.product); } cache.membersMap[memberIndex].total += pTotal; if (cache.membersMap[memberIndex].totalExpression.length) { cache.membersMap[memberIndex].totalExpression += ' + '; } cache.membersMap[memberIndex].totalExpression += pTotal; cache.membersMap[memberIndex].productNames?.push(obj.product.name); cache.membersMap[memberIndex].totalExpression += bracketValueObj?.total ? `(${bracketValueObj.origin})` : `(${obj.product.originValue}/${obj.pMembersCount})`; }); }); items //common products with currency valueType .filter(p => p.valueType === 'currency' && lte(size(p.members), 0)) .forEach(product => { range(1, cache.membersCount + 1).forEach(memberIndex => { if (!cache.membersMap[memberIndex]) { cache.membersMap[memberIndex] = { index: memberIndex, total: 0, items: [], productNames: [], totalExpression: '', }; } const pTotal = NumberFormatter.round(product.value / cache.membersCount); cache.membersMap[memberIndex].items?.push({ ...product, value: pTotal }); cache.membersMap[memberIndex].total += pTotal; if (cache.membersMap[memberIndex].totalExpression.length) { cache.membersMap[memberIndex].totalExpression += ' + '; } cache.membersMap[memberIndex].totalExpression += pTotal; cache.membersMap[memberIndex].productNames?.push(product.name); cache.membersMap[memberIndex].totalExpression += `(${product.value}/${cache.membersCount})`; }); }); items //percentage valueType .filter(p => p.valueType === 'percentage' && lte(size(p.members), 0)) .forEach((product, ind) => { let percentageTotal = 0; Object.keys(cache.membersMap) .forEach(memberIndex => { const oldTotal = cache.membersMap[memberIndex].total; const pTotal = NumberFormatter.round((oldTotal * product.value) / 100); cache.membersMap[memberIndex].items?.push({ ...product, value: pTotal }); cache.membersMap[memberIndex].total += pTotal; percentageTotal += pTotal; if (cache.membersMap[memberIndex].totalExpression.length) { cache.membersMap[memberIndex].totalExpression += ' + '; } cache.membersMap[memberIndex].totalExpression += pTotal; cache.membersMap[memberIndex].productNames?.push(product.name); cache.membersMap[memberIndex].totalExpression += `(${oldTotal}*${product.value / 100})`; }); product.value = percentageTotal; }); const summary = Object.keys(cache.membersMap) .map(ind => { delete cache.membersMap[ind].items; return cache.membersMap[ind]; }) .reduce( (obj, member) => { obj.total += member.total; obj.total = NumberFormatter.round(obj.total); if (usernames.length >= member.index) { member.username = usernames[member.index - 1]; } obj.members.push(member); return obj; }, { membersCount: cache.membersCount, members: [], total: 0, inputText: exp } as Summary, ); return { items, summary } } private static getValueAndType(valueStr: string, members: number[] = []): Pick<Item, 'value' | 'valueType' | 'originValue' | 'values' | 'quantity' | 'unitValue'> { const valueType = valueStr.includes('%') ? 'percentage' : 'currency'; const value = NumberFormatter.evaluateAndRound(valueStr.replace(NOT_ARITHMETIC_CHARS_PATTERN, '')); const quantity = NumberFormatter.evaluateAndRound(valueStr.match(/([^*]+)\*.+/)?.[1] || '') || 1; const membersCount = size(members); const values: Item['values'] = {}; if (membersCount > 0 && valueStr.includes('(')) { const arr = valueStr.split(/\)?\*/); if (arr.length > 1) { const bracketStr = arr[0].slice(1); if (bracketStr) { const bracketNumbers = bracketStr.split('+'); members.forEach((m, ind) => { values[m] = { total: NumberFormatter.evaluateAndRound(bracketNumbers[ind]) * NumberFormatter.evaluateAndRound(arr[1]), origin: `${NumberFormatter.evaluateAndRound(bracketNumbers[ind])}*${NumberFormatter.evaluateAndRound(arr[1])}`, }; }); } } } return { value, quantity: quantity || 1, unitValue: value / quantity, originValue: valueStr, values, valueType, }; } private static getSplitItems(exp: string): string[] { const state = { bracketOpened: false, arr: [] as string[], }; for (const char of exp) { if (char === '(') { state.bracketOpened = true; } else if (char === ')') { state.bracketOpened = false; } if (!state.bracketOpened && char === '+') { state.arr.push(''); continue; } if (state.arr.length === 0) { state.arr.push(char); } else { state.arr[state.arr.length - 1] += char; } } return compact(state.arr); } private static getMembersFromName(label: string): Pick<Item, 'name' | 'members'> { const members = MembersParser.getUserIndexes(label) const cleanLabel = trim(replace(label, MEMBER_INDEXES_PATTERN, '')); return { members, name: cleanLabel, }; } }