Using only vim to solve AdventOfCode Challenges | Episode 3
Tags: vim, neovim, linux, adventofcode
Published: 2023-12-17 20:34:13

# 1\. Getting Started

To begin, access [today's challenge](https://adventofcode.com/2022/day/3) and carefully read the instructions below before attempting to solve it.

**New here?** `scenario1` and `scenario2` might be unfamiliar, these sprints are designed to stretch your limits – you'll need to tackle the problem from both angles, or your solution won't fly. So, if you're content to stay within your comfort zone, this [sprint](https://github.com/cipherlogs/s909/) isn't for you. Start with the [first episode](https://cipherlogs.com/how-to-solve-advent-of-code-using-only-vim-episode-1) and give it a go yourself. Spend at least 15 minutes grappling with both scenarios before peeking at the solutions.

Remember, make sure that you are in sync with your team, *if you don't have a team yet submit your application here https://forms.gle/bwegPDvbgEuWKDF47*

**Good luck!**  

## 1.1 Rules

1. In the first scenario, refrain from creating low-level functions that involve iterating through strings to extract duplicated characters. Instead, leverage built-in Vimscript functions creatively or utilize Unix utilities. Alternatively, if achieving the desired results promptly requires a one-liner in a programming language, that approach is acceptable. However, avoid reinventing the wheel; the emphasis is on efficiency in this speed-focused scenario.
    
2. In the second scenario, pay attention to the following considerations:
    
* The initial aspect of the challenge involves splitting each line in half. However, the script should be flexible enough to allow for splitting into thirds, quarters, or any other specified interval. Ensure your script is capable of dynamically handling different split configurations.
        
* For the first part of the challenge, when splitting each line into segments of size 'n,' retain only those chunks that match the specified size. For instance, if `'abcdefgh'` is split into groups of 3, the desired result should be `['abc', 'def']`.
        
* The second part of the challenge focuses on joining lines in groups of `3`. Extend the script's capability to accommodate future needs for joining lines in groups of `5`, `7`, or any other specified interval `n`. Ensure your script is adaptable to varying group sizes.
        

## 1.2 Sample Data

The provided sample data should be used for testing the commands. Once you feel comfortable and confident, you can apply these commands to your actual input data.

**Input**:

```plaintext
vJrwpWtwJgWrhcsFMMfFFhFp
jqHRNqRjqzjGDLGLrsFMfFZSrLrFZsSL
PmmdzqPrVvPwwTWBwg
wMqvLMZHhHMvwLHjbvcjnnSBnvTQFn
ttgJtRGJQctTZtZT
CrZsJsPPZsGzwwsLwLmpwMDw
```

**Expected output**:

* **part1:** 157
    
* **part2:** 70
    

  

# 2\. Solutions

This guide assumes you've already watched the accompanying walk-through YouTube video. If you haven't, please do.

{% embed https://youtu.be/YazmNjy-mCA %}

## 2.1 The First Scenario

### TL;DR

##### Scenario1 Part1:

1. Define a function `Char2nr` to calculate character priorities.
    
2. Use `getline('.')` to get the current line.
    
3. Split the line into left and right halves.
    
4. Use `grep` to find unique characters in both halves.
    
5. Execute the command and replace the line with the output.
    
6. Apply the process to all lines.
    
7. Convert unique characters to priorities using `:%s/.*/\=Char2nr(submatch(0))`
    
8. Sum up the priorities using `%! paste -sd+ | bc`
    

##### Scenario1 Part2:

1. Merge three lines at a time using `:g/^/.,+2j`.
    
2. Calculate the total score using `let total += split(getline('.'))->reduce(Intersect)->Char2nr()`.
    
3. View the final score with `:echo total`
    

  

### 2.1.1 Part One

Let's begin by addressing the obvious.

As we progress through the challenge, we will eventually need to convert characters to their corresponding priority levels.

* Lowercase item types 'a' through 'z' have priorities ranging from 1 through 26.
    
* Uppercase item types 'A' through 'Z' have priorities ranging from 27 through 52.
    

```plaintext
" 96 because -char2nr('a') + 1 == 96
" 38 because -char2nr('A') + 26 + 1 == 38

let Char2nr = {c -> char2nr(c) - (tolower(c) ==# c ? 96 : 38)}
```

We use the `==#` operator to perform a case-sensitive comparison, as opposed to the `==` operator.

Now, let's proceed with the following steps:

Initiate the recording of a macro with `qq`, then extract useful information that we'll need later:

```plaintext
let str = getline('.') | let mid = len(str)/2
```

Having acquired the content of the current line and its midpoint, let's split it in half and save the left and right sides:

```plaintext
let [left, right] = [str[:mid+1], str[mid:]]
```

To extract unique characters from both the left and right sides, we can employ `grep`. In this case we want to highlight the unique characters so we'll use `grep -o`. Constructing the command within VIM requires formatting, for a more cleaner look we'll use `printf()` instead of concatenation. Here's the formatted command:

```plaintext
let cmd = printf(".!echo '%s' | grep -o '[%s]' | sort -u | head -1", left, right)
```

The `!` signals to Vim that this is a shell command, and the `.` indicates that Vim should replace the current line with the output of our constructed command.

Now, Execute the command using `:exe cmd` and move to the next line and conclude the macro with `q`

With the macro defined, apply it to the remaining lines using `:% norm @q`. This process transforms all the lines into a single unique character.

The next step is to convert all unique characters to their corresponding priority and sum them up:

```plaintext
:%s/.*/\=Char2nr(submatch(0))/ | %! paste -sd+ | bc
```

**Congratulations, Part One is now solved!**

Before advancing to the next part, let's review and identify potential enhancements. During the initial attempt, it's common to miss the broader perspective, especially in this scenario where we are emphasizing speed.

The command we composed to find unique characters in two chunks of strings can be encapsulated in a function:

```plaintext
let Intersect = {a, b -> printf("echo '%s' | grep -o '[%s]' | sort -u | head -1", a, b)->system()}
```

This function now returns a string instead of mutating the actual line invoked from. Furthermore, it operates as a reducer, a concept from functional programming. Reducers should be designed to accumulate, akin to recording a macro that can be repeated.

However, there's a subtlety. If we apply our reducer to chunks of strings (2 by 2), there's a potential issue. The reducer returns strings with new lines (due to `grep` and `sort`), and merging these results might be inconsistent. Additionally, using `head` in this situation is discouraged. *More details can be found in the exercises section*.

To address this, the function must consistently return either an empty string or a string, regardless of the input. Let's remove any new lines from the output using `tr -d '\n'`:

```plaintext
let Intersect = {a, b -> printf("echo '%s' | grep -o '[%s]' | sort -u | tr -d '\n'", a, b)->system()}
```

Understanding the underlying mechanics is crucial; it provides more value than just learning about specific utilities.

Now we can proceed to the next part.

### 2.1.2 Part Two

Our current objective is to consolidate three lines at a time, extract unique characters from each merged line, and finally, display the cumulative score for all lines. This might sound like a lot.

Well, let's see what vim has to say about that:

```plaintext
:let total = 0
:g/^/.,+2j | let total += split(getline('.'))->reduce(Intersect)->Char2nr()
```

**Congrats, Part Two is solved!**

These commands efficiently merge lines in groups of three and calculate the total score. The final score is stored in the 'total' variable. To view the result, simply run `:echo total`.

## 2.2 The Second Scenario

Let's begin by revisiting familiar ground. You still remember `Char2nr` right?

```plaintext
let Char2nr = {c -> char2nr(c) - (tolower(c) ==# c ? 96 : 38)}
```

Now, onto the Intersect function. While we could reuse the one from the first scenario, for the sake of learning, let's explore an alternative implementation:

```plaintext
let Intersect = {a, b ->split(a, '\zs')->filter({_, c -> c =~# "[".b."]"})->join('')}
```

* We use '\\zs' to split at the start of each character, transforming, for instance, "abc" into \['a', 'b', 'c'\].
    
* The filter step retains only the characters matching our regex pattern, with the `c =~ 'regex'`, the use of `#` is to ensure case sensitivity.
    
* Finally, we reassemble the characters into a single string using `join()`.
    

With both `Intersect` and `Char2nr` in place, let's streamline the process further by creating a utility function to determine the priority level of each line:

```plaintext
let GetPriority = {str -> split(str)->reduce(g:Intersect)->g:Char2nr()}
```

Going forward, we only need to interact with `GetPriority`, simplifying our code.

Now, let's approach the challenge from a different perspective, considering the additional rules introduced. Despite seeming disparate, joining lines and splitting strings boil down to manipulating delimiters.

To address this, let's design a function that adds a space delimiter to any list every n elements. For example, calling `SpaceEvery(2, ['a', 'b', 'c', 'd'])` should yield `['a', 'b', ' ', 'c', 'd']`:

```plaintext
let SpaceEvery = {n, xs ->
  \ range(n, len(xs) - n, n)
  \ ->map({i, x -> x + i})
  \ ->reduce({acc, i -> insert(xs, ' ', i)}, copy(xs))
  \}
```

This versatile function can now be applied to each part of the process, enhancing flexibility and maintainability.  
  
**Note**: In case `SpaceEvery()` is expecting shorter strings, then it is better to use `range(n, len(xs), n)`

### 2.2.1 Part One

The first step is to split a line in half

```plaintext
let InHalf = {xs -> g:SpaceEvery(len(xs)/2, split(xs, '\zs'))->join('')}
```

### 2.2.2 Part Two

the second part is to join lines in groups of 3

```plaintext
let JoinBy3 = {xs -> g:SpaceEvery(3, xs)->join(',')->split()->map("split(v:val,',')->join()")}
```

### 2.2.3 Finale Part

Now that we have created all the necessary utilities, let’s apply them to our input file.

For the first part, we can say:

```plaintext
echo
      \ readfile("inputdemo")
      \ ->map("InHalf(v:val)")
      \ ->map("GetPriority(v:val)")
      \ ->reduce({a, b -> a + b})
```

And for the second part:

```plaintext
echo
      \ same as before
      \ ->JoinBy3()
      \ same as before
      \ same as before
```

**Congrats**, Not only did we solve the original challenge, but we also overcame the additional constraints and rules.

### 2.2.4 Finale Script

the final script is as follow:

```plaintext
let Intersect = {a, b ->
      \ split(a, '\zs')->filter({_, c -> c =~# "[".b."]"})->join('')}

let Char2nr = {c ->
      \ char2nr(c) - (tolower(c) ==# c ? 96 : 38)}

let GetPriority = {str ->
      \ split(str)->reduce(g:Intersect)->g:Char2nr()}

let SpaceEvery = {n, xs ->
      \ range(n, len(xs) - n, n)
      \ ->map({i, x -> x + i})
      \ ->reduce({acc, i -> insert(xs, ' ', i)}, copy(xs))}

let InHalf = {xs ->
      \ g:SpaceEvery(len(xs)/2, split(xs, '\zs'))->join('')}

let JoinBy3 = {xs ->
      \ g:SpaceEvery(3, xs)
      \ ->join(',')
      \ ->split()
      \ ->map("split(v:val,',')->join()")}


echo
      \ readfile("inputdemo")
      \ ->JoinBy3()
      \ ->map("GetPriority(v:val)")
      \ ->reduce({a, b -> a + b})
```

# 4\. Bonus

**For a more detailed explanation of this section, please refer to the walkthrough video.**

In the second scenario, we decided to reinvent the wheel. This is entirely acceptable because we are in a learning process, and my objective is to equip you with the skills necessary for the challenges that lie ahead. when we start the [second season](https://github.com/cipherlogs/s909), we will delve into the development of plugins and tackle more complex tasks. It is my intention that these foundational concepts become second nature to you, akin to drinking water.

Allow me to illustrate a real-life scenario. When expediency is essential, opting for a quick-and-dirty approach is viable. Instead of employing the `readfile()` function, we can load a file into memory using buffers and manipulate it as if it were open(technically it is open).

Let's explore how we can use commands in a script to address a particular problem. Please note that I will demonstrate this using the original challenge, not the augmented version from the second scenario, as the latter involves additional elements.

Imagine that your script file, 'myscript.vim', is open. If you input the following lines:

```plaintext
badd inputfile | buff inputfile
buff myscript
```

This is equivalent to `:e inputfile`, followed by `buff myscript`, swiftly transitioning between the two files. Executing this file might seem uneventful, but VIM's efficiency creates an illusion of no movement due to its rapid switching between files.

*Now, what's the significance of this?*

We can insert commands in between and modify our files using vim commands. Observe the following:

```plaintext
badd inputfile | buff inputfile
norm ggdd
buff myscript
```

In this scenario, our script switches to `inputfile`, navigates to the first line, deletes it, and then switches back to our `myscript` file. Remarkably efficient, isn't it?

Now, let's apply this technique to solve part 2 of our challenge:

```plaintext
badd inputfile | buffinputfile
let total = 0
g/^/.,+2j | let total += split(getline('.'))->reduce(Intersect)->Char2nr()
norm u
buff myscript
echo total
```

The `norm u` command ensures that after modifying the buffer, we undo the changes to maintain the original state of the buffer. Our primary focus is on calculating the result without altering the file.

This method of scriptwriting is invaluable, especially when you need to create scripts in this format or when hardcoding macros prove to be efficient. We'll explore examples later to highlight the efficiency of this approach.

# 5\. Exercises

From time to time, I'll share the solutions on Twitter, however, I want you to spend some time with the exercises on your own and with your team.

1. In the first scenario, instead of employing `Char2nr` to calculate priority, achieve the same result using only `:%s`. A helpful hint is to utilize the `'\C'` flag to specify that the substitution should be case-sensitive.
    
2. Rather than consolidating three lines using the global command `:g`, explore an alternative approach using only regular expressions.
    
3. When we wanted to make `Intersect` acts as a reducer
    
    3.1. why we weren't capable of using `head -1`  
    
    3.2. If `Intersect` finds many occurrences it will return them all as a string, how can we make `Intersect` only returns one character while keeping the function useful if used as a reducer?  
    
    3.3. In our case why is it okay to not use `tr -d '\n'`?
    
4. In the second scenario, if we find multiple unique occurrences we only choose the first one, this time select the one that appears the most.
    
5. Instead of `SpaceEvery`. Create a function that can put any delimiter for each `n` interval.
    
6. Using buffers only, write the solution for `scenario1 part1` and `part2`, without opening the input file and make sure that you use the same solution that we came up with. (you can either hardcode macors using `let @q=...` or use `norm` to record it.)