La Rose Dorée (Gilded Rose) - Solution

On se retrouve aujourd'hui pour la solution du précédent #KataOfTheWeek proposé par Jordan en début de semaine !

Voici la solution proposée en Kotlin:

data class Item(var name: String, var sellIn: Int, var quality: Int)

enum class ItemTypes(val itemName: String) {
    AGED_BRIE("Aged Brie"),
    BACKSTAGE_PASSES("Backstage passes to a TAFKAL80ETC concert"),
    SULFURAS("Sulfuras, Hand of Ragnaros"),
    CONJURED("Conjured Mana Cake")
}

// In a file ItemUtils.kt
val QUALITY_RANGE = 0..50

val itemUpdaters: HashMap<String, ItemUpdater> = hashMapOf(
    ItemTypes.BACKSTAGE_PASSES.itemName to BackstagePassesItemUpdater(),
    ItemTypes.AGED_BRIE.itemName to AgedBrieItemUpdater(),
    ItemTypes.CONJURED.itemName to ConjuredItemUpdater(),
    ItemTypes.SULFURAS.itemName to ImmutableItemUpdater()
)


fun Item.deteriorateQuality() {
    when {
        sellIn < 0 -> quality -= 2
        quality in QUALITY_RANGE -> quality -= 1
    }
    quality = quality.coerceIn(QUALITY_RANGE)
}

fun Item.decrementSellIn() {
    sellIn -= 1
}

fun Item.upgradeQuality() {
    when {
        sellIn < 0 -> quality = 0
        sellIn < 5 -> quality += 3
        sellIn < 10 -> quality += 2
        quality in QUALITY_RANGE -> quality += 1
    }
    quality = quality.coerceIn(QUALITY_RANGE)
}

fun Item.upgradeAgedBrieQuality() {
    when {
        sellIn < 0 -> quality += 2
        quality in QUALITY_RANGE -> quality += 1
    }
    quality = quality.coerceIn(QUALITY_RANGE)
}


class GildedRose(
        var items: Array<Item>,
        private val updaters: ItemUpdaters = ItemUpdaters(itemUpdaters)) {

    fun updateQuality() {
        items.forEach { item -> updaters.updateItem(item) }
    }

}

class ItemUpdaters(
        private val map: HashMap<String, ItemUpdater>) {

    fun updateItem(item: Item) = item.getUpdater().update(item)

    private fun Item.getUpdater(): ItemUpdater {
        return map[this.name] ?: DeterioratingItemUpdater()
    }
}

interface ItemUpdater {

    fun update(item: Item) = Unit
}

abstract class BaseItemUpdater : ItemUpdater {

    override fun update(item: Item) {
        item.decrementSellIn()
    }
}

class ImmutableItemUpdater : BaseItemUpdater() {

    override fun update(item: Item) {
        // nothing is done because immutable Duh !
    }
}

open class DeterioratingItemUpdater : BaseItemUpdater() {

    override fun update(item: Item) {
        super.update(item)
        item.deteriorateQuality()
    }
}

class ConjuredItemUpdater : DeterioratingItemUpdater() {

    override fun update(item: Item) {
        super.update(item)
        item.deteriorateQuality()
    }
}

class BackstagePassesItemUpdater : BaseItemUpdater() {

    override fun update(item: Item) {
        super.update(item)
        item.upgradeQuality()
    }
}

class AgedBrieItemUpdater : BaseItemUpdater() {

    override fun update(item: Item) {
        super.update(item)
        item.upgradeAgedBrieQuality()
    }
}

// And some tests

abstract class BaseTest {

    protected lateinit var items: List<Item>

    @Before
    fun setUpItems() {
        items = listOf(
            Item("+5 Dexterity Vest", 10, 20),
            Item("Aged Brie", 2, 0),
            Item("Elixir of the Mongoose", 5, 7),
            Item("Sulfuras, Hand of Ragnaros", 0, 80),
            Item("Sulfuras, Hand of Ragnaros", -1, 80),
            Item("Backstage passes to a TAFKAL80ETC concert", 15, 20),
            Item("Backstage passes to a TAFKAL80ETC concert", 10, 49),
            Item("Backstage passes to a TAFKAL80ETC concert", 5, 49),
            Item("Conjured Mana Cake", 3, 6))
    }

