Solve Advent of Code 2017 Day 1 with XQuery and BaseX

Solve Advent of Code 2017 Day 1 with XQuery and BaseX

I solved the Advent of Code 2017 Day 1 puzzle with XQuery, an XML query language, and BaseX, an XML database engine and XQuery processor. This post details my approach to solving both parts of Advent of Code 2017 Day 1 using a ‘circular list’, XQuery, and BaseX.

Something I enjoy about Advent of Code puzzles and coding in general is that there are numerous ways to conceptualize and approach these problems, and we can compare our approaches within and across languages. The XQuery code I reference within this post is available in my GitHub repository aoc-xquery. Word of warning: Advent of Code 2017 Day 1 spoilers ahead.

Advent of Code 2017 Day 1

Advent of Code 2017 Day 1 is an inverse CAPTCHA puzzle. We generally interact with a CAPTCHA to prove we are human, yet this challenge is to prove we’re not human by solving a complex problem in two parts:

Part one

The captcha requires you to review a sequence of digits (your puzzle input) and find the sum of all digits that match the next digit in the list. The list is circular, so the digit after the last digit is the first digit in the list.

Part two

Now, instead of considering the next digit, it wants you to consider the digit halfway around the circular list. That is, if your list contains 10 items, only include a digit in your sum if the digit 10/2 = 5 steps forward matches it. Fortunately, your list has an even number of elements.

To solve Advent of Code 2017 Day 1, I thought it’d be fun to turn the sequence of digits into an actual ‘circular list’ in order to find matching digits. First, however, we need to have a sequence of digits. Advent of Code provides unique ‘puzzle input’ to each user, and my sequence of digits is the following string of numbers:

77736991856689225253142335214746294932318813454849177823468674346512426482777696993348135287531487622845155339235443718798255411492778415157351753377959586612882455464736285648473397681163729345143319577258292849619491486748832944425643737899293811819448271546283914592546989275992844383947572926628695617661344293284789225493932487897149244685921644561896799491668147588536732985476538413354195246785378443492137893161362862587297219368699689318441563683292683855151652394244688119527728613756153348584975372656877565662527436152551476175644428333449297581939357656843784849965764796365272113837436618857363585783813291999774718355479485961244782148994281845717611589612672436243788252212252489833952785291284935439662751339273847424621193587955284885915987692812313251556836958571335334281322495251889724281863765636441971178795365413267178792118544937392522893132283573129821178591214594778712292228515169348771198167462495988252456944269678515277886142827218825358561772588377998394984947946121983115158951297156321289231481348126998584455974277123213413359859659339792627742476688827577318285573236187838749444212666293172899385531383551142896847178342163129883523694183388123567744916752899386265368245342587281521723872555392212596227684414269667696229995976182762587281829533181925696289733325513618571116199419759821597197636415243789757789129824537812428338192536462468554399548893532588928486825398895911533744671691387494516395641555683144968644717265849634943691721391779987198764147667349266877149238695714118982841721323853294642175381514347345237721288281254828745122878268792661867994785585131534136646954347165597315643658739688567246339618795777125767432162928257331951255792438831957359141651634491912746875748363394329848227391812251812842263277229514125426682179711184717737714178235995431465217547759282779499842892993556918977773236196185348965713241211365895519697294982523166196268941976859987925578945185217127344619169353395993198368185217391883839449331638641744279836858188235296951745922667612379649453277174224722894599153367373494255388826855322712652812127873536473277

We can start by assigning the above input to the variable $puzzle-input as a string in order to use it as the first argument for the XQuery function I wrote to solve the puzzle: inverse-captcha(). The second argument of inverse-captcha() represents the part of the puzzle we intend to solve, and should be an integer value of 1 or 2. Next, let’s examine how inverse-captcha() works.

inverse-captcha() explained

The XQuery function inverse-captcha() solves Advent of Code 2017 Day 1 by piping $puzzle-input through three lambda expressions$get-digits(), $create-circular-list(), and $find-matches()—and the XQuery function fn:sum() to return an integer solution for either part one or part two of the puzzle.

$get-digits() explained

We begin by passing $puzzle-input as an argument to the lambda expression $get-digits(), which breaks the string into a sequence of integers:

