diff --git a/csharp.xUnit/GildedRose/AgedBrieUpdater.cs b/csharp.xUnit/GildedRose/AgedBrieUpdater.cs new file mode 100644 index 00000000..955f2d96 --- /dev/null +++ b/csharp.xUnit/GildedRose/AgedBrieUpdater.cs @@ -0,0 +1,16 @@ +namespace GildedRoseKata; + +public class AgedBrieUpdater : ItemUpdaterBase +{ + public override string Name => "Aged Brie"; + + public override void Update(Item item) + { + item.Quality = ClampQuality(item.Quality + 1); + item.SellIn -= 1; + if (item.SellIn < 0) + { + item.Quality = ClampQuality(item.Quality + 1); + } + } +} diff --git a/csharp.xUnit/GildedRose/BackstagePassUpdater.cs b/csharp.xUnit/GildedRose/BackstagePassUpdater.cs new file mode 100644 index 00000000..c1c1f4af --- /dev/null +++ b/csharp.xUnit/GildedRose/BackstagePassUpdater.cs @@ -0,0 +1,17 @@ +namespace GildedRoseKata; + +public class BackstagePassUpdater : ItemUpdaterBase +{ + public override string Name => "Backstage passes to a TAFKAL80ETC concert"; + + public override void Update(Item item) + { + var increase = item.SellIn <= 5 ? 3 : item.SellIn <= 10 ? 2 : 1; + item.Quality = ClampQuality(item.Quality + increase); + item.SellIn -= 1; + if (item.SellIn < 0) + { + item.Quality = 0; + } + } +} diff --git a/csharp.xUnit/GildedRose/ConjuredItemUpdater.cs b/csharp.xUnit/GildedRose/ConjuredItemUpdater.cs new file mode 100644 index 00000000..8d6499ef --- /dev/null +++ b/csharp.xUnit/GildedRose/ConjuredItemUpdater.cs @@ -0,0 +1,17 @@ +namespace GildedRoseKata; + +public class ConjuredItemUpdater : ItemUpdaterBase +{ + // Matched by prefix in ItemUpdaterFactory: handles every "Conjured..." item. + public override string Name => ItemUpdaterFactory.ConjuredPrefix; + + public override void Update(Item item) + { + item.Quality = ClampQuality(item.Quality - 2); + item.SellIn -= 1; + if (item.SellIn < 0) + { + item.Quality = ClampQuality(item.Quality - 2); + } + } +} diff --git a/csharp.xUnit/GildedRose/GildedRose.cs b/csharp.xUnit/GildedRose/GildedRose.cs index c08bf5f8..9b37db99 100644 --- a/csharp.xUnit/GildedRose/GildedRose.cs +++ b/csharp.xUnit/GildedRose/GildedRose.cs @@ -1,89 +1,25 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace GildedRoseKata; public class GildedRose { - IList Items; + private readonly IList _items; + private readonly IItemUpdaterFactory _factory; - public GildedRose(IList Items) + public GildedRose(IList items) : this(items, ItemUpdaterFactory.Default()) { - this.Items = Items; + } + + public GildedRose(IList items, IItemUpdaterFactory factory) + { + _items = items; + _factory = factory; } public void UpdateQuality() { - for (var i = 0; i < Items.Count; i++) - { - if (Items[i].Name != "Aged Brie" && Items[i].Name != "Backstage passes to a TAFKAL80ETC concert") - { - if (Items[i].Quality > 0) - { - if (Items[i].Name != "Sulfuras, Hand of Ragnaros") - { - Items[i].Quality = Items[i].Quality - 1; - } - } - } - else - { - if (Items[i].Quality < 50) - { - Items[i].Quality = Items[i].Quality + 1; - - if (Items[i].Name == "Backstage passes to a TAFKAL80ETC concert") - { - if (Items[i].SellIn < 11) - { - if (Items[i].Quality < 50) - { - Items[i].Quality = Items[i].Quality + 1; - } - } - - if (Items[i].SellIn < 6) - { - if (Items[i].Quality < 50) - { - Items[i].Quality = Items[i].Quality + 1; - } - } - } - } - } - - if (Items[i].Name != "Sulfuras, Hand of Ragnaros") - { - Items[i].SellIn = Items[i].SellIn - 1; - } - - if (Items[i].SellIn < 0) - { - if (Items[i].Name != "Aged Brie") - { - if (Items[i].Name != "Backstage passes to a TAFKAL80ETC concert") - { - if (Items[i].Quality > 0) - { - if (Items[i].Name != "Sulfuras, Hand of Ragnaros") - { - Items[i].Quality = Items[i].Quality - 1; - } - } - } - else - { - Items[i].Quality = Items[i].Quality - Items[i].Quality; - } - } - else - { - if (Items[i].Quality < 50) - { - Items[i].Quality = Items[i].Quality + 1; - } - } - } - } + foreach (var item in _items) + _factory.Get(item).Update(item); } -} \ No newline at end of file +} diff --git a/csharp.xUnit/GildedRose/IItemUpdater.cs b/csharp.xUnit/GildedRose/IItemUpdater.cs new file mode 100644 index 00000000..c7a0a823 --- /dev/null +++ b/csharp.xUnit/GildedRose/IItemUpdater.cs @@ -0,0 +1,10 @@ +namespace GildedRoseKata; + +public interface IItemUpdater +{ + /// The exact item name this strategy handles ("default" for the fallback). + string Name { get; } + + /// Applies one full day: quality change + SellIn change + clamping. + void Update(Item item); +} diff --git a/csharp.xUnit/GildedRose/IItemUpdaterFactory.cs b/csharp.xUnit/GildedRose/IItemUpdaterFactory.cs new file mode 100644 index 00000000..66855fe6 --- /dev/null +++ b/csharp.xUnit/GildedRose/IItemUpdaterFactory.cs @@ -0,0 +1,6 @@ +namespace GildedRoseKata; + +public interface IItemUpdaterFactory +{ + IItemUpdater Get(Item item); +} diff --git a/csharp.xUnit/GildedRose/ItemUpdaterBase.cs b/csharp.xUnit/GildedRose/ItemUpdaterBase.cs new file mode 100644 index 00000000..a973b8d4 --- /dev/null +++ b/csharp.xUnit/GildedRose/ItemUpdaterBase.cs @@ -0,0 +1,15 @@ +using System; + +namespace GildedRoseKata; + +public abstract class ItemUpdaterBase : IItemUpdater +{ + public const int MinQuality = 0; + public const int MaxQuality = 50; + + public abstract string Name { get; } + + public abstract void Update(Item item); + + protected static int ClampQuality(int quality) => Math.Clamp(quality, MinQuality, MaxQuality); +} diff --git a/csharp.xUnit/GildedRose/ItemUpdaterFactory.cs b/csharp.xUnit/GildedRose/ItemUpdaterFactory.cs new file mode 100644 index 00000000..02968064 --- /dev/null +++ b/csharp.xUnit/GildedRose/ItemUpdaterFactory.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; + +namespace GildedRoseKata; + +public class ItemUpdaterFactory : IItemUpdaterFactory +{ + public const string ConjuredPrefix = "Conjured"; + private const string DefaultName = "default"; + + private readonly Dictionary _updatersByName; + private readonly IItemUpdater _fallback; + + public ItemUpdaterFactory(IEnumerable updaters) + { + _updatersByName = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var updater in updaters) + { + _updatersByName[updater.Name] = updater; + } + + if (!_updatersByName.TryGetValue(DefaultName, out _fallback)) + { + throw new ArgumentException($"A \"{DefaultName}\" fallback updater is required.", nameof(updaters)); + } + } + + public static ItemUpdaterFactory Default() => new(new IItemUpdater[] + { + new NormalItemUpdater(), + new AgedBrieUpdater(), + new SulfurasUpdater(), + new BackstagePassUpdater(), + new ConjuredItemUpdater(), + }); + + public IItemUpdater Get(Item item) + { + if (_updatersByName.TryGetValue(item.Name, out var updater)) + { + return updater; + } + + if (item.Name.StartsWith(ConjuredPrefix, StringComparison.OrdinalIgnoreCase) && + _updatersByName.TryGetValue(ConjuredPrefix, out var conjured)) + { + return conjured; + } + + return _fallback; + } +} diff --git a/csharp.xUnit/GildedRose/NormalItemUpdater.cs b/csharp.xUnit/GildedRose/NormalItemUpdater.cs new file mode 100644 index 00000000..5f35bf3a --- /dev/null +++ b/csharp.xUnit/GildedRose/NormalItemUpdater.cs @@ -0,0 +1,16 @@ +namespace GildedRoseKata; + +public class NormalItemUpdater : ItemUpdaterBase +{ + public override string Name => "default"; + + public override void Update(Item item) + { + item.Quality = ClampQuality(item.Quality - 1); + item.SellIn -= 1; + if (item.SellIn < 0) + { + item.Quality = ClampQuality(item.Quality - 1); + } + } +} diff --git a/csharp.xUnit/GildedRose/SulfurasUpdater.cs b/csharp.xUnit/GildedRose/SulfurasUpdater.cs new file mode 100644 index 00000000..66ee371e --- /dev/null +++ b/csharp.xUnit/GildedRose/SulfurasUpdater.cs @@ -0,0 +1,11 @@ +namespace GildedRoseKata; + +public class SulfurasUpdater : ItemUpdaterBase +{ + public override string Name => "Sulfuras, Hand of Ragnaros"; + + public override void Update(Item item) + { + // Legendary item: Quality stays at 80 and SellIn never changes. + } +} diff --git a/csharp.xUnit/GildedRoseTests/ApprovalTest.Foo.verified.txt b/csharp.xUnit/GildedRoseTests/ApprovalTest.Foo.verified.txt new file mode 100644 index 00000000..c79d294c --- /dev/null +++ b/csharp.xUnit/GildedRoseTests/ApprovalTest.Foo.verified.txt @@ -0,0 +1,6 @@ +[ + { + Name: foo, + SellIn: -1 + } +] \ No newline at end of file diff --git a/csharp.xUnit/GildedRoseTests/ApprovalTest.ThirtyDays.verified.txt b/csharp.xUnit/GildedRoseTests/ApprovalTest.ThirtyDays.verified.txt index cd66984f..f80af4e4 100644 --- a/csharp.xUnit/GildedRoseTests/ApprovalTest.ThirtyDays.verified.txt +++ b/csharp.xUnit/GildedRoseTests/ApprovalTest.ThirtyDays.verified.txt @@ -21,7 +21,7 @@ Sulfuras, Hand of Ragnaros, -1, 80 Backstage passes to a TAFKAL80ETC concert, 14, 21 Backstage passes to a TAFKAL80ETC concert, 9, 50 Backstage passes to a TAFKAL80ETC concert, 4, 50 -Conjured Mana Cake, 2, 5 +Conjured Mana Cake, 2, 4 -------- day 2 -------- name, sellIn, quality @@ -33,7 +33,7 @@ Sulfuras, Hand of Ragnaros, -1, 80 Backstage passes to a TAFKAL80ETC concert, 13, 22 Backstage passes to a TAFKAL80ETC concert, 8, 50 Backstage passes to a TAFKAL80ETC concert, 3, 50 -Conjured Mana Cake, 1, 4 +Conjured Mana Cake, 1, 2 -------- day 3 -------- name, sellIn, quality @@ -45,7 +45,7 @@ Sulfuras, Hand of Ragnaros, -1, 80 Backstage passes to a TAFKAL80ETC concert, 12, 23 Backstage passes to a TAFKAL80ETC concert, 7, 50 Backstage passes to a TAFKAL80ETC concert, 2, 50 -Conjured Mana Cake, 0, 3 +Conjured Mana Cake, 0, 0 -------- day 4 -------- name, sellIn, quality @@ -57,7 +57,7 @@ Sulfuras, Hand of Ragnaros, -1, 80 Backstage passes to a TAFKAL80ETC concert, 11, 24 Backstage passes to a TAFKAL80ETC concert, 6, 50 Backstage passes to a TAFKAL80ETC concert, 1, 50 -Conjured Mana Cake, -1, 1 +Conjured Mana Cake, -1, 0 -------- day 5 -------- name, sellIn, quality diff --git a/csharp.xUnit/GildedRoseTests/GildedRoseTest.cs b/csharp.xUnit/GildedRoseTests/GildedRoseTest.cs index 63fd7b1b..2f4ae367 100644 --- a/csharp.xUnit/GildedRoseTests/GildedRoseTest.cs +++ b/csharp.xUnit/GildedRoseTests/GildedRoseTest.cs @@ -1,4 +1,4 @@ -using Xunit; +using Xunit; using System.Collections.Generic; using GildedRoseKata; @@ -6,12 +6,193 @@ namespace GildedRoseTests; public class GildedRoseTest { - [Fact] - public void foo() + private const string AgedBrie = "Aged Brie"; + private const string Sulfuras = "Sulfuras, Hand of Ragnaros"; + private const string BackstagePass = "Backstage passes to a TAFKAL80ETC concert"; + + private static Item UpdateOne(string name, int sellIn, int quality) { - IList Items = new List { new Item { Name = "foo", SellIn = 0, Quality = 0 } }; - GildedRose app = new GildedRose(Items); + var item = new Item { Name = name, SellIn = sellIn, Quality = quality }; + var app = new GildedRose(new List { item }); app.UpdateQuality(); - Assert.Equal("fixme", Items[0].Name); + return item; } -} \ No newline at end of file + + [Fact] + public void UpdateQuality_DoesNotChangeItemName() + { + var item = UpdateOne("foo", 0, 0); + Assert.Equal("foo", item.Name); + } + + // ---------- Normal items ---------- + + [Fact] + public void NormalItem_BeforeSellBy_QualityDropsByOne() + { + var item = UpdateOne("+5 Dexterity Vest", 10, 20); + Assert.Equal(9, item.SellIn); + Assert.Equal(19, item.Quality); + } + + [Fact] + public void NormalItem_OnSellByDate_QualityDropsByTwo() + { + var item = UpdateOne("+5 Dexterity Vest", 0, 20); + Assert.Equal(-1, item.SellIn); + Assert.Equal(18, item.Quality); + } + + [Fact] + public void NormalItem_AfterSellBy_QualityDropsByTwo() + { + var item = UpdateOne("+5 Dexterity Vest", -1, 20); + Assert.Equal(-2, item.SellIn); + Assert.Equal(18, item.Quality); + } + + [Fact] + public void NormalItem_QualityNeverNegative() + { + var item = UpdateOne("+5 Dexterity Vest", 5, 0); + Assert.Equal(0, item.Quality); + } + + [Fact] + public void NormalItem_AfterSellByWithQualityOne_QualityStopsAtZero() + { + var item = UpdateOne("+5 Dexterity Vest", 0, 1); + Assert.Equal(0, item.Quality); + } + + // ---------- Aged Brie ---------- + + [Fact] + public void AgedBrie_BeforeSellBy_QualityIncreasesByOne() + { + var item = UpdateOne(AgedBrie, 2, 0); + Assert.Equal(1, item.SellIn); + Assert.Equal(1, item.Quality); + } + + [Fact] + public void AgedBrie_AfterSellBy_QualityIncreasesByTwo() + { + var item = UpdateOne(AgedBrie, 0, 10); + Assert.Equal(-1, item.SellIn); + Assert.Equal(12, item.Quality); + } + + [Fact] + public void AgedBrie_QualityNeverExceedsFifty() + { + var item = UpdateOne(AgedBrie, 5, 50); + Assert.Equal(50, item.Quality); + } + + [Fact] + public void AgedBrie_AfterSellByAtFortyNine_QualityCapsAtFifty() + { + var item = UpdateOne(AgedBrie, 0, 49); + Assert.Equal(50, item.Quality); + } + + // ---------- Sulfuras ---------- + + [Fact] + public void Sulfuras_AtSellInZero_NeverChanges() + { + var item = UpdateOne(Sulfuras, 0, 80); + Assert.Equal(0, item.SellIn); + Assert.Equal(80, item.Quality); + } + + [Fact] + public void Sulfuras_AtSellInMinusOne_NeverChanges() + { + var item = UpdateOne(Sulfuras, -1, 80); + Assert.Equal(-1, item.SellIn); + Assert.Equal(80, item.Quality); + } + + // ---------- Backstage passes ---------- + + [Fact] + public void BackstagePass_MoreThanTenDays_QualityIncreasesByOne() + { + var item = UpdateOne(BackstagePass, 11, 20); + Assert.Equal(10, item.SellIn); + Assert.Equal(21, item.Quality); + } + + [Fact] + public void BackstagePass_TenDaysOrLess_QualityIncreasesByTwo() + { + var item = UpdateOne(BackstagePass, 10, 20); + Assert.Equal(9, item.SellIn); + Assert.Equal(22, item.Quality); + } + + [Fact] + public void BackstagePass_SixDays_QualityIncreasesByTwo() + { + var item = UpdateOne(BackstagePass, 6, 20); + Assert.Equal(22, item.Quality); + } + + [Fact] + public void BackstagePass_FiveDaysOrLess_QualityIncreasesByThree() + { + var item = UpdateOne(BackstagePass, 5, 20); + Assert.Equal(4, item.SellIn); + Assert.Equal(23, item.Quality); + } + + [Fact] + public void BackstagePass_AfterConcert_QualityDropsToZero() + { + var item = UpdateOne(BackstagePass, 0, 20); + Assert.Equal(-1, item.SellIn); + Assert.Equal(0, item.Quality); + } + + [Fact] + public void BackstagePass_AtFortyNineCloseToConcert_QualityCapsAtFifty() + { + var item = UpdateOne(BackstagePass, 5, 49); + Assert.Equal(50, item.Quality); + } + + // ---------- Conjured items ---------- + + [Fact] + public void ConjuredItem_BeforeSellBy_QualityDropsByTwo() + { + var item = UpdateOne("Conjured Mana Cake", 3, 6); + Assert.Equal(2, item.SellIn); + Assert.Equal(4, item.Quality); + } + + [Fact] + public void ConjuredItem_OnSellByDate_QualityDropsByFour() + { + var item = UpdateOne("Conjured Mana Cake", 0, 6); + Assert.Equal(-1, item.SellIn); + Assert.Equal(2, item.Quality); + } + + [Fact] + public void ConjuredItem_AfterSellBy_QualityDropsByFour() + { + var item = UpdateOne("Conjured Mana Cake", -1, 6); + Assert.Equal(-2, item.SellIn); + Assert.Equal(2, item.Quality); + } + + [Fact] + public void ConjuredItem_QualityNeverNegative() + { + var item = UpdateOne("Conjured Mana Cake", 0, 3); + Assert.Equal(0, item.Quality); + } +}