aboutsummaryrefslogtreecommitdiff
path: root/aoc-2020-gleam/src/days/day21.gleam
diff options
context:
space:
mode:
Diffstat (limited to 'aoc-2020-gleam/src/days/day21.gleam')
-rw-r--r--aoc-2020-gleam/src/days/day21.gleam149
1 files changed, 149 insertions, 0 deletions
diff --git a/aoc-2020-gleam/src/days/day21.gleam b/aoc-2020-gleam/src/days/day21.gleam
new file mode 100644
index 0000000..a398fb5
--- /dev/null
+++ b/aoc-2020-gleam/src/days/day21.gleam
@@ -0,0 +1,149 @@
+import gleam/io
+import gleam/int
+import gleam/list
+import gleam/pair
+import gleam/string as str
+import gleam/set.{type Set}
+import gleam/dict.{type Dict}
+import ext/setx
+import ext/boolx
+import ext/resultx as resx
+import util/input_util
+import util/parser as p
+
+type Ingredient =
+ String
+
+type Allergen =
+ String
+
+type Food {
+ Food(ingredients: Set(Ingredient), some_allergens: Set(Allergen))
+}
+
+type Candidates =
+ Dict(Allergen, Set(Ingredient))
+
+fn parse_food(text: String) -> Food {
+ let food_parser =
+ p.alpha1()
+ |> p.sep1(by: p.ws_gc())
+ |> p.map(with: set.from_list)
+ |> p.skip(p.literal(" (contains "))
+ |> p.then(
+ p.alpha1()
+ |> p.sep1(by: p.literal(", "))
+ |> p.map(with: set.from_list),
+ )
+ |> p.skip(p.literal(")"))
+ |> p.map2(with: Food)
+
+ text
+ |> p.parse_entire(with: food_parser)
+ |> resx.assert_unwrap
+}
+
+fn allergen_candidates(foods: List(Food)) -> Candidates {
+ foods
+ |> list.map(fn(f) { f.some_allergens })
+ |> setx.arbitrary_union
+ |> set.to_list
+ |> list.map(with: fn(allergen) {
+ #(
+ allergen,
+ foods
+ |> list.filter(keeping: fn(f) {
+ set.contains(in: f.some_allergens, this: allergen)
+ })
+ |> list.map(with: fn(f) { f.ingredients })
+ |> setx.arbitrary_intersection,
+ )
+ })
+ |> dict.from_list
+}
+
+fn stabilize(candidates: Candidates) -> Dict(Allergen, Ingredient) {
+ let is_stable = fn(s) { set.size(s) == 1 }
+
+ use <- boolx.guard_lazy(
+ when: candidates
+ |> dict.values
+ |> list.all(satisfying: is_stable),
+ return: fn() {
+ candidates
+ |> dict.map_values(with: fn(_, ingredients) {
+ let assert [ingredient] = set.to_list(ingredients)
+ ingredient
+ })
+ },
+ )
+
+ let stable_set =
+ candidates
+ |> dict.to_list
+ |> list.filter_map(with: fn(entry) {
+ let #(_, ingredients) = entry
+ case set.to_list(ingredients) {
+ [stable] -> Ok(stable)
+ _ -> Error(Nil)
+ }
+ })
+ |> set.from_list
+
+ candidates
+ |> dict.map_values(with: fn(_, ingredients) {
+ case is_stable(ingredients) {
+ True -> ingredients
+ False -> setx.subtract(from: ingredients, given: stable_set)
+ }
+ })
+ |> stabilize
+}
+
+fn part1(lines: List(String)) -> Int {
+ let foods = list.map(lines, with: parse_food)
+
+ let all_ingredients =
+ foods
+ |> list.map(fn(f) { f.ingredients })
+ |> setx.arbitrary_union
+
+ let candidates = allergen_candidates(foods)
+
+ let suspicious_ingredients =
+ candidates
+ |> dict.values
+ |> setx.arbitrary_union
+
+ let safe_ingredients =
+ setx.subtract(from: all_ingredients, given: suspicious_ingredients)
+
+ foods
+ |> list.map(with: fn(f) {
+ setx.count(f.ingredients, satisfying: set.contains(safe_ingredients, _))
+ })
+ |> int.sum
+}
+
+fn part2(lines: List(String)) -> String {
+ lines
+ |> list.map(with: parse_food)
+ |> allergen_candidates
+ |> stabilize
+ |> dict.to_list
+ |> list.sort(by: fn(a, b) { str.compare(pair.first(a), pair.first(b)) })
+ |> list.map(with: pair.second)
+ |> str.join(",")
+}
+
+pub fn main() -> Nil {
+ let testing = input_util.read_lines("test21")
+ let assert 5 = part1(testing)
+ let assert "mxmxvkd,sqjhc,fvjkl" = part2(testing)
+
+ let input = input_util.read_lines("day21")
+ io.debug(part1(input))
+ io.println(part2(input))
+
+ Nil
+}