    fun IntRange.randomize() = this.shuffled().last()

    fun generateRandomNumber() = (0 until 999).randomize()

    fun pickRandomItem(items: List<Item>) = items[items.indices.randomize()]

    fun generateRandomQualityValue() = (0..50).randomize()

    fun GildedRose.advanceTimeBy(days: Int) = repeat(days) { this.updateQuality() }

    fun List<Item>.excludeSulfuras() = this.filter { it.name != "Sulfuras, Hand of Ragnaros" }

    fun List<Item>.excludeBackstagePasses() = this.filter { it.name != "Backstage passes to a TAFKAL80ETC concert" }

    fun List<Item>.excludeAgedBrie() = this.filter { it.name != "Aged Brie" }
}

class QualityTest : BaseTest() {

    @Test
    fun givenSulfuras_afterAnyNumberOfDays_shouldHaveQualityEighty() {
        val days = generateRandomNumber()
        val item = Item("Sulfuras, Hand of Ragnaros", generateRandomNumber(), 80)
        val app = GildedRose(arrayOf(item))

        app.advanceTimeBy(days)

        assertThat(item.quality).isEqualTo(80)
    }

    @Test
    fun givenAnyItemExceptSulfuras_afterAnyNumberOfDays_shouldHaveQualityGreaterThanOrEqualToZero() {
        items = items.excludeSulfuras()
        val days = generateRandomNumber()
        val app = GildedRose(items.toTypedArray())

        app.advanceTimeBy(days)

        items.forEach { item ->
            assertThat(item.quality).isGreaterThanOrEqualTo(0)
        }
    }

    @Test
    fun givenAnyItemExceptSulfuras_afterAnyNumberOfDays_shouldHaveQualityLessThanOrEqualToFifty() {
        items = items.excludeSulfuras()
        val days = generateRandomNumber()
        val app = GildedRose(items.toTypedArray())

        app.advanceTimeBy(days)

        items.forEach { item ->
            assertThat(item.quality).isLessThanOrEqualTo(50)
        }
    }

    @Test
    fun givenAnyDegradableItem_whenSellInIsGreaterThanOrEqualToZero_shouldDegradeQualityByNumberOfDays() {
        items = items.excludeSulfuras()
                .excludeBackstagePasses()
                .excludeAgedBrie()
        val days = generateRandomNumber().coerceAtLeast(0)
        val item = pickRandomItem(items)
        val initialItemQuality = item.quality
        val app = GildedRose(arrayOf(item))

        app.advanceTimeBy(days)

        val expectedQuality = (initialItemQuality - days).coerceAtLeast(0)
        assertThat(item.quality).isEqualTo(expectedQuality)
    }

    @Test
    fun givenAnyDegradableItem_whenSellInIsLessThanZero_shouldDegradeQualityByTwiceTheNumberOfDays() {
        items = items.excludeSulfuras()
                .excludeBackstagePasses()
                .excludeAgedBrie()
        val sellIn = (-generateRandomNumber() until 0).randomize()
        val days = (0 until 100).randomize()
        val item = pickRandomItem(items).copy(sellIn = sellIn)
        val initialItemQuality = item.quality
        val app = GildedRose(arrayOf(item))

        app.advanceTimeBy(days)

        val expectedQuality = (initialItemQuality - days * 2).coerceAtLeast(0)
        assertThat(item.quality).isEqualTo(expectedQuality)
    }

    @Test
    fun givenAgedBrie_whenSellInGreaterOrEqualToZero_shouldIncreaseQualityByNumberOfDays() {
        for (sellIn in 0 until 1000) {
            val days = (0..sellIn).randomize()
            val initialItemQuality = generateRandomQualityValue()
            val item = Item("Aged Brie", sellIn, initialItemQuality)
            val app = GildedRose(arrayOf(item))

            app.advanceTimeBy(days)

            val expectedQuality = (initialItemQuality + days).coerceAtMost(50)
            assertThat(item.quality).isEqualTo(expectedQuality)
        }
    }

