This is a problem from exercism.io. View the repo here (contains full example of song).
Generate the lyrics of the song 'I Know an Old Lady Who Swallowed a Fly'.
While you could copy/paste the lyrics, or read them from a file, this problem is much more interesting if you approach it algorithmically.
This problem is quite similar to my previous question of 99 bottles. I've adopted what I learned from that review with the goal of writing idiomatic javascript and clean code.
Being new to javascript, I'm wondering if anything sticks out that might be improved.
food-chain.js
const FoodChain = module.exports = {
FOOD_CHAIN: {
'fly': "",
'spider': "It wriggled and jiggled and tickled inside her.\n",
'bird': "How absurd to swallow a bird!\n",
'cat': "Imagine that, to swallow a cat!\n",
'dog': "What a hog, to swallow a dog!\n",
'goat': "Just opened her throat and swallowed a goat!\n",
'cow': "I don't know how she swallowed a cow!\n",
'horse': "She's dead, of course!\n",
},
getKeyAtIndex(index) {
return Object.keys(this.FOOD_CHAIN)[index];
},
getVerseAtKey(key) {
return this.FOOD_CHAIN[key];
},
verse(number) {
const food = Food.getFood(number - 1);
return (
`I know an old lady who swallowed a ${food.name}.\n` +
`${food.verse}` +
`${this.getRepeatedVerses(food.rank)}`
);
},
getRepeatedVerses(rank) {
return [...this.reciteRepeatedVerses(rank)].join('\n');
},
*reciteRepeatedVerses(rank) {
let current = rank;
while (current >= 0) {
const food = Food.getFood(current--);
if (food.name === 'horse') break;
yield food.repeatedVerse;
}
},
verses(...range) {
this.validateArguments(...range);
return [...this.reciteRangeOfVerses(...range)].join('\n') + '\n';
},
*reciteRangeOfVerses(starting, ending = 0) {
let current = starting;
while (current <= ending) {
yield this.verse(current++);
}
},
validateArguments(...args) {
if (args.some((arg) => arg < 0)) {
throw new RangeError('Invalid arguments: cannot be negative.');
}
if (args.length > 2) {
throw new RangeError('Invalid arguments: max 2.');
}
if (args.length == 2 && args[0] > args[1]) {
throw new RangeError('Invalid arguments: must be in increasing order.');
}
},
}
class Food {
constructor(rank) {
this.rank = rank;
}
static getFood(rank) {
const name = FoodChain.getKeyAtIndex(rank);
switch (name) {
case 'fly':
return new Fly(rank);
case 'bird':
return new Bird(rank);
default:
return new Food(rank);
}
}
get name() {
return FoodChain.getKeyAtIndex(this.rank);
}
get verse() {
return FoodChain.getVerseAtKey(this.name);
}
get predecessor() {
return FoodChain.getKeyAtIndex(this.rank - 1);
}
get repeatedVerse() {
return `She swallowed the ${this.name} to catch the ${this.predecessor}.`;
}
}
class Fly extends Food {
constructor(rank) {
super(rank);
}
get repeatedVerse() {
return "I don't know why she swallowed the fly. Perhaps she'll die.\n";
}
}
class Bird extends Food {
constructor(rank) {
super(rank);
}
get repeatedVerse() {
return 'She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.';
}
}
food-chain.spec.js
const song = require('./food-chain');
describe('Food Chain', function () {
it('fly', function () {
const expected = 'I know an old lady who swallowed a fly.\nI don\'t know why she swallowed the fly. Perhaps she\'ll die.\n';
expect(song.verse(1)).toEqual(expected);
});
it('spider', function () {
const expected = 'I know an old lady who swallowed a spider.\nIt wriggled and jiggled and tickled inside her.\n' +
'She swallowed the spider to catch the fly.\n' + 'I don\'t know why she swallowed the fly. Perhaps she\'ll die.\n';
expect(song.verse(2)).toEqual(expected);
});
it('bird', function () {
const expected = 'I know an old lady who swallowed a bird.\n' +
'How absurd to swallow a bird!\n' +
'She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.\n' +
'She swallowed the spider to catch the fly.\n' +
'I don\'t know why she swallowed the fly. Perhaps she\'ll die.\n';
expect(song.verse(3)).toEqual(expected);
});
it('cat', function () {
const expected = 'I know an old lady who swallowed a cat.\n' +
'Imagine that, to swallow a cat!\n' +
'She swallowed the cat to catch the bird.\n' +
'She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.\n' +
'She swallowed the spider to catch the fly.\n' +
'I don\'t know why she swallowed the fly. ' +
'Perhaps she\'ll die.\n';
expect(song.verse(4)).toEqual(expected);
});
it('dog', function () {
const expected = 'I know an old lady who swallowed a dog.\n' +
'What a hog, to swallow a dog!\n' +
'She swallowed the dog to catch the cat.\n' +
'She swallowed the cat to catch the bird.\n' +
'She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.\n' +
'She swallowed the spider to catch the fly.\n' +
'I don\'t know why she swallowed the fly. ' +
'Perhaps she\'ll die.\n';
expect(song.verse(5)).toEqual(expected);
});
it('goat', function () {
const expected = 'I know an old lady who swallowed a goat.\n' +
'Just opened her throat and swallowed a goat!\n' +
'She swallowed the goat to catch the dog.\n' +
'She swallowed the dog to catch the cat.\n' +
'She swallowed the cat to catch the bird.\n' +
'She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.\n' +
'She swallowed the spider to catch the fly.\n' +
'I don\'t know why she swallowed the fly. ' +
'Perhaps she\'ll die.\n';
expect(song.verse(6)).toEqual(expected);
});
it('cow', function () {
const expected = 'I know an old lady who swallowed a cow.\n' +
'I don\'t know how she swallowed a cow!\n' +
'She swallowed the cow to catch the goat.\n' +
'She swallowed the goat to catch the dog.\n' +
'She swallowed the dog to catch the cat.\n' +
'She swallowed the cat to catch the bird.\n' +
'She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.\n' +
'She swallowed the spider to catch the fly.\n' +
'I don\'t know why she swallowed the fly. ' +
'Perhaps she\'ll die.\n';
expect(song.verse(7)).toEqual(expected);
});
it('horse', function () {
const expected = 'I know an old lady who swallowed a horse.\n' + 'She\'s dead, of course!\n';
expect(song.verse(8)).toEqual(expected);
});
it('multiple verses', function () {
let expected = '';
expected += 'I know an old lady who swallowed a fly.\nI don\'t know why she swallowed the fly. Perhaps she\'ll die.\n\n';
expected += 'I know an old lady who swallowed a spider.\nIt wriggled and jiggled and tickled inside her.\n' +
'She swallowed the spider to catch the fly.\n' +
'I don\'t know why she swallowed the fly. Perhaps she\'ll die.\n\n';
expect(song.verses(1, 2)).toEqual(expected);
});
it('the whole song', function () {
let expected = '';
expected += 'I know an old lady who swallowed a fly.\nI don\'t know why she swallowed the fly. Perhaps she\'ll die.\n\n';
expected += 'I know an old lady who swallowed a spider.\nIt wriggled and jiggled and tickled inside her.\n' +
'She swallowed the spider to catch the fly.\n' +
'I don\'t know why she swallowed the fly. Perhaps she\'ll die.\n\n';
expected += 'I know an old lady who swallowed a bird.\n' +
'How absurd to swallow a bird!\n' +
'She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.\n' +
'She swallowed the spider to catch the fly.\n' +
'I don\'t know why she swallowed the fly. Perhaps she\'ll die.\n\n';
expected += 'I know an old lady who swallowed a cat.\n' +
'Imagine that, to swallow a cat!\n' +
'She swallowed the cat to catch the bird.\n' +
'She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.\n' +
'She swallowed the spider to catch the fly.\n' +
'I don\'t know why she swallowed the fly. ' +
'Perhaps she\'ll die.\n\n';
expected += 'I know an old lady who swallowed a dog.\n' +
'What a hog, to swallow a dog!\n' +
'She swallowed the dog to catch the cat.\n' +
'She swallowed the cat to catch the bird.\n' +
'She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.\n' +
'She swallowed the spider to catch the fly.\n' +
'I don\'t know why she swallowed the fly. ' +
'Perhaps she\'ll die.\n\n';
expected += 'I know an old lady who swallowed a goat.\n' +
'Just opened her throat and swallowed a goat!\n' +
'She swallowed the goat to catch the dog.\n' +
'She swallowed the dog to catch the cat.\n' +
'She swallowed the cat to catch the bird.\n' +
'She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.\n' +
'She swallowed the spider to catch the fly.\n' +
'I don\'t know why she swallowed the fly. ' +
'Perhaps she\'ll die.\n\n';
expected += 'I know an old lady who swallowed a cow.\n' +
'I don\'t know how she swallowed a cow!\n' +
'She swallowed the cow to catch the goat.\n' +
'She swallowed the goat to catch the dog.\n' +
'She swallowed the dog to catch the cat.\n' +
'She swallowed the cat to catch the bird.\n' +
'She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.\n' +
'She swallowed the spider to catch the fly.\n' +
'I don\'t know why she swallowed the fly. ' +
'Perhaps she\'ll die.\n\n';
expected += 'I know an old lady who swallowed a horse.\n' + 'She\'s dead, of course!\n\n';
expect(song.verses(1, 8)).toEqual(expected);
});
it('throws when given too many arguments', function() {
const expected = new RangeError('Invalid arguments: max 2.');
expect(() => song.verses(0, 2, 3)).toThrow(expected);
});
it("throws when given 2 arguments that aren't in increasing order", function() {
const expected = new RangeError('Invalid arguments: must be in increasing order.');
expect(() => song.verses(8, 1)).toThrow(expected);
});
it("throws when given a negative argument", function() {
const expected = new RangeError('Invalid arguments: cannot be negative.');
expect(() => song.verses(-1)).toThrow(expected);
});
it("throws when given arguments containing a negative value", function() {
const expected = new RangeError('Invalid arguments: cannot be negative.');
expect(() => song.verses(-8, 1)).toThrow(expected);
});
});