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
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
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.
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!)