From 81332e2d2845908f356b2c3f06f25be184f8e190 Mon Sep 17 00:00:00 2001 From: Raj Vora Date: Tue, 5 May 2026 13:34:40 -0700 Subject: [PATCH] feat: refactor GildedRose with Strategy pattern, ItemRecord, and days injection --- python/gilded_rose.py | 81 ++++++++++++++++++++------------ python/tests/test_gilded_rose.py | 61 ++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 31 deletions(-) diff --git a/python/gilded_rose.py b/python/gilded_rose.py index 8f0c56e2..cf7e0d9a 100755 --- a/python/gilded_rose.py +++ b/python/gilded_rose.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from __future__ import annotations + from dataclasses import dataclass from typing import Protocol @@ -99,40 +101,57 @@ class ConjuredStrategy: item.quality = max(0, item.quality - 2) -class GildedRose(object): +_STRATEGY_MAP: dict[str, UpdateStrategy] = { + "Aged Brie": AgedBrieStrategy(), + "Backstage passes to a TAFKAL80ETC concert": BackstagePassStrategy(), + "Sulfuras, Hand of Ragnaros": SulfurasStrategy(), + "Conjured Mana Cake": ConjuredStrategy(), +} - def __init__(self, items): + +@dataclass +class ItemRecord: + """ + Internal pairing of an Item with its UpdateStrategy. + + Built once in GildedRose.__init__ so the name-to-strategy lookup + happens at construction time, not on every update call. Exposed + publicly so tests can inject strategies directly without going + through the name-lookup dict. + """ + + item: Item + strategy: UpdateStrategy + + +class GildedRose: + """ + Inventory manager for the Gilded Rose inn. + + Accepts a list of Item objects and updates their quality and sell_in + values according to each item's type-specific strategy. The Item + class and the items list are left untouched per the goblin's terms. + + Usage: + rose = GildedRose(items) + rose.update_quality() # advance one day + rose.update_quality(days=7) # advance a full week + """ + + def __init__(self, items: list[Item]) -> None: self.items = items + self._records: list[ItemRecord] = [ + ItemRecord( + item=i, + strategy=_STRATEGY_MAP.get(i.name, NormalStrategy()), + ) + for i in items + ] - def update_quality(self): - for item in self.items: - if item.name != "Aged Brie" and 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.sell_in < 11: - if item.quality < 50: - item.quality = item.quality + 1 - if item.sell_in < 6: - if item.quality < 50: - item.quality = item.quality + 1 - if item.name != "Sulfuras, Hand of Ragnaros": - item.sell_in = item.sell_in - 1 - if item.sell_in < 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 + def update_quality(self, days: int = 1) -> None: + """Advance quality and sell_in for all items by `days` days.""" + for record in self._records: + record.strategy.update(record.item, days) class Item: diff --git a/python/tests/test_gilded_rose.py b/python/tests/test_gilded_rose.py index 4354b594..98084276 100644 --- a/python/tests/test_gilded_rose.py +++ b/python/tests/test_gilded_rose.py @@ -10,6 +10,7 @@ if str(_EXERCISE_ROOT) not in sys.path: from gilded_rose import ( Item, GildedRose, NormalStrategy, AgedBrieStrategy, BackstagePassStrategy, SulfurasStrategy, ConjuredStrategy, + ItemRecord, ) @@ -207,5 +208,65 @@ class TestConjuredStrategy(unittest.TestCase): self.assertEqual(0, item.quality) +class TestGildedRose(unittest.TestCase): + """ + Integration tests for GildedRose. + + Verifies strategy dispatch by item name and that the days parameter + is forwarded correctly to each strategy. + """ + + def test_dispatches_normal_strategy_for_unknown_item(self): + items = [Item("unknown widget", sell_in=5, quality=10)] + GildedRose(items).update_quality() + self.assertEqual(9, items[0].quality) + self.assertEqual(4, items[0].sell_in) + + def test_dispatches_aged_brie_strategy(self): + items = [Item("Aged Brie", sell_in=5, quality=20)] + GildedRose(items).update_quality() + self.assertEqual(21, items[0].quality) + + def test_dispatches_backstage_pass_strategy(self): + items = [Item("Backstage passes to a TAFKAL80ETC concert", sell_in=5, quality=20)] + GildedRose(items).update_quality() + self.assertEqual(23, items[0].quality) + + def test_dispatches_sulfuras_strategy(self): + items = [Item("Sulfuras, Hand of Ragnaros", sell_in=0, quality=80)] + GildedRose(items).update_quality() + self.assertEqual(80, items[0].quality) + self.assertEqual(0, items[0].sell_in) + + def test_dispatches_conjured_strategy(self): + items = [Item("Conjured Mana Cake", sell_in=5, quality=20)] + GildedRose(items).update_quality() + self.assertEqual(18, items[0].quality) + + def test_multi_day_update_via_days_param(self): + # sell_in=3, quality=10, days=3 + # Day1: q=9 si=2 | Day2: q=8 si=1 | Day3: q=7 si=0 + items = [Item("unknown widget", sell_in=3, quality=10)] + GildedRose(items).update_quality(days=3) + self.assertEqual(7, items[0].quality) + self.assertEqual(0, items[0].sell_in) + + def test_multiple_items_updated_independently(self): + items = [ + Item("Aged Brie", sell_in=5, quality=20), + Item("unknown widget", sell_in=5, quality=20), + ] + GildedRose(items).update_quality() + self.assertEqual(21, items[0].quality) # Aged Brie goes up + self.assertEqual(19, items[1].quality) # normal goes down + + def test_item_record_allows_direct_strategy_injection(self): + # ItemRecord lets tests bypass name-lookup and inject a strategy directly + item = Item("anything", sell_in=5, quality=20) + record = ItemRecord(item=item, strategy=ConjuredStrategy()) + record.strategy.update(record.item, days=1) + self.assertEqual(18, item.quality) + + if __name__ == '__main__': unittest.main()