2023 Day 4: Scratchcards

I got stuck on silly parsing bugs in part 1 and am too tired for part 2 tonight. Looks like it’s gonna be a bit of recursion.

I switched to using Dmitry’s Strings_Edit library for parsing this year. Seems easier than standard Ada integer and text IO.

Each card has a set of integers for the winning numbers and my numbers respectively.

The card pile is a vector of cards.

My solutions

1 Like

I think that there is no need to convert to Naturals; comparing (sub)strings is more than enough. But be careful selecting the sub-string, because " 4" matches "12 46 15" but they represent different numbers; it should have three characters per number, including the space at the beginning.

Both parts can be programmed line by line: once a line is read, it can be processed and discarded. Only the second part requires a small array to accumulate copies of cards to come.

Check solutions here: https://github.com/rocher/advent-of-code/tree/main/2023/04

1 Like

I maintained an array rather than going line-by-line, and I didn’t have issues parsing at all. But as @rocher points out, and as @JeremyGrosser may want to note, there’s no need for recursion; just keep track of the number of copies of cards.

Edit: Here’s my Ada solution.

Edit 2: Here’s my Rust solution. It uses the same approach as the Ada solution, and GitHub reports that the number of lines of code is roughly the same, but the Rust uses exclusively functional programming constructs. Not a single for loop appears! you have to decipher .iter and .map and .filter etc. :microscope: :wink:

1 Like

My take on it:

I defined my type to be a map of winning numbers (for easy search) and vector of my own numbers:

   -- Need some element type for the map.  Not going
   -- to use it.
   type Placeholder is null record;

   -- Container type packages for upcoming card type
   package Natural_Maps is new Ada.Containers.Ordered_Maps
      (Key_Type     => Natural,
       Element_Type => Placeholder);
   package Natural_Vectors is new Ada.Containers.Vectors
      (Index_Type   => Positive,
       Element_Type => Natural);

   -- Holds parsed data for each card.  Winning numbers are placed into
   -- a map for quick searching.  Your current numbers are placed into 
   -- a standard vector
   type Card is record
      Winning_Numbers : Natural_Maps.Map       := Natural_Maps.Empty_Map;
      Current_Numbers : Natural_Vectors.Vector := Natural_Vectors.Empty_Vector;
   end record;

Then I just parsed out each line:

   -- Line parsing constants
   Card_Delimiter : constant String := ":";
   List_Delimiter : constant String := "|";

   function Parse_Line(Line : String) return Card is

      use Ada.Strings.Fixed;
      First  : Natural := Index(Line, Card_Delimiter);
      Last   : Natural := Index(Line, List_Delimiter);
      Index  : Natural := First;
      Result : Card;
      Dummy  : Placeholder;
      Number : Natural;

      -- Clean up string by removing unneeded spaces around 
      -- data sets
      Winning_String : constant String := Ada.Strings.Fixed.Trim
         (Source => Line(First+1 .. Last-1),
          Side   => Ada.Strings.Both);
      Current_String : constant String := Ada.Strings.Fixed.Trim
         (Source => Line(Last+1 .. Line'Last),
          Side   => Ada.Strings.Both);

      -- package for parsing numbers out of strings
      package Natural_IO is new Ada.Text_IO.Integer_IO(Natural);

   begin
      if First = 0 or Last = 0 then
         raise Program_Error with "Invalid file format";
      end if;

      --  Update First and Last for new string search
      First := Winning_String'First;  
      Last  := Winning_String'Last;

      -- Get Winning Numbers first
      loop
         Natural_IO.Get
            (From => Winning_String(First..Last),
             Item => Number,
             Last => Index);
         Result.Winning_Numbers.Insert(Number, Dummy);

         exit when Index = Last;
         First := Index + 1;
      end loop;

      --  Update First and Last for new string search
      First := Current_String'First;
      Last  := Current_String'Last;

      -- Get your current numbers
      loop
         Natural_IO.Get
            (From => Current_String(First..Last),
             Item => Number,
             Last => Index);
         Result.Current_Numbers.Append(Number);

         exit when Index = Last;
         First := Index + 1;
      end loop;

      return Result;
   end Parse_Line;

I used the following calculation function:

   function Calculate_Points(Card : Day_4.Card) return Points is
      Score : Points  := 0;
   begin
      if Card.Winning_Numbers.Is_Empty or Card.Current_Numbers.Is_Empty then
         return 0;
      end if;

      for Number of Card.Current_Numbers loop
         if Card.Winning_Numbers.Contains(Number) then
            if Score = 0 then
               Score := 1; -- First match worth one point
            else
               Score := Score * 2;  -- Score double after the first match
            end if;
         end if;
      end loop;

      return Score;
      
   end Calculate_Points;

After that, it was just a matter of opening a file, read it line by line, parse each line, calculate their score, and add it to the total.

Dmitry’s library is definitely really useful for string parsing. On my end, I limited myself to only standard library stuff from Ada. I do keep a Utilities package in my project to hold useful stuff across all day solutions. I didn’t find using integer_io to be too cumbersome though.

Here’s an example snippet of how I used it and I felt it was pretty clean:

      First  : Natural := Ada.Strings.Fixed.Index(Line, "" & ':');
      Last   : Natural := Ada.Strings.Fixed.Index(Line, "" & '|');

      -- Clean up string by removing unneeded spaces around 
      -- data sets
      Winning_String : constant String := Ada.Strings.Fixed.Trim
         (Source => Line(First+1 .. Last-1),
          Side   => Ada.Strings.Both);
      Current_String : constant String := Ada.Strings.Fixed.Trim
         (Source => Line(Last+1 .. Line'Last),
          Side   => Ada.Strings.Both);

      -- package for parsing numbers out of strings
      package Natural_IO is new Ada.Text_IO.Integer_IO(Natural);
      --  Update First and Last for new string search
      First := Winning_String'First;  
      Last  := Winning_String'Last;

      -- Get Winning Numbers first
      loop
         Natural_IO.Get
            (From => Winning_String(First..Last),
             Item => Number,
             Last => Index);
         Result.Winning_Numbers.Insert(Number, Dummy);

         exit when Index = Last;
         First := Index + 1;
      end loop;

Since Integer_IO.Get ignores the white space between the numbers automatically, I didn’t have to parse those (aside from the outer space so that I could make sure the check against Last was valid, but the Trim function did that for me).

Why not use Ada.Containers.Hashed_Sets instead of .Hashed_Maps?

No particular reason. In the past I found maps giving me better searching performance over sets, but I don’t usually use the hashed versions and they are clunky to use with some Key types (coming up with a good hash function for some types of inputs is interesting sometimes). I’m not super worried about performance here, so just did what was comfortable.

Oh, yeah. That’s one thing I like about Rust; I almost never have to think up a hashing function; the compiler, or perhaps the standard library, figures out a good default on its own. And I have definitely come up with some real stinkers in my lazier moments. (Happened to me in a previous Advent of Code, in fact!)