A message can contain an user name or group name, the user name has to be 2
character long and should contain uppercase ASCII characters only, similarly group name can be 3-10
characters long. There can be any number of these entities with the optional comment at the end.
The message would have the following format:
amount|<user|group> <,user|group>* opt_comment
Example:
Suppose there is a group COOKIEMONSTERS
with users CD, RM
:
Note: There can be multiple users in a group.
Valid messages
AB+2, BC, COOKIEMONSTERS "Dinner"
AB, BC
AB, BC, FOODIE
AB, BC, FOODIE+4
Invalid messages
A, BC, COOKIEMONSTERS "Dinner"
A, BC, "Dinner" COOKIEMONSTERS
A, BC, SOMEVERYVERYLONGNAME
1+A, BC
I have to parse the message and if there is group name present then I have to expand it into its constituent user names (assume that I have some service for it) hence, given the group constraint I chose not to follow the regex path.
So, given the above message I need the following information:
['AB+2', 'BC', 'CD', 'RM'] and "Dinner" if comment present
where CD
and RM
are members of the group COOKIEMONSTERS
.
I have written some very ugly code and want to know if some cleaner approach is possible.
Code:
const USER_LENGTH = 2;
function _parseExpenseMessage(message, dependencies) {
const parseInfo = parse(message);
const mul = [];
const add = [];
const handles = [];
let idx = 0;
for (let handle of parseInfo.handles) {
handles.push(handle.substring(0, USERNAME_LENGTH));
idx = handle.indexOf('*');
if (idx !== -1) {
mul.push(roundToTwo(handle[idx + 1]));
} else {
mul.push(1);
}
idx = handle.indexOf('+');
if (idx !== -1) {
add.push(roundToTwo(handle[idx + 1]));
} else {
add.push(0);
}
}
let amount = +parseInfo.amount;
let each = (amount - add.reduce((x, sum) => x + sum, 0)) / mul.reduce((x, sum) => x + sum, 0);
const res = [];
for (let i = 0; i < handles.length; i++) {
res.push({
target: handles[i],
amount: each * mul[i] + add[i]
});
}
return {expenses: res, comment: parseInfo.comment};
};
function parse(text) {
const duplicates = new Set();
const handles = [];
const amount = text.split('|')[0];
const message = text.split('|')[1];
let name = '';
let expr = '';
let hasComment = false;
let i = 0;
let len = message.length;
for (; i < len; i++) {
var char = message.charAt(i);
if (isValidCharacter(char)) {
name += char;
} else if (isExpression(char)) {
expr += char;
}
if (isSeparator(char) || i === message.length - 1) {
if (char === '"') {
if (message.lastIndexOf('"') === message.length - 1) {
hasComment = true;
break;
} else {
throw new Error('Invalid comment format');
}
}
if (name.length === USERNAME_LENGTH) {
handles.push(name + expr);
duplicates.add(name);
} else if (name.length > USERNAME_LENGTH) {
let group = someService(name);
for (let user of group.users()) {
let name = user.getName();
duplicates.add(user);
handles.push(user + expr);
}
}
name = '';
expr = '';
}
}
let comment = '';
if (hasComment) {
comment = message.substr(i, len).replace(/"/g, '');
}
// Duplicate entry not allowed
if (duplicates.size < handles.length) {
throw new Error('Duplicate entry');
}
return {amount: amount, handles: handles, comment: comment};
}
function isValidCharacter(char) {
return (char >= 'A' && char <= 'Z');
}
function isExpression (char) {
return (char === '*' || char === '+' || (char >= '0' && char <= '9'));
}
function isSeparator (char) {
return (char === ' ' || char === ',' || char === '"');
}
function roundToTwo(num) {
return Math.round(+(num) * 100) / 100;
}
Note:
I also need to check that there are not duplicate user in the message above.
Update 1:
The user name can contain expression with the limited semantics, eg: AB+1
or AB*2
or AB+1*2
that's it. There can be only two operators +
and *
.
CD
andRM
are coming from in your example? Also what is the+2
for? \$\endgroup\$+n
suffix mean? Are there always exactly two user names? If not, thenAB, CD
is ambiguous - CD could be a group. So I assume it's always two usernames. But then what's the purpose of the 2nd valid example, if the idea is to associate users with a group and no group is specified? \$\endgroup\$