Skip to content

Instantly share code, notes, and snippets.

@x86-39
Last active December 29, 2023 23:43
Show Gist options
  • Save x86-39/8b137a2d8df42cd856c8d266f865235f to your computer and use it in GitHub Desktop.
Save x86-39/8b137a2d8df42cd856c8d266f865235f to your computer and use it in GitHub Desktop.
Ansible Advent of Code 2023, Day 3
---
# WARNING: THIS TAKES A LONG TIME TO RUN ON REAL INPUT (6 hours!)
# This entire shitfuckery is exponential as all hell, Ansible was not made to do 190k tasks!
# Could be optimised I think by doing more logic within the Jinja2 loops, which would be calculated in the main Ansible process
# Set the input
- name: Set example input
ansible.builtin.set_fact:
day3_input: |-
467..114..
...*......
..35..633.
......#...
617*......
.....+.58.
..592.....
......755.
...$.*....
.664.598..
when: day3_input is not defined
# Get a list we can loop over
- name: Split input into separate lines
ansible.builtin.set_fact:
lines: "{{ day3_input | split('\n') }}"
# Create a list of characters for each list (2D array, e.g. [0][0])
- name: Split rows into columns
ansible.builtin.set_fact:
rows: "{{ lines | map('list') }}"
# We'll need this to check if we're not going over the limit
- name: Set the number of rows and row length
ansible.builtin.set_fact:
rows_length: "{{ rows | length }}"
row_length: "{{ rows[0] | length }}"
- name: Loop through every coordinate
ansible.builtin.set_fact:
# PART 1:
# Add the coordinates to a list, so we don't check them again (e.g. so 123 become 123, 23, 3)
coords_checked_already: "{{ coords_checked_already | default([]) + numbers_checked }}"
# If the number is adjacent to a symbol, add it to the list of numbers
numbers: "{% if is_adjacent_to_symbol | bool %}{{ numbers | default([]) + [number] }}{% else %}{{ numbers | default([]) }}{% endif %}"
# PART 2:
# Set the gears dict
# it should end up looking like { x: y: [gears_related_numbers]}
gears_dict: "{{ _gears_dict | trim | default({}) | from_yaml }}"
vars:
# Current character
current_item: "{{ rows[item.0][item.1] }}"
# Is this character a number?
current_is_number: "{% if current_item | regex_search('[0-9]') != None %}true{% else %}false{% endif %}"
# Every character to the right of the current character
look_right: "{{ rows[item.0][item.1 + 1:rows[0] | length] | map('regex_search', '([0-9]+)') }}"
# Some absolutely fucking abhorrent Jinja2 statement that:
# If we have not already checked this coord
# Gets the current position, loops over to the right until it finds a non-number character
# Every number it adds to the string (which we later cast to int)
# Adds to the coordinates we've checked, so we don't get duplicates
_look_right_nums: "
{# COMMENT: Check if we are at a number #}
{% if current_is_number | bool %}
{# COMMENT: Skip if we already have this in a different number #}
{% if [item.0, item.1] not in coords_checked_already | default([]) %}
{# COMMENT: Set vars so we track symbols, whether to break the loop and the numbers #}
{%- set ns = namespace(has_symbol = false, continues = true, numstring = '', coords_checked = []) -%}
{%- set ns.numstring = ns.numstring + current_item -%}
{% if ns.numstring != '' %}
{# COMMENT: Add current coordinate to coords checked #}
{%- set ns.coords_checked = ns.coords_checked + [(item.0, item.1)] -%}
{# COMMENT: Loop over every character to the right of the current character #}
{% for i in range(0, look_right | length) %}
{# COMMENT: If we can continue, and the current character is a number #}
{% if ns.continues %}
{% if look_right[i] | regex_search('[0-9]') != None %}
{%- set ns.coords_checked = ns.coords_checked + [(item.0, item.1 + 1 + i)] -%}
{# COMMENT: Concatenate the number to the string #}
{%- set ns.numstring = ns.numstring + look_right[i] -%}
{% else %}
{# COMMENT: If this isn't a number, break the loop #}
{%- set ns.continues = false -%}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endif %}
{% endif %}
{{ [ns.numstring | default(''),ns.coords_checked | default([])] }}"
look_right_nums: "{{ _look_right_nums | trim }}" # Every indent is taken literally, but I do want to keep them for """readability""", so we trim them
numbers_checked: "{{ look_right_nums | from_yaml | json_query('[1]') | list }}"
number: "{{ look_right_nums | from_yaml | json_query('[0]') }}"
# Surrounding chars, including diagonals, NOT going in negatives
# Apparently it can get worse than the previous statement
# Check every surrounding character to the numbers we've checked
# Check if it's within the bounds
# If it's not also a number we've checked
# Add it to a list we return
# what the fuck is this shit!
_surrounding_chars: "
{# COMMENT: Set empty list #}
{% set ns = namespace(chars = []) %}
{# COMMENT: If we are a number #}
{% if number != '' %}
{% for coord in numbers_checked %}
{% if coord.0 - 1 >= 0 %}
{% if coord.1 - 1 >= 0 %}
{# COMMENT: Check if this is a number we checked #}
{% if (coord.0 - 1, coord.1 - 1) not in numbers_checked %}
{%- set ns.chars = ns.chars + [[coord.0 - 1, coord.1 - 1, rows[coord.0 - 1][coord.1 - 1]]] -%}
{% endif %}
{% endif %}
{% if (coord.0 - 1, coord.1) not in numbers_checked %}
{%- set ns.chars = ns.chars + [[coord.0 - 1, coord.1, rows[coord.0 - 1][coord.1]]] -%}
{% endif %}
{% if coord.1 + 1 < row_length | int %}
{% if (coord.0 - 1, coord.1 + 1) not in numbers_checked %}
{%- set ns.chars = ns.chars + [[coord.0 - 1, coord.1 + 1, rows[coord.0 - 1][coord.1 + 1]]] -%}
{% endif %}
{% endif %}
{% endif %}
{% if coord.1 - 1 >= 0 %}
{% if (coord.0, coord.1 - 1) not in numbers_checked %}
{%- set ns.chars = ns.chars + [[coord.0, coord.1 - 1, rows[coord.0][coord.1 - 1]]] -%}
{% endif %}
{% endif %}
{% if coord.1 + 1 < row_length | int %}
{% if (coord.0, coord.1 + 1) not in numbers_checked %}
{%- set ns.chars = ns.chars + [[coord.0, coord.1 + 1, rows[coord.0][coord.1 + 1]]] -%}
{% endif %}
{% endif %}
{% if coord.0 + 1 < rows_length | int %}
{% if coord.1 - 1 >= 0 %}
{% if (coord.0 + 1, coord.1 - 1) not in numbers_checked %}
{%- set ns.chars = ns.chars + [[coord.0 + 1, coord.1 - 1, rows[coord.0 + 1][coord.1 - 1]]] -%}
{% endif %}
{% endif %}
{% if (coord.0 + 1, coord.1) not in numbers_checked %}
{%- set ns.chars = ns.chars + [[coord.0 + 1, coord.1, rows[coord.0 + 1][coord.1]]] -%}
{% endif %}
{% if coord.1 + 1 < row_length | int %}
{% if (coord.0 + 1, coord.1 + 1) not in numbers_checked %}
{%- set ns.chars = ns.chars + [[coord.0 + 1, coord.1 + 1, rows[coord.0 + 1][coord.1 + 1]]] -%}
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{{ ns.chars | list }}"
# Surrounding chars, trimmed
surrounding_chars: "{{ _surrounding_chars | trim | from_yaml | map('last') }}"
# Surrounding chars, without dots
surrounding_chars_without_dots: "{{ surrounding_chars | reject('equalto', '.') }}"
# Is this number adjacent to a symbol (not a dot)?
is_adjacent_to_symbol: "{{ surrounding_chars_without_dots | length > 0 }}"
# PART 2:
# Get the surrounding characters, but keep the coordinates
surrounding_chars_with_coords: "{{ _surrounding_chars | trim | from_yaml }}"
# Surrounding gears (*)
surrounding_gears: "{{ surrounding_chars_with_coords | selectattr('2', 'equalto', '*') }}"
# Get all the gears that are adjacent to the current number
# Then loop over them, add the current number to the gears' list in the gears dict
# it should end up looking like { x: y: [gears_related_numbers]}
# this took so long to get right. fuck this shit
_gears_dict: "
{%- set ns = namespace(new_dict = gears_dict | default({} )) -%}
{%- for gear in surrounding_gears -%}
{%- set gearns = namespace(gear_current_row = gears_dict[gear.0] | default({}), gears_related_numbers = gears_dict[gear.0][gear.1] | default([])) -%}
{%- set gearns.gears_related_numbers = gearns.gears_related_numbers + [number] -%}
{%- set gearns.gear_current_row = gearns.gear_current_row | combine({gear.1: gearns.gears_related_numbers}) -%}
{%- set ns.new_dict = ns.new_dict | combine({gear.0: gearns.gear_current_row}) -%}
{%- endfor -%}
{{ ns.new_dict }}"
additional_gears_dict: "{{ _additional_gears_dict | trim | from_yaml }}"
loop: "{{ range(0, rows | length) | list | product(range(0, rows[0] | length)) | list }}"
- name: ANSWER TO PART 1 | Sum the integers that are next to a symbol
ansible.builtin.debug:
msg: "{{ numbers | map('int') | sum }}"
- name: Get the gears that have two neighbouring numbers
ansible.builtin.set_fact:
gear_ratios: "{{ _gear_ratios | trim | from_yaml }}"
vars:
# Get the numbers without the coordinates (we just needed the coordinates earlier so we can know which numbers touch through a gear)
# Create a new empty list
# For every X
# For every Y
# Add the Y value (the list of numbers) to the new list
_flattened_numbers: "
{%- set ns = namespace(new_list = []) -%}
{% for x in gears_dict | dict2items %}
{% for y in x.value | dict2items %}
{%- set ns.new_list = ns.new_list + [y.value] -%}
{% endfor %}
{% endfor %}
{{ ns.new_list }}"
# Remove whitespace & make object
flattened_numbers: "{{ _flattened_numbers | trim | from_yaml }}"
# Get the numbers that have two neighbours
# Use a JMESPath query to get the numbers that have two neighbours
# This was hellish too i fucking hate this
equal_to_two: "{{ flattened_numbers | json_query('[? length(@) == `2`]') }}"
# Now for every pair we multiply them together..... almost there.. my sanity is dwindling
_gear_ratios: "
{%- set ns = namespace(new_list = []) -%}
{% for pair in equal_to_two %}
{%- set ns.new_list = ns.new_list + [pair.0 | int * pair.1 | int] -%}
{% endfor %}
{{ ns.new_list }}"
- name: ANSWER TO PART 2 | Sum the gear ratios
ansible.builtin.debug:
msg: "{{ gear_ratios | map('int') | sum }}"
...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment