I am trying to solve the following challenge:
Description
Given an input string, count occurrences of a given emoji (encoded as
:apple:
) for each user after encountering their user name (encoded as<@User />
). If a user does not have any of the given emoji, they inherit the count from the next user in the sequence. This process continues until all emoji occurrences are found, and those are then assigned to all users in the group that lack them.The input string and the emoji (without the colons) are passed as argument to the function
countEmoji(message, emoji)
. This function should return an object that has a key (lowercase) for each encoded user, and the corresponding number of emojis found for that user.Example
Input:
message = '<@Kate />:apple: <@Max/>sometext<@alisa /> :like: received:apple::apple:'
- `emoji = 'apple'
Expected output:
{ kate: 1, max: 2, alisa: 2 }
Emoji valid pattern:
A valid emoji is a text (not case sensitive) delimited on both sides by a colon. For example
:apple:
It should have no spaces. There is no sharing of colons between emoji. So
:apple:apple:
only has one valid emoji.Examples:
:apple:
encodes 1 apple:aPPle::aPpLe:
encodes 2 apples:apple:apple:
encodes 1 apple:apple :
encodes 0 apples: Apple :
encodes 0 applesName pattern:
A valid name is encoded as
<@Name />
without spaces in or before the name, where the varyingName
must consist of only English letters, and is not considered case sensitive. There can be spaces before the ending/>
. So<@namee />
is valid.Requirements
- Use only 1 for loop, with all iteration in one loop.
- Only use character codes for parsing.
- Use 1 object to store the results (no additional arrays or objects are allowed).
- Use
charCodeAt
to identify necessary characters andfromCharCode
to convert to lowercase.- Can't use JavaScript methods other than those mentioned (No
map
,filter
, ...), and nofor...in
loop.
I can keep track of the previous username, but how do I handle a long chain of usernames?
Using a for...in
loop would make this easy, but I am restricted to only using a single for loop.
function countEmoji(message, emoji) {
const ASCII = {
numFirstCharCode: 48,
numLastCharCode: 57,
openBracket: 60,
closeBracket: 62,
atSign: 64,
slash: 47,
toLowerOffest: 32,
upperCaseStart: 65,
upperCaseEnd: 90,
lowerCaseStart: 97,
lowerCaseEnd: 122,
space: 32,
colon: 58,
};
let result = {};
let name = "";
let isNameCreated = false;
let nameCreationStage = 0;
let emojiFounded = 0;
let emojiStage = -1;
function toLowerCase(char) {
const isInUpperCase =
ASCII.upperCaseStart <= char && char <= ASCII.upperCaseEnd;
if (isInUpperCase) {
return String.fromCharCode(char + ASCII.toLowerOffest);
}
return char;
}
for (let i = 0; i < message.length; i++) {
const charCode = message.charAt(i).charCodeAt();
const isInLowerCase =
ASCII.lowerCaseStart <= charCode && charCode <= ASCII.lowerCaseEnd;
const isInUpperCase =
ASCII.upperCaseStart <= charCode && charCode <= ASCII.upperCaseEnd;
if (!isNameCreated) {
if (charCode === ASCII.openBracket && nameCreationStage === 0) {
nameCreationStage++;
} else if (charCode === ASCII.atSign && nameCreationStage === 1) {
nameCreationStage++;
} else if ((isInUpperCase || isInLowerCase) && nameCreationStage === 2) {
if (isInLowerCase) {
name += String.fromCharCode(charCode);
} else if (isInUpperCase) {
name += String.fromCharCode(charCode + ASCII.toLowerOffest);
}
} else if (name) {
if (charCode === ASCII.space) {
continue;
} else if (charCode === ASCII.slash) {
nameCreationStage++;
} else if (charCode === ASCII.closeBracket && nameCreationStage === 3) {
nameCreationStage = 0;
isNameCreated = true;
}
}
continue;
}
if (isNameCreated) {
//search for pattern start - :
if (charCode === ASCII.colon && emojiStage === -1) {
emojiStage++;
continue;
}
if (emojiStage - (emoji.length - 1) === 1) {
if (charCode === ASCII.colon) {
emojiFounded++;
emojiStage = -1;
continue;
}
}
//start compering symbols
if (charCode === toLowerCase(emoji.charAt(emojiStage).charCodeAt())) {
emojiStage++;
continue;
} else {
if (charCode === ASCII.openBracket && nameCreationStage === 0) {
nameCreationStage++;
isNameCreated = false;
result = { ...result,
[name]: emojiFounded
};
name = "";
emojiFounded = 0;
}
emojiStage = -1;
}
}
}
result = { ...result,
[name]: emojiFounded
};
return result;
}
const text = '<@Kate />:apple: <@Max/>sometext<@alisa /> :like: received:apple::apple:';
const emoji = "apple";
console.log(countEmoji(text, emoji));
My code returns {kate: 1, max: 0; alisa: 2}
, but this has the wrong number for max
. It should be 2.
How can I do this without an extra loop and within all the requirements?
You could use recursion. Every time you match a name tag, you could make a recursive call and get back a count of emojis, which is the number of emojis that follow that tag until the next name tag (or the end of the string). As you unwind from recursion, you still have the username for which you made the recursive call, and thus are in a position to update that user's emoji-count.
Here is an implementation:
const ASCII = {
numFirstCharCode: 48,
numLastCharCode: 57,
openBracket: 60,
closeBracket: 62,
atSign: 64,
slash: 47,
toLowerOffset: 32,
upperCaseStart: 65,
upperCaseEnd: 90,
lowerCaseStart: 97,
lowerCaseEnd: 122,
space: 32,
colon: 58,
};
function toLowerCase(ch) {
return ch >= ASCII.upperCaseStart && ch <= ASCII.upperCaseEnd
? ch + ASCII.toLowerOffset
: ch;
}
function isLetter(ch) {
return ch >= ASCII.lowerCaseStart && ch <= ASCII.lowerCaseEnd;
}
function process(message, emoji, start, result) {
let emojiCounted = 0; // Number of emoji counted from the start index onwards, before the next name tag
let emojiIndex = -1; // Number of characters matched of the emoji (-1 = no colon yet)
let name = ""; // Letters constituting a (partial) tag name
let nameReadingStage = 0; /* 0: did not encounter (part of) a name tag yet;
1: found "<@" followed by 0 or more letters;
2: found "<@" followed by 0 or more letters, followed by 1 or more spaces */
for (let i = start; i < message.length; i++) {
const ch = toLowerCase(message.charCodeAt(i));
if (ch === ASCII.colon) {
nameReadingStage = 0;
if (emojiIndex === emoji.length) { // We have matched an emoji
emojiIndex = -1;
emojiCounted++;
} else {
emojiIndex = 0; // At the start of a potential emoji
}
continue;
}
if (ch === toLowerCase(emoji.charCodeAt(emojiIndex))) { // Matching the emoji
nameReadingStage = 0;
emojiIndex++;
continue;
}
emojiIndex = -1;
const ahead = toLowerCase(message.charCodeAt(i+1));
if (ch === ASCII.openBracket && ahead === ASCII.atSign) {
i++;
nameReadingStage = 1;
name = "";
} else if (nameReadingStage === 1 && isLetter(ch)) {
name += String.fromCharCode(ch);
} else if (nameReadingStage > 0 && ch == ASCII.space) {
nameReadingStage = 2; // From now on only spaces are allowed before tag closure
} else if (nameReadingStage > 0 && name && ch === ASCII.slash && ahead === ASCII.closeBracket) {
result[name] = 0; // Create a key now (so we maintain key order)
// Use recursion to process the rest of the message and update this key
result[name] = process(message, emoji, i+2, result);
emojiCounted ||= result[name]; // Override zero with what we got back
break;
} else {
nameReadingStage = 0;
}
}
return emojiCounted;
}
function countEmoji(message, emoji) {
const result = {};
process(message, emoji, 0, result);
return result;
}
// Demo
const text = '<@Kate />:apple: <@Max />sometext :apple <@alisa /> :like: received:apple::apple:';
const result = countEmoji(text, "apple");
console.log(result);