calculate-items
Version:
Items calculation template
263 lines (234 loc) • 11 kB
text/typescript
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,
};
}
}