Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/src/adagrams.js"
}
]
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
},
"dependencies": {
"core-js": "^3.8.0",
"package.json": "^2.0.1",
"vorpal": "^1.12.0"
}
}
137 changes: 126 additions & 11 deletions src/adagrams.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,130 @@
export const drawLetters = () => {
// Implement this method for wave 1
};
"use strict";

export const usesAvailableLetters = (input, lettersInHand) => {
// Implement this method for wave 2
};
class Adagrams{

export const scoreWord = (word) => {
// Implement this method for wave 3
};
drawLetters = () => {
const letterPool = {
A: 9, B: 2, C: 2, D: 4,
E: 12, F: 2, G: 3, H: 2,
I: 9, J: 1, K: 1, L: 4,
M: 2, N: 6, O: 8, P: 2,
Q: 1, R: 6, S: 4, T: 6,
U: 4, V: 2, W: 2, X: 1,
Y: 2, Z: 1
};
Comment on lines +6 to +14

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny style nitpick: This is not a typical structure we see for objects/dictionaries. It takes up less lines, but we need to carefully read each line to be aware of what keys/values are on each. Though it takes more space, I suggest dropping each key/value pair to a new line to make them very easy and quick to read, unless you are on a team that has expressed a different styling that should be used.

//creates a bag of letters
let bagOfLetters = [];
for (let letter in letterPool) {
for (let i = 0; i < letterPool[letter]; ++i) {
bagOfLetters.push(letter);
}
}
const drawnLetters = [];
Comment on lines +16 to +22

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice solution for ensuring we get the desired distribution of letters.



// adds ten random letters to hand
for (let i = 0; i < 10; ++i) {
let randomIndex = Math.floor((Math.random() * bagOfLetters.length));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is getting a little nested, it isn't necessary, but I would consider moving the multiplication to its own line to make it even easier to read quickly.

let randomLetter = bagOfLetters[randomIndex];
drawnLetters.push(randomLetter); //adds letter to hand
bagOfLetters.splice(randomIndex, 1); //removes letter from bag
}
return drawnLetters;
};

usesAvailableLetters = (input, lettersInHand) => {
// dictionary with letters in hand as keys and their frequencies as values
const letterFreqMap = {};

// anonymous function that is called in the forEach method below
// purpose: populates letterFreqMap
const createMap = function(letter){
if (letter in letterFreqMap) {
letterFreqMap[letter]++;
}
else {
letterFreqMap[letter] = 1;
}
}

export const highestScoreFrom = (words) => {
// Implement this method for wave 4
lettersInHand.forEach(createMap);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool use of a nested function to build the frequency map!


// loops through every letter in input
// checks if letter is in hand
// if letter is there BUT there are no available letters left, return false
// if letter is there BUT there are available letters left, decrement the frequency of the letter by 1
// if letter is not there, return false
for (let i = 0; i < input.length; ++i) {
if (lettersInHand.includes(input[i])){

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something to consider for complexity, .includes does a linear search of the object keys and will be O(n) where n is the total number of keys. If we want that O(1) access, we could do something like:

letterValue = lettersInHand[input[i]]
if (letterValue === undefined || letterValue === 0) {
    return false;
}

letterFreqMap[input[i]]--;

if (letterFreqMap[input[i]] === 0) {
return false;
}
else {
letterFreqMap[input[i]]--;
}
}
else {
return false;
}
}
return true;

};

scoreWord = (word) => {
const scoreChart = {
A: 1, B: 3, C: 3, D: 2,
E: 1, F: 4, G: 2, H: 4,
I: 1, J: 8, K: 5, L: 1,
M: 3, N: 1, O: 1, P: 3,
Q: 10, R: 1, S: 1, T: 1,
U: 1, V: 4, W: 4, X: 8,
Y: 4, Z: 10
};

let score = 0;
if (word.length > 0) {
for (let letter in word){

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can iterate over the letters in word directly using a for...of loop:

for (const letter of word) {
    score += scoreChart[letter.toUpperCase()];
}

If we will not be reassigning the loop variable letter in the body of a loop, I recommend using const over let to declare the variable.

score += scoreChart[word[letter].toUpperCase()];
}
Comment on lines +87 to +89

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could have an unexpected result if the string word has characters that aren't in scoreChart. If we have an input like "ABC DEF" then result will hold NaN (Not a Number) after the loop. If we wanted to skip non-alphabetic characters or characters that aren't in scoreChart, how could we do that?

if (word.length >=7){
score += 8;
}
}
return score;
Comment on lines +85 to +94

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This structure causes the "happy path" or intended flow of our code to be indented. I suggest treating the length check like a guard clause to make the main flow of the code more prominent.

if (word.length > 0) {
    return 0;
}

score = 0;
for (let letter in word){
    score += scoreChart[word[letter].toUpperCase()];
}

if (word.length >=7){
    score += 8;
}

return score;

};

highestScoreFrom = (words) => {
let highestWordAndScore = ["", 0];
let score = 0;
for (let i in words) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice implementation to tie break in a single loop!

score = this.scoreWord(words[i]);
if (score > highestWordAndScore[1]) {
highestWordAndScore = [words[i], score];
}
// check if scores are tied
else if (score === highestWordAndScore[1]) {
// check if the lengths of both words are the same
// if they are, do nothing
if (words[i].length != highestWordAndScore[0].length) {
// if they arent, check if word is 10 letters long
if (words[i].length === 10) {
highestWordAndScore = [words[i], score];
}
// if not, check if word with highest score is not 10 letters long
else if (highestWordAndScore[0].length != 10) {
// if not, check which word has fewer letters
if (words[i].length < highestWordAndScore[0].length) {
highestWordAndScore = [words[i], score];
}
}
}
}

}

return {"score":highestWordAndScore[1], "word":highestWordAndScore[0]};
};
};

