2023 Day 1: Trebuchet?!

I hope someone has a more elegant solution for part 2. I wrote a rudimentary parser, regex seemed like overkill.

Out of curiosity, is there a place to see part 2 without submitting part 1 that you know of? I’m just doing it for fun, so no github, etc, so I wasn’t planning on submitting results officially to AoC.

Edit: what is the URL for part 2, maybe I can just go there manually?

I don’t think you can get to Part 2 without completing Part 1 first.

Ha! I thought it started tonight, so I haven’t looked at it yet.

For what it’s worth, I wrote an even more rudimentary parser. I’m not sure how you solve it otherwise in Ada.

I spoke with some folks at work about doing it in Rust, and one mentioned using the aho-corasick crate. I’ll probably try that when I attack this in Rust, which I hope to do later today. I know we’ve used that in one of our projects.

Edit: Here’s the Rust version.

1 Like

In my case, a simple case over a String worked really well, in this procedure.

Is it locked behind a login or an email? I was wondering because if not, I could probably get to it via just manually typing in the web url, assuming I had an example url for part 2.

You’ll need a login, because Part 2 doesn’t appear unless you submit a correct answer for Part 1. I will tell you Part 2 via private message.

Thank you so much! I appreciate it.

First problem solved… I spent an hour just because of a < vs <=… sight… Well, I am not entirely happy with how I did it, but it is a kinda small solution. It does way too many operations on strings and the extra whitespace from 'Image came to bite me… Though I am happy that I leverage types heavily!

2 Likes

@JeremyGrosser (and anyone else interested)

I’ve now completed implementations of Day 1 in Ada, Modula-2, and Rust. Programming Modula-2 for the first time in 30+ years was fun… for the first 5 minutes or so. :grin: The Ada and Modula-2 implementations are about as identical as I can make them; I deliberately went a slightly different route with the Rust implementation.

Regarding the three, a few things I observed, sometimes only with great pain:

  1. One standard library

    • :heavy_check_mark: Ada
    • :x: Modula-2
      The default library for the gnu Modula-2 compiler (gm2) is based on PIM, Niklaus Wirth’s “Programming in Modula-2” specification. But there are others, including different revisions of PIM, and they all differ in some areas, such as string comparison. PIM, like C’s string comparison, returns an integer. The ISO standard, which I wanted to use, defines and returns a proper type. After I lost a few minutes trying to figure out why gm2 didn’t like my use of Strings.Compare, then switching to PIM unhappily, I remembered that gnu Modula-2 offers the option to compile against the ISO libraries, and was able to switch back.
    • :heavy_check_mark: Rust
  2. Prohibiting the use of uninitialized variables.

    • :heavy_minus_sign: Ada
      You can use and return uninitialized variables in Ada, and it absolutely can crash your code, but
      • gnat (at least) typically warns you,
      • you can initialize them at declaration (see below), and
      • it won’t allow you to leave a discriminant uninitialized on a variable of discriminated record type.
    • :x: Modula-2
      I spent way too long debugging an error where I was returning an uninitialized variable.
    • :heavy_check_mark: Rust
      Requires you to initialize all variables.
  3. Ability to initialize a variable at declaration

    • :heavy_check_mark: Ada
    • :x: Modula-2
      This was annoying, and contributed to the bug mentioned above, because I forgot to initialize a variable.
    • :heavy_check_mark: Rust
      In addition to the requirement that you initialize every variable at declaration, Rust makes it convenient to define a default value by implementing the Default trait or even by deriving it automatically.
  4. Easy substrings (e.g., S (S'First + 1 .. S'Last - 1))

    • :heavy_check_mark: Ada
      The example is Ada. Offhand though I don’t know if it allocates new memory for the string, or merely creates a new structure that points into the original. (Anyone reading this far know?)
    • :x: Modula-2
      I had to initialize a new array of the desired length and copy the characters one-by-one. :roll_eyes: Maybe there’s a better way to do this in Modula-2, but if so I don’t know what it is.
    • :heavy_check_mark: Rust
      Slicing, borrowing, copying, moving are something Rust’s designers thought a lot about, and there are a lot of features that make it possible, convenient, and safe.
  5. String comparison via =

    • :heavy_check_mark: Ada
    • :x: Modula-2
      In Modula-2 you have to use a library function, and as noted above, which library function you use depends on which standard you use.
    • :heavy_check_mark: Rust

There are probably other things worth mentioning, but these were the points that most stuck out to me as I was writing the code. Mostly the Modula-2 code, as you’ll notice.

1 Like

This is a really good argument for SPARK’s requirement that all variables be initialized at declaration. Is there a Restriction that can enforce this for Ada without running gnatprove? I’m not sure Initialize_Scalars is the right thing.

No allocation happens when slicing arrays like this, the returned value points to the same memory with the altered 'First and 'Last values.

This kind of comparison is really useful, thank you!

1 Like