let $get-digits := (
  function (
    $digit-list as xs:string
  ) as xs:integer+
  {
    $digit-list
    => fn:string-to-codepoints()
    => fn:for-each(fn:codepoints-to-string#1)
    => fn:for-each(xs:integer#1)
  }
)

The XQuery code above takes an argument of type xs:string and binds it to the variable $digit-list before piping it through a series of functions to return a sequence of xs:integer items. We first pipe $digit-list to fn:string-to-codepoints(), which returns a sequence containing each individual character’s codepoint. For example, the head character of the string, 7, is returned as the head of the output sequence in its Unicode decimal form: 55. Next, we pipe our newly formed sequence of codepoints to fn:for-each(). Because fn:for-each() takes a function as its argument it’s termed a higher-order function, and in this case we use it to apply fn:codepoints-to-string() to each item in our sequence—using some syntactic sugar, #1, to represent our sequence. At this point, we’ve broken $digit-list into a sequence of type xs:string and our last step is to convert each item to type xs:integer using fn:for-each() and xs:integer() in order to return our data as an xs:integer+ sequence.

Overall, $get-digits() is an efficient expression for converting a string of digits into a sequence of integers. From this point forward, we’ll require items of type xs:integer for mathematical operations and creating a ‘circular list’.

$create-circular-list() explained

Upon reading the description of Advent of Code 2017 Day 1, I was inspired to solve this puzzle by creating a ‘circular list’ rather than by looping through a sequence, which for some reason I imagine as more of a line beginning and ending with a ‘Portal-style’ wormhole rather than a circular loop. So, what is a ‘circular list’? Technically, I’ve used an XQuery map to associate angles with our output from $get-digits() as key-value pairs. My idea is that if we think of the length of our xs:integer+ sequence as the circumference of a circle, we can create a circle and label points on its circumference with the digits from our xs:integer+ sequence. This method also solves my ‘Portal problem’ since 0 and are the same point on the circle’s circumference and we can begin and end there. The XQuery code to create a ‘circular list’ follows:

let $create-circular-list := (
  function (
    $digits as xs:integer+
  ) as map(*)
  {
    let $circumference := (
      fn:count($digits)
    )
    let $radius := (
      $circumference div (2 * math:pi())
    )
    let $theta := (
      2 * math:pi() div $circumference
    )
    let $circular-list := (
      map:merge((
        for $point in (1 to $circumference)
        let $angle := (
          $theta * $point
        )
        let $digit := (
          $digits[$point]
        )
        return (
          map:entry($angle, $digit)
        ),
        map:entry(0, $digits[$circumference]),
        map:entry("radius", $radius),
        map:entry("theta", $theta)
      ))
    )
    return (
      $circular-list
    )
  }
)

The lambda expression $create-circular-list() takes our xs:integer+ sequence output from $get-digits() and returns a map(*) item representing a ‘circular list’ and some metadata. The value of the variable $digits is the xs:integer+ sequence argument of $create-circular-list(). To create our circle, we first use fn:count() to determine the length of $digits and bind the result to the variable $circumference. Next, we use a mathematical formula to derive the radius of our ‘circular list’ and bind its result to the variable $radius. To do so, we use the div operator to divide $circumference by , which in XQuery requires an expression containing the integer 2, the operator *, and the function math:pi(). The last step we’ll take before creating a ‘circular list’ is determining the smallest angle between two points on the circle’s circumference. Here, we bind the result of dividing by $circumference to the variable $theta.

We use XQuery’s for clause within the function map:merge() to iterate through each point along our circle’s circumference and create a ‘circular list’. Specifically, we use XQuery’s for clause and declare the variable $point, whose initial value is 1 and final value is the length of our xs:integer+ sequence: $circumference. Therefore, for each $point we bind the result of $theta * $point to the variable $angle and the value of $digits[$point] to the variable $digit before returning $angle and $digit as a key-value pair with the function map:entry(). For example, the first point on our circle’s circumference is stored in our map(*) item with our circle’s smallest angle, $theta, representing the point’s key and the head of our xs:integer+ sequence, 7, representing the point’s label (i.e., its value). After the for clause, we use map:entry() to add three additional entries: the final item within $digits with the integer key 0, ensuring 0 and are the same point; $radius with the string key radius; and, $theta with the string key theta. The addition of these entries completes our ‘circular list’ with the necessary metadata to solve Advent of Code 2017 Day 1 by finding matching digits within our ‘circular list’.

$find-matches() explained

The instructions for finding matches within are slightly differ for part one and part two of Advent of Code 2017 Day 1. Where necessary, we’ll use conditional expressions to return output appropriate for each part of the puzzle—either the next digit or the digit halfway around our ‘circular list’. Essentially, $find-matches() compares points along our circle’s circumference and returns the points with matching labels as an xs:integer+ sequence:

let $find-matches := (
  function (
    $circular-list as map(*)
  ) as xs:integer+
  {
    let $radius := (
      $circular-list
      => map:get("radius")
    )
    let $theta := (
      $circular-list
      => map:get("theta")
    )
    let $arc-length := (
      if (
        $part = 1
      )
      then (
        $radius * $theta
      )
      else if (
        $part = 2
      )
      then (
        math:pi() * $radius
      )
      else ()
    )
    let $points := (
      $circular-list
      => map:size() - 3 - $arc-length
      => xs:integer()
    )
    let $matches := (
      for $point in (0 to $points)
      let $angle-A := (
        $theta * $point
      )
      let $angle-B := (
        $theta * ($point + $arc-length)
      )
      let $digit-A := (
        $circular-list
        => map:get($angle-A)
      )
      let $digit-B := (
        $circular-list
        => map:get($angle-B)
      )
      where (
        $digit-A = $digit-B
      )
      return (
        $digit-A
      )
    )
    return (
      if (
        $part = 1
      )
      then (
        $matches
      )
      else if (
        $part = 2
      )
      then (
        $matches,
        $matches
      )
      else ()
    )
    => fn:unordered()
  }
)

The argument of $find-matches() is the map(*) item returned by $get-digits(), and we assign it to the variable $circular-list. Before finding matches, we declare the variables $radius and $theta and use the function map:get() to retrieve the metadata values we stored within our ‘circular list’. Next, we declare the variable $arc-length, which represents the length of the arc between two points along our circle’s circumference. A conditional expression determines the value of $arc-length by either multiplying $radius by $theta or by π in order to match either the next digit or the digit halfway around our ‘circular list’ respectively.

The number of points required to check for matches is dependent on the value of $arc-length. In other words, while part one checks whether each digit matches the next digit on our ‘circular list’ and requires we travel 2π radians around our circle’s circumference, part two checks the digit halfway around and requires we travel only π radians. Therefore, we assign the variable $points a value of map:size() minus 3—our three additional entries—and minus $arc-length. From here, we use a for clause to iterate the value of $point along our circle’s circumference from 0 to $points. At each $point, we declare two angles: $angle-A—for the current point—and $angle-B—for the comparison point. We pass these angles to map:get() in order to retrieve two values (i.e., digits) from our ‘circular list’: $digit-A and $digit-B. Next, we use XQuery’s where clause to filter only digits with a match through to our return clause. Finally, $find-matches() returns an xs:integer+ sequence of digits with matches, using fn:unordered() for a performance boost because we have no compelling reason to maintain the order of our output sequence before passing it to fn:sum(). Note that we return $matches twice for part two since we travelled only π radians around our ‘circular list’ and these matches are reciprocal.

Solution

Ultimately, we solve Advent of Code 2017 Day 1 by piping $puzzle-input through our lambda expressions and fn:sum():

let $solution := (
  $puzzle-input
  => $get-digits()
  => $create-circular-list()
  => $find-matches()
  => fn:sum()
)

Running inverse-captcha() in BaseX with my $puzzle-input and the $part argument 1 returns the following result:

1223

With the $part argument 2, BaseX returns a different number of matches:

1284

I’m making a note here: HUGE SUCCESS. I’ve completed both parts of Advent of Code 2017 Day 1 using XQuery and a ‘circular list’, and have received two gold stars.

Notes

My thanks to Clifford Anderson for introducing me to Advent of Code. I’d also like to thank the Vanderbilt University XQuery Working Group and the Nashville Functional Programmers for allowing me to present this XQuery code in person in addition to elevating my understanding of XQuery and functional programming.

What we learned

Thanks to Advent of Code 2017 Day 1, we can now:

  • write and use lambda expressions
  • convert a string a digits to a sequence of integers
  • create a ‘circular list’
  • perform mathematical operations in XQuery
  • solve an inverse CAPTCHA
  • use the following XQuery functions: fn:codepoints-to-string(), fn:count(), fn:for-each(), fn:string-to-codepoints(), fn:sum(), fn:unordered(), map:entry(), map:get(), map:merge(), map:size(), math:pi(), and xs:integer()

Thank you for reading, and have fun coding.