Kata: Conference tickets

Few weeks ago, with one of the other organizer of the Software Crafters Lyon, we had to setup the ticket office for Lyon Craft our conference in Lyon, next May.

It was a nightmare, the software we have is really rigid and we spent one hour to configure a two-days event.

Let's turn it into a code kata!

Rule 0: an event can have a maximum of attendees and a purchase can have multiple tickets.

We will focus on, given a list of attendees and tickets, tell whether there are enough tickets.

Purchases will not be handled.

We can start by only checking the number of attendees as follows:

-- spec :: Spec
-- spec =
--   describe "Conference tickets" $ do
--     it "with no maximum attendees should have enough tickets" $
--       hasEnoughTickets (Event Nothing) (replicate 5 Attendee) `shouldBe` True
--     it "with maximum attendees should have enough tickets" $
--       hasEnoughTickets (Event (Just 4)) (replicate 3 Attendee) `shouldBe` True
--     it "with maximum attendees should not have enough tickets" $
--       hasEnoughTickets (Event (Just 4)) (replicate 5 Attendee) `shouldBe` False

newtype Event = Event
  { makeAttendees :: Maybe Int
  }

data Attendee = Attendee

hasEnoughTickets :: Event -> [Attendee] -> Bool
hasEnoughTickets Event {..} attendees =
  case makeAttendees of
    Nothing -> True
    Just m -> length attendees <= m

Rule 1: Events can span over days, each attendee can attend to at least one day and a most one each day, each day can have a limit.

We can can come up we an naive implementation adding days information as follows:

-- spec :: Spec
-- spec =
--   describe "Conference tickets" $ do
--     describe "Total limit" $ do
--       let oneDayNoLimit = Map.singleton 1 Nothing
--           attendee = Attendee (Set.singleton 1)
--       it "with no maximum attendees should have enough tickets" $
--         hasEnoughTickets (Event Nothing oneDayNoLimit) (replicate 5 attendee) `shouldBe` True
--       it "with maximum attendees should have enough tickets" $
--         hasEnoughTickets (Event (Just 4) oneDayNoLimit) (replicate 3 attendee) `shouldBe` True
--       it "with maximum attendees should not have enough tickets" $
--         hasEnoughTickets (Event (Just 4) oneDayNoLimit) (replicate 5 attendee) `shouldBe` False
--     describe "Daily limit" $ do
--       let oneDayWithLimit = Map.singleton 1 (Just 5)
--           attendee = Attendee (Set.singleton 1)
--       it "with daily maximum attendees should have enough tickets" $
--         hasEnoughTickets (Event Nothing oneDayWithLimit) (replicate 3 attendee) `shouldBe` True
--       it "with daily maximum attendees should not have enough tickets" $
--         hasEnoughTickets (Event Nothing oneDayWithLimit) (replicate 6 attendee) `shouldBe` False

data Event = Event
  { makeAttendees :: Maybe Int,
    days :: Map.Map Int (Maybe Int)
  }

newtype Attendee = Attendee {bookedDays :: Set.Set Int}

hasEnoughTickets :: Event -> [Attendee] -> Bool
hasEnoughTickets Event {..} attendees =
  checkMaxAttendees
    && checkNumberOfDays
  where
    checkMaxAttendees =
      case makeAttendees of
        Nothing -> True
        Just m -> length attendees <= m
    checkNumberOfDays =
      all (\(day, limit) -> maybe True (\limit' -> countDays day <= limit') limit) $ Map.toList days
    countDays day =
      length $ filter (Set.member day . (.bookedDays)) attendees

And that it, that's disappointing, and that took us one hour.

In the actual system, there are extra rules such as:

  • Tickets have price
  • There could be two tickets, with different prices, for the same day
  • There could be multiple days-tickets

We will handle them next weeks.