    @Test
    fun givenAgedBrie_whenSellInNegative_shouldIncreaseQualityByNumberOfDaysMultipliedByTwo() {
        for (sellIn in -100 until 0) {
            val days = (0..-sellIn).randomize()
            val initialItemQuality = generateRandomQualityValue()
            val item = Item("Aged Brie", sellIn, initialItemQuality)
            val app = GildedRose(arrayOf(item))

            app.advanceTimeBy(days)

            val expectedQuality = (initialItemQuality + days * 2).coerceAtMost(50)
            assertThat(item.quality).isEqualTo(expectedQuality)
        }
    }

    @Test
    fun givenBackstagePasses_whenSellInNegativeOrZero_shouldSetQualityToZero() {
        val days = generateRandomNumber()
        val sellIn = (-generateRandomNumber()..0).randomize()
        val item = Item("Backstage passes to a TAFKAL80ETC concert", sellIn, generateRandomQualityValue())
        val app = GildedRose(arrayOf(item))

        app.advanceTimeBy(days)

        assertThat(item.quality).isZero()
    }

    @Test
    fun givenBackstagePasses_whenSellInMoreThanZeroAndLessThanSix_shouldIncreaseQualityByNumberOfDaysMultipliedByThree() {
        for (sellIn in 1 until 6) {
            val days = (0..sellIn).randomize()
            val initialItemQuality = generateRandomQualityValue()
            val item = Item("Backstage passes to a TAFKAL80ETC concert", sellIn, initialItemQuality)
            val app = GildedRose(arrayOf(item))

            app.advanceTimeBy(days)

            val expectedQuality = (initialItemQuality + days * 3).coerceAtMost(50)
            assertThat(item.quality).isEqualTo(expectedQuality)
        }
    }

    @Test
    fun givenBackstagePasses_whenSellInMoreThanFiveAndLessThanEleven_shouldIncreaseQualityByNumberOfDaysMultipliedByTwo() {
        for (sellIn in 6 until 11) {
            val days = (0..sellIn - 6).randomize()
            val initialItemQuality = generateRandomQualityValue()
            val item = Item("Backstage passes to a TAFKAL80ETC concert", sellIn, initialItemQuality)
            val app = GildedRose(arrayOf(item))

            app.advanceTimeBy(days)

            val expectedQuality = (initialItemQuality + days * 2).coerceAtMost(50)
            assertThat(item.quality).isEqualTo(expectedQuality)
        }
    }

    @Test
    fun givenBackstagePasses_whenSellInMoreThanEleven_shouldIncreaseQualityByNumberOfDays() {
        for (sellIn in 11 until 999) {
            val days = (0..sellIn - 11).randomize()
            val initialItemQuality = generateRandomQualityValue()
            val item = Item("Backstage passes to a TAFKAL80ETC concert", sellIn, initialItemQuality)
            val app = GildedRose(arrayOf(item))

            app.advanceTimeBy(days)

            val expectedQuality = (initialItemQuality + days).coerceAtMost(50)
            assertThat(item.quality).isEqualTo(expectedQuality)
        }
    }

    @Test
    fun givenAnyItemExceptSulfuras_afterAnyNumberOfDays_shouldDecreaseSellInByNumberOfDays() {
        items = items.excludeSulfuras()
        val days = generateRandomNumber()
        val initialSellIn = generateRandomNumber()
        val item = pickRandomItem(items).copy(sellIn = initialSellIn)
        val app = GildedRose(arrayOf(item))

        app.advanceTimeBy(days)

        assertThat(item.sellIn).isEqualTo(initialSellIn - days)
    }

    @Test
    fun givenSulfuras_afterAnyNumberOfDays_shouldHaveSameSellIn() {
        val days = generateRandomNumber()
        val initialSellIn = generateRandomNumber()
        val item = Item("Sulfuras, Hand of Ragnaros", initialSellIn, generateRandomQualityValue())
        val app = GildedRose(arrayOf(item))

        app.advanceTimeBy(days)

        assertThat(item.sellIn).isEqualTo(initialSellIn)
    }
}

A bientôt pour un nouveau #KataOfTheWeek !

TakiVeille

TakiVeille