export default Adagrams;
56 changes: 26 additions & 30 deletions test/adagrams.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {
drawLetters,
usesAvailableLetters,
scoreWord,
highestScoreFrom,
} from "adagrams";
import Adagrams from "adagrams";

const LETTER_POOL = {
A: 9,
Expand Down Expand Up @@ -35,15 +30,16 @@ const LETTER_POOL = {
};

describe("Adagrams", () => {
let adagrams = new Adagrams();
describe("drawLetters", () => {
it("draws ten letters from the letter pool", () => {
const drawn = drawLetters();
const drawn = adagrams.drawLetters();

expect(drawn).toHaveLength(10);
});

it("returns an array, and each item is a single-letter string", () => {
const drawn = drawLetters();
const drawn = adagrams.drawLetters();

expect(Array.isArray(drawn)).toBe(true);
drawn.forEach((l) => {
Expand All @@ -53,7 +49,7 @@ describe("Adagrams", () => {

it("does not draw a letter too many times", () => {
for (let i = 0; i < 1000; i++) {
const drawn = drawLetters();
const drawn = adagrams.drawLetters();
const letter_freq = {};
for (let letter of drawn) {
if (letter in letter_freq) {
Expand All @@ -75,31 +71,31 @@ describe("Adagrams", () => {
const drawn = ["D", "O", "G", "X", "X", "X", "X", "X", "X", "X"];
const word = "DOG";

const isValid = usesAvailableLetters(word, drawn);
const isValid = adagrams.usesAvailableLetters(word, drawn);
expect(isValid).toBe(true);
});

it("returns false when word contains letters not in the drawn letters", () => {
const drawn = ["D", "O", "X", "X", "X", "X", "X", "X", "X", "X"];
const word = "DOG";

const isValid = usesAvailableLetters(word, drawn);
const isValid = adagrams.usesAvailableLetters(word, drawn);
expect(isValid).toBe(false);
});

it("returns false when word contains repeated letters more than in the drawn letters", () => {
const drawn = ["D", "O", "G", "X", "X", "X", "X", "X", "X", "X"];
const word = "GOOD";

const isValid = usesAvailableLetters(word, drawn);
const isValid = adagrams.usesAvailableLetters(word, drawn);
expect(isValid).toBe(false);
});
});

describe("scoreWord", () => {
const expectScores = (wordScores) => {
Object.entries(wordScores).forEach(([word, score]) => {
expect(scoreWord(word)).toBe(score);
expect(adagrams.scoreWord(word)).toBe(score);
});
};

Expand All @@ -120,7 +116,7 @@ describe("Adagrams", () => {
});

it("returns a score of 0 if given an empty input", () => {
throw "Complete test";
expectScores({"":0});
});

it("adds an extra 8 points if word is 7 or more characters long", () => {
Expand All @@ -133,24 +129,24 @@ describe("Adagrams", () => {
});
});

describe.skip("highestScoreFrom", () => {
describe("highestScoreFrom", () => {
it("returns a hash that contains the word and score of best word in an array", () => {
const words = ["X", "XX", "XXX", "XXXX"];
const correct = { word: "XXXX", score: scoreWord("XXXX") };
const correct = { word: "XXXX", score: adagrams.scoreWord("XXXX") };

expect(highestScoreFrom(words)).toEqual(correct);
expect(adagrams.highestScoreFrom(words)).toEqual(correct);
});

it("accurately finds best scoring word even if not sorted", () => {
const words = ["XXX", "XXXX", "X", "XX"];
const correct = { word: "XXXX", score: scoreWord("XXXX") };
const correct = { word: "XXXX", score: adagrams.scoreWord("XXXX") };

throw "Complete test by adding an assertion";
expect(adagrams.highestScoreFrom(words)).toEqual(correct);
});

describe("in case of tied score", () => {
const expectTie = (words) => {
const scores = words.map((word) => scoreWord(word));
const scores = words.map((word) => adagrams.scoreWord(word));
const highScore = scores.reduce((h, s) => (h < s ? s : h), 0);
const tiedWords = scores.filter((s) => s == highScore);

Expand All @@ -162,37 +158,37 @@ describe("Adagrams", () => {
const words = ["AAAAAAAAAA", "BBBBBB"];
const correct = {
word: "AAAAAAAAAA",
score: scoreWord("AAAAAAAAAA"),
score: adagrams.scoreWord("AAAAAAAAAA"),
};
expectTie(words);

expect(highestScoreFrom(words)).toEqual(correct);
expect(highestScoreFrom(words.reverse())).toEqual(correct);
expect(adagrams.highestScoreFrom(words)).toEqual(correct);
expect(adagrams.highestScoreFrom(words.reverse())).toEqual(correct);
});

it("selects the word with fewer letters when neither are 10 letters", () => {
const words = ["MMMM", "WWW"];
const correct = { word: "WWW", score: scoreWord("WWW") };
const correct = { word: "WWW", score: adagrams.scoreWord("WWW") };
expectTie(words);

expect(highestScoreFrom(words)).toEqual(correct);
expect(highestScoreFrom(words.reverse())).toEqual(correct);
expect(adagrams.highestScoreFrom(words)).toEqual(correct);
expect(adagrams.highestScoreFrom(words.reverse())).toEqual(correct);
});

it("selects the first word when both have same length", () => {
const words = ["AAAAAAAAAA", "EEEEEEEEEE"];
const first = {
word: "AAAAAAAAAA",
score: scoreWord("AAAAAAAAAA"),
score: adagrams.scoreWord("AAAAAAAAAA"),
};
const second = {
word: "EEEEEEEEEE",
score: scoreWord("EEEEEEEEEE"),
score: adagrams.scoreWord("EEEEEEEEEE"),
};
expectTie(words);

expect(highestScoreFrom(words)).toEqual(first);
expect(highestScoreFrom(words.reverse())).toEqual(second);
expect(adagrams.highestScoreFrom(words)).toEqual(first);
expect(adagrams.highestScoreFrom(words.reverse())).toEqual(second);
});
});
});
Expand Down
Loading