Thanks; I’m glad to hear it helps someone. If I had all the time in the world, I’d try implementing this in Eiffel, Modula-3, Nim, and maybe even Kotlin. Alas, there’s only so much time in the day.

This is a really good argument for SPARK’s requirement that all variables be initialized at declaration.

Indeed. A lot of Rust’s safety features pre-exist it. In some ways, Rust is a lot like Ada, in that they took a lot of great ideas from other languages.

Is there a Restriction that can enforce this for Ada without running gnatprove?

I don’t find one in the gnat switches, but as I recall, Ada by itself isn’t always smart enough to figure out that a subprogram does indeed return a value. I’ve received that warning when, for instance, I return from a loop that terminates only when it returns, and I know for a fact that it will return at some point. To be honest, offhand I’m not sure whether Rust is smart enough to figure that out, and perhaps I’m misremembering who it was that warned me. :grin:

The problem with requiring all variables to be initialised at declaration is that there are situations where there isn’t a sensible default value to use, but initialisation should happen afterwards on all paths in the code.

In such situations it is recommended to not default initialize at declaration, and to use Initialize_Scalars, which will allow one to detect the use of a variable for which initialisation was missing in a preceeding path. Forcing a default initialisation would cause the code to continue running with an incorrect value, possibly producing incorrect results.

For more context and rationale, see our paper which introduced Initialize_Scalars: “Exposing Uninitialized Variables: Strengthening and Extending Run-Time Checks in Ada” at https://people.cs.kuleuven.be/~dirk.craeynest/papers/ae02cfmu-paper.pdf

4 Likes

I’m a little behind so this is late on my part.

So I did it a little weird.

I created an enumeration type:

type Base_10_Digit is (zero, one, two, three, four, five, six, seven, eight, nine);

Then I used the Image attribute paired up with Ada.Characters.Handling.To_Lower to generate a lower case string image of them. Since all enumerations have a “Pos” attribute that nicely mapped directly to my type (zero => 0, one => 1, etc.), so I could use that combined with a Natural’Image (and logic to remove the leading space). to generate a printable result for each digit later.

Reference code:

   type Base_10_Digit is (Zero, One, Two, Three, Four, Five, Six, Seven, Eight, Nine);
   function Name_String(Value : Base_10_Digit) return String is
      (Ada.Characters.Handling.To_Lower(Value'Image));
   function Digit_String(Value : Base_10_Digit) return String is
      (Image(Base_10_Digit'Pos(Value)));

*** That last Image function is just wrapper function that strips out the leading zero

With that, I could just loop through each digit in the type and call Ada.Strings.Fixed.Index with the appropriate input string from one of those functions above to get the first and last locations of those strings if they existed. In the loop I kept track of the first-most or last-most result as I looped through each digit value.

Reference code:

   type Optional_Digit(Valid : Boolean := False) is record
      case Valid is
         when False => null;
         when True  => 
            Index : Positive;
            Digit : Base_10_Digit;
      end case;
   end record;

   function Get_Digit
      (Source : String; 
       Going  : Ada.Strings.Direction) return String 
   is
      use Ada.Strings;
      Saved : Optional_Digit := (Valid => False);
      Index : Natural        := 0;

      procedure Update_Saved(Digit : Base_10_Digit) is
      begin
         -- If a match is found, then Index /= 0
         if Index /= 0 then

            -- Need to do some logic if Saved already has a value in it
            if Saved.Valid then
               case Going is
                  when Ada.Strings.Forward  =>

                     -- Look for the first instance
                     if Index < Saved.Index then
                        Saved := (Valid => True, Index => Index, Digit => Digit);
                     end if;

                  when Ada.Strings.Backward =>

                     -- Look for the last instance
                     if Index > Saved.Index then
                        Saved := (Valid => True, Index => Index, Digit => Digit);
                     end if;

               end case;
               
            -- Otherwise, if this is the first time, then just save the data
            else
               Saved := (Valid => True, Index => Index, Digit => Digit);
               end if;
         end if;

      end Update_Saved;
   begin
      for Digit in Base_10_Digit'Range loop

         -- Check for spelled out digit names
         Index := Fixed.Index
            (Source  => Source,
             Pattern => Name_String(Digit),
             Going   => Going);
         Update_Saved(Digit);

         -- Check for character digits
         Index := Fixed.Index
            (Source  => Source,
             Pattern => Digit_String(Digit),
             Going   => Going);
         Update_Saved(Digit);

      end loop;

      if Saved.Valid then
         return Digit_String(Saved.Digit);
      else
         return "";
      end if;

   end Get_Digit;

I’d just call this for Forward and Backward on the same line input, validate the results, and concatenate them together if they were good.

I don’t know if that qualifies for elegant or not though. It’s not super efficient, but not horrible to code at least.

Decided to screw around with regex for this one.

1 Like

:astonished: :scream:

Was it worth it?