Skip to content

Instantly share code, notes, and snippets.

@peerreynders
Last active March 8, 2023 23:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save peerreynders/146d5a2c7d6adf5358580da5054c76b4 to your computer and use it in GitHub Desktop.
Save peerreynders/146d5a2c7d6adf5358580da5054c76b4 to your computer and use it in GitHub Desktop.
Gilded Rose Kata—Look-up approach in TypeScript

Gilded Rose Kata—Look-up approach in TypeScript

Inspired by “it's a table driven problem, it's not an object oriented problem, it's not even a control flow problem”.


/*
 * RUN Script
 */
tests(updateQualityBefore);
[LOG]: "tests DONE" 

/*
 * RUN Script
 */
tests(updateQualityAfter);
[LOG]: "tests DONE" 

/*
 * RUN Script
 */
demo(updateQualityBefore);
[LOG]: "-------- day 0 --------" 
[LOG]: "name, sellIn, quality" 
[LOG]: "+5 Dexterity Vest 10 20" 
[LOG]: "Aged Brie 2 0" 
[LOG]: "Elixir of the Mongoose 5 7" 
[LOG]: "Sulfuras, Hand of Ragnaros 0 80" 
[LOG]: "Sulfuras, Hand of Ragnaros -1 80" 
[LOG]: "Backstage passes to a TAFKAL80ETC concert 15 20" 
[LOG]: "Backstage passes to a TAFKAL80ETC concert 10 49" 
[LOG]: "Backstage passes to a TAFKAL80ETC concert 5 49" 
[LOG]: 
[LOG]: "-------- day 1 --------" 
[LOG]: "name, sellIn, quality" 
[LOG]: "+5 Dexterity Vest 9 19" 
[LOG]: "Aged Brie 1 1" 
[LOG]: "Elixir of the Mongoose 4 6" 
[LOG]: "Sulfuras, Hand of Ragnaros 0 80" 
[LOG]: "Sulfuras, Hand of Ragnaros -1 80" 
[LOG]: "Backstage passes to a TAFKAL80ETC concert 14 21" 
[LOG]: "Backstage passes to a TAFKAL80ETC concert 9 50" 
[LOG]: "Backstage passes to a TAFKAL80ETC concert 4 50" 

/*
 * RUN Script
 */
demo(updateQualityAfter);
[LOG]: "-------- day 0 --------" 
[LOG]: "name, sellIn, quality" 
[LOG]: "+5 Dexterity Vest 10 20" 
[LOG]: "Aged Brie 2 0" 
[LOG]: "Elixir of the Mongoose 5 7" 
[LOG]: "Sulfuras, Hand of Ragnaros 0 80" 
[LOG]: "Sulfuras, Hand of Ragnaros -1 80" 
[LOG]: "Backstage passes to a TAFKAL80ETC concert 15 20" 
[LOG]: "Backstage passes to a TAFKAL80ETC concert 10 49" 
[LOG]: "Backstage passes to a TAFKAL80ETC concert 5 49" 
[LOG]: "Conjured Mana Cake 3 6" 
[LOG]: 
[LOG]: "-------- day 1 --------" 
[LOG]: "name, sellIn, quality" 
[LOG]: "+5 Dexterity Vest 9 19" 
[LOG]: "Aged Brie 1 1" 
[LOG]: "Elixir of the Mongoose 4 6" 
[LOG]: "Sulfuras, Hand of Ragnaros 0 80" 
[LOG]: "Sulfuras, Hand of Ragnaros -1 80" 
[LOG]: "Backstage passes to a TAFKAL80ETC concert 14 21" 
[LOG]: "Backstage passes to a TAFKAL80ETC concert 9 50" 
[LOG]: "Backstage passes to a TAFKAL80ETC concert 4 50" 
[LOG]: "Conjured Mana Cake 2 4"  
// File: gilded-rose.ts
// For: https://gist.github.com/peerreynders/146d5a2c7d6adf5358580da5054c76b4
type Item = {
name: string;
sellIn: number;
quality: number;
};
function makeItem(name: string, sellIn: number, quality: number): Item {
return {
name,
sellIn,
quality,
};
}
type UpdateQuality = (item: Item) => void;
/*
* BEFORE implementation
*
* Original:
* https://github.com/emilybache/GildedRose-Refactoring-Kata/blob/main/TypeScript/app/gilded-rose.ts
*
*/
function updateQualityBefore(item: Item) {
if (
item.name != 'Aged Brie' &&
item.name != 'Backstage passes to a TAFKAL80ETC concert'
) {
if (item.quality > 0) {
if (item.name != 'Sulfuras, Hand of Ragnaros') {
item.quality = item.quality - 1;
}
}
} else {
if (item.quality < 50) {
item.quality = item.quality + 1;
if (item.name == 'Backstage passes to a TAFKAL80ETC concert') {
if (item.sellIn < 11) {
if (item.quality < 50) {
item.quality = item.quality + 1;
}
}
if (item.sellIn < 6) {
if (item.quality < 50) {
item.quality = item.quality + 1;
}
}
}
}
}
if (item.name != 'Sulfuras, Hand of Ragnaros') {
item.sellIn = item.sellIn - 1;
}
if (item.sellIn < 0) {
if (item.name != 'Aged Brie') {
if (item.name != 'Backstage passes to a TAFKAL80ETC concert') {
if (item.quality > 0) {
if (item.name != 'Sulfuras, Hand of Ragnaros') {
item.quality = item.quality - 1;
}
}
} else {
item.quality = item.quality - item.quality;
}
} else {
if (item.quality < 50) {
item.quality = item.quality + 1;
}
}
}
}
/*
* AFTER implementation
*
* Requirements:
* https://github.com/emilybache/GildedRose-Refactoring-Kata/blob/main/GildedRoseRequirements.txt
*
*/
type QualityAdjustmentFn = (item: Item) => number;
// - At the end of each day our system lowers both values for every item
// - Once the sell by date has passed, Quality degrades twice as fast
const qualityAdjustment: QualityAdjustmentFn = ({ sellIn }) =>
sellIn > 0 ? -1 : -2;
// - "Aged Brie" actually increases in Quality the older it gets
const brieAdjustment: QualityAdjustmentFn = (item) => -qualityAdjustment(item);
// - "Backstage passes", like aged brie,
// increases in Quality as its SellIn value approaches;
// Quality increases by 2 when there are 10 days or less and
// by 3 when there are 5 days or less but Quality
// drops to 0 after the concert
const passesAdjustment: QualityAdjustmentFn = (item) =>
item.sellIn > 10
? -qualityAdjustment(item)
: item.sellIn > 5
? -2 * qualityAdjustment(item)
: item.sellIn > 0
? -3 * qualityAdjustment(item)
: -item.quality;
// - "Conjured" items degrade in Quality twice as fast as normal items
const conjuredAdjustment: QualityAdjustmentFn = (item) =>
2 * qualityAdjustment(item);
const clamp = (min: number, max: number, value: number) =>
value > max ? max : value < min ? min : value;
// - At the end of each day our system lowers both values for every item
// - The Quality of an item is never negative
// - The Quality of an item is never more than 50
function updateItem(item: Item, fn: QualityAdjustmentFn) {
const quality = clamp(0, 50, item.quality + fn(item));
item.sellIn -= 1;
item.quality = quality;
}
type ConfigEntry = [name: string, update: UpdateQuality];
function selectUpdate(itemName: string, config: ConfigEntry[]) {
for (let i = 1; i < config.length; i += 1) {
const [name, update] = config[i];
if (itemName.startsWith(name)) return update;
}
return config[0][1];
}
const NAME_BRIE = 'Aged Brie';
const NAME_SULFARAS = 'Sulfuras';
const NAME_PASSES = 'Backstage passes';
const NAME_CONJURED = 'Conjured';
const config2: ConfigEntry[] = [
['', (item: Item) => updateItem(item, qualityAdjustment)],
[NAME_BRIE, (item: Item) => updateItem(item, brieAdjustment)],
[NAME_SULFARAS, (item: Item) => void 0],
[NAME_PASSES, (item: Item) => updateItem(item, passesAdjustment)],
[NAME_CONJURED, (item: Item) => updateItem(item, conjuredAdjustment)],
];
const updateQualityAfter = (item: Item) =>
selectUpdate(item.name, config2)(item);
/*
* TESTS
*/
function tests(updateQuality: UpdateQuality) {
// NOTE:
// Assertion failures show in the
// Developer Tools Console NOT the `Logs` panel
const assert = console.assert;
const isExpected = (item: Item, sellIn: number, quality: number) =>
item.sellIn === sellIn && item.quality === quality;
const test01 = () => {
const title = 'Both sellIn and quality are lowered';
const item = makeItem('Elixir of the Mongoose', 5, 7);
updateQuality(item);
assert(
isExpected(item, 4, 6),
`${title}: sellIn ${item.sellIn} and/or quality ${item.quality} don't match lower values`
);
};
test01();
const test02 = () => {
const title = 'Quality degrades twice as fast once past sellIn';
const item = makeItem('Elixir of the Mongoose', 0, 7);
updateQuality(item);
assert(
isExpected(item, -1, 5),
`${title}: quality ${item.quality} doesn't match degraded value`
);
};
test02();
const test03 = () => {
const title = 'Quality is never negative';
const item = makeItem('Elixir of the Mongoose', 5, 0);
updateQuality(item);
assert(
isExpected(item, 4, 0),
`${title}: quality ${item.quality} isn't zero`
);
};
test03();
const test04 = () => {
const title = 'Aged Brie increases in quality';
const item = makeItem('Aged Brie', 2, 0);
updateQuality(item);
assert(
isExpected(item, 1, 1),
`${title}: quality ${item.quality} didn't increase`
);
};
test04();
const test05 = () => {
const title = 'Quality never exceeds 50';
const item = makeItem('Aged Brie', 2, 50);
updateQuality(item);
assert(
isExpected(item, 1, 50),
`${title}: quality ${item.quality} exceeds maximum`
);
};
test05();
const test05A = () => {
const title = 'Aged Brie quality increase doubles past sellIn';
const item = makeItem('Aged Brie', 0, 20);
updateQuality(item);
assert(
isExpected(item, -1, 22),
`${title}: quality ${item.quality} doesn't match double increase`
);
};
test05A();
const test06 = () => {
const title = `Sulfaras doesn't change`;
const item = makeItem('Sulfuras, Hand of Ragnaros', -1, 80);
updateQuality(item);
assert(isExpected(item, -1, 80), `${title}: item unexpectedly changed`);
};
test06();
const test07 = () => {
const title = 'Backstages passes increase in quality (> 10 days)';
const item = makeItem('Backstage passes to a TAFKAL80ETC concert', 11, 20);
updateQuality(item);
assert(
isExpected(item, 10, 21),
`${title}: quality ${item.quality} didn't increase`
);
};
test07();
const test08 = () => {
const title = 'Backstages passes increase by 2 in quality (11 > days > 5)';
const item1 = makeItem('Backstage passes to a TAFKAL80ETC concert', 10, 20);
const item2 = makeItem('Backstage passes to a TAFKAL80ETC concert', 6, 30);
updateQuality(item1);
updateQuality(item2);
assert(
isExpected(item1, 9, 22),
`${title}: quality ${item1.quality} didn't increase by 2`
);
assert(
isExpected(item2, 5, 32),
`${title}: quality ${item2.quality} didn't increase by 2`
);
};
test08();
const test09 = () => {
const title = 'Backstages passes increase by 3 in quality (6 > days > -1)';
const item1 = makeItem('Backstage passes to a TAFKAL80ETC concert', 5, 20);
const item2 = makeItem('Backstage passes to a TAFKAL80ETC concert', 1, 30);
updateQuality(item1);
updateQuality(item2);
assert(
isExpected(item1, 4, 23),
`${title}: quality ${item1.quality} didn't increase by 3`
);
assert(
isExpected(item2, 0, 33),
`${title}: quality ${item2.quality} didn't increase by 3`
);
};
test09();
const test10 = () => {
const title = 'Backstages passes quality drops to zero';
const item = makeItem('Backstage passes to a TAFKAL80ETC concert', 0, 30);
updateQuality(item);
assert(
isExpected(item, -1, 0),
`${title}: quality ${item.quality} didn't drop to zero`
);
};
test10();
if (updateQuality === updateQualityAfter) {
const test11 = () => {
const title = 'Conjured quality degrades twice as fast BEFORE sellIn';
const item = makeItem('Conjured Mana Cake', 3, 6);
updateQuality(item);
assert(
isExpected(item, 2, 4),
`${title}: quality ${item.quality} doesn't match twice degraded value`
);
};
test11();
const test12 = () => {
const title = 'Conjured quality degrades twice as fast AFTER sellIn';
const item = makeItem('Conjured Mana Cake', 0, 6);
updateQuality(item);
assert(
isExpected(item, -1, 2),
`${title}: quality ${item.quality} doesn't match twice degraded value`
);
};
test12();
}
console.log('tests DONE');
}
/*
* DEMO
*
* Original:
* https://github.com/emilybache/GildedRose-Refactoring-Kata/blob/main/TypeScript/test/golden-master-text-test.ts
*/
function demo(updateQuality: UpdateQuality) {
const data: [string, number, number][] = [
['+5 Dexterity Vest', 10, 20],
['Aged Brie', 2, 0],
['Elixir of the Mongoose', 5, 7],
['Sulfuras, Hand of Ragnaros', 0, 80],
['Sulfuras, Hand of Ragnaros', -1, 80],
['Backstage passes to a TAFKAL80ETC concert', 15, 20],
['Backstage passes to a TAFKAL80ETC concert', 10, 49],
['Backstage passes to a TAFKAL80ETC concert', 5, 49],
];
if (updateQuality === updateQualityAfter) {
data.push(['Conjured Mana Cake', 3, 6]);
}
const items = data.map(([name, sellIn, quality]) =>
makeItem(name, sellIn, quality)
);
const log = console.log;
const days = 2;
const displayItem = ({ name, sellIn, quality }: Item) =>
log(`${name} ${sellIn} ${quality}`);
for (let i = days; i > 0; i -= 1) {
log(`-------- day ${days - i} --------`);
log(`name, sellIn, quality`);
items.forEach(displayItem);
log();
items.forEach(updateQuality);
}
}
/*
* RUN Script
*/
// tests(updateQualityBefore);
tests(updateQualityAfter);
// demo(updateQualityBefore);
// demo(updateQualityAfter);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment