aboutsummaryrefslogtreecommitdiff
path: root/aoc2023/build/packages/gap/src/gap.gleam
diff options
context:
space:
mode:
Diffstat (limited to 'aoc2023/build/packages/gap/src/gap.gleam')
-rw-r--r--aoc2023/build/packages/gap/src/gap.gleam438
1 files changed, 438 insertions, 0 deletions
diff --git a/aoc2023/build/packages/gap/src/gap.gleam b/aoc2023/build/packages/gap/src/gap.gleam
new file mode 100644
index 0000000..7eb0e7f
--- /dev/null
+++ b/aoc2023/build/packages/gap/src/gap.gleam
@@ -0,0 +1,438 @@
+import gleam/string
+import gleam/list
+import gleam/pair
+import gleam/map.{type Map}
+import gleam/result
+import gleam/option.{type Option, None, Some}
+import gleam/int
+import gleam/order.{Eq, Gt, Lt}
+import gleam/set
+import gap/comparison.{
+ type Comparison, type Match, type Segments, ListComparison, Match, NoMatch,
+ StringComparison,
+}
+import gap/styled_comparison.{type StyledComparison}
+import gap/styling.{
+ first_highlight_default, from_comparison, highlight, no_highlight,
+ second_highlight_default, to_styled_comparison,
+}
+import gap/myers.{type Edit, Del, Eq as MyerEq, Ins}
+
+type MatchedItem(a) =
+ #(#(Int, Int), a)
+
+type Score(a) {
+ Score(value: Int, item: Option(a))
+}
+
+type DiffMap(a) =
+ Map(#(Int, Int), Score(a))
+
+/// Creates a `StyledComparison` from `Comparison` using default values for
+/// highting and serialization.
+///
+/// ## Example
+///
+/// ```gleam
+/// > compare_strings("abc", "abe") |> to_styled()
+/// ```
+/// This will return a `StyledComparison(first, second)` where "c" in `first` is green
+/// and "e" in `second` is red.
+pub fn to_styled(comparison: Comparison(a)) -> StyledComparison {
+ comparison
+ |> from_comparison()
+ |> highlight(first_highlight_default, second_highlight_default, no_highlight)
+ |> to_styled_comparison()
+}
+
+/// Compare two string and return a `StringComparison` which will be styled as string
+/// when passed to `to_styled`
+///
+/// Will use the default `myers` algorithm
+pub fn compare_strings(first: String, second: String) -> Comparison(String) {
+ let comparison =
+ compare_lists(string.to_graphemes(first), string.to_graphemes(second))
+ case comparison {
+ ListComparison(first, second) -> StringComparison(first, second)
+ StringComparison(first, second) -> StringComparison(first, second)
+ }
+}
+
+/// Compare two string and return a `StringComparison` which will be styled as string
+/// when passed to `to_styled`
+///
+/// Algorithm can be used to select either `myers` or the legacy `lcs` algorithm
+pub fn compare_strings_with_algorithm(
+ first: String,
+ second: String,
+ algorithm,
+) -> Comparison(String) {
+ let comparison =
+ algorithm(string.to_graphemes(first), string.to_graphemes(second))
+ case comparison {
+ ListComparison(first, second) -> StringComparison(first, second)
+ StringComparison(first, second) -> StringComparison(first, second)
+ }
+}
+
+/// Compare two lists and return a `ListComparison` which will be styled as list
+/// when passed to `to_styled`
+///
+/// Will use the default `myers` algorithm
+pub fn compare_lists(
+ first_sequence: List(a),
+ second_sequence: List(a),
+) -> Comparison(a) {
+ myers(first_sequence, second_sequence)
+}
+
+/// Compare two lists and return a `ListComparison` which will be styled as list
+/// when passed to `to_styled`
+///
+/// Algorithm can be used to select either `myers` or the legacy `lcs` algorithm
+pub fn compare_lists_with_algorithm(
+ first_sequence: List(a),
+ second_sequence: List(a),
+ algorithm,
+) -> Comparison(a) {
+ algorithm(first_sequence, second_sequence)
+}
+
+/// An adapter for the the `myers` algorithm.
+/// Intended to be use as an argument to `compare_strings_with_algorithm` or
+/// `compare_lists_with_algorithm`
+pub fn myers(first_sequence: List(a), second_sequence: List(a)) -> Comparison(a) {
+ let edits = myers.difference(first_sequence, second_sequence)
+ edits
+ |> list.reverse()
+ |> list.fold(
+ ListComparison([], []),
+ fn(comparison: Comparison(a), edit: Edit(a)) {
+ case comparison {
+ ListComparison(first, second) -> {
+ case edit {
+ MyerEq(segment) ->
+ ListComparison(
+ [Match(segment), ..first],
+ [Match(segment), ..second],
+ )
+ Ins(segment) -> ListComparison(first, [NoMatch(segment), ..second])
+ Del(segment) -> ListComparison([NoMatch(segment), ..first], second)
+ }
+ }
+ StringComparison(..) -> comparison
+ }
+ },
+ )
+}
+
+/// An adapter for the the `lcs` (longest common subsequence) algorithm.
+/// Intended to be use as an argument to `compare_strings_with_algorithm` or
+/// `compare_lists_with_algorithm`
+pub fn lcs(first_sequence: List(a), second_sequence: List(a)) -> Comparison(a) {
+ let leading_matches =
+ list.zip(first_sequence, second_sequence)
+ |> list.take_while(fn(pair) { pair.0 == pair.1 })
+ |> list.map(pair.first)
+ let num_leading_matches = list.length(leading_matches)
+ let trailing_matches =
+ list.zip(list.reverse(first_sequence), list.reverse(second_sequence))
+ |> list.take_while(fn(pair) { pair.0 == pair.1 })
+ |> list.map(pair.first)
+ |> list.reverse()
+ let num_trailing_matches = list.length(trailing_matches)
+ let first_sequence_to_diff =
+ first_sequence
+ |> list.drop(num_leading_matches)
+ |> list.take(
+ list.length(first_sequence) - num_leading_matches - num_trailing_matches,
+ )
+ let second_sequence_to_diff =
+ second_sequence
+ |> list.drop(num_leading_matches)
+ |> list.take(
+ list.length(second_sequence) - num_leading_matches - num_trailing_matches,
+ )
+
+ let diff_map =
+ second_sequence_to_diff
+ |> list.index_fold(
+ map.new(),
+ fn(diff_map, item_second, index_second) {
+ first_sequence_to_diff
+ |> list.index_fold(
+ diff_map,
+ fn(diff_map, item_first, index_first) {
+ build_diff_map(
+ item_first,
+ index_first,
+ item_second,
+ index_second,
+ diff_map,
+ )
+ },
+ )
+ },
+ )
+ let #(first_segments, second_segments) = case
+ first_sequence_to_diff,
+ second_sequence_to_diff
+ {
+ [], [] -> #([], [])
+ first_matching, [] -> #([NoMatch(first_matching)], [])
+ [], second_matching -> #([], [NoMatch(second_matching)])
+ first_sequence_to_diff, second_sequence_to_diff -> {
+ let tracking =
+ back_track(
+ diff_map,
+ list.length(first_sequence_to_diff) - 1,
+ list.length(second_sequence_to_diff) - 1,
+ [],
+ )
+ |> map.from_list()
+
+ let first_segments =
+ collect_matches(
+ tracking,
+ first_sequence_to_diff,
+ fn(key) {
+ let #(first, _) = key
+ first
+ },
+ )
+ let second_segments =
+ collect_matches(
+ tracking,
+ second_sequence_to_diff,
+ fn(key) {
+ let #(_, second) = key
+ second
+ },
+ )
+ #(first_segments, second_segments)
+ }
+ }
+
+ let #(
+ first_segments_with_leading_trailing,
+ second_segments_with_leading_trailing,
+ ) = case leading_matches, trailing_matches {
+ [], [] -> #(first_segments, second_segments)
+ [], trailing_matches -> #(
+ first_segments
+ |> append_and_merge(Match(trailing_matches)),
+ second_segments
+ |> append_and_merge(Match(trailing_matches)),
+ )
+ leading_matches, [] -> #(
+ first_segments
+ |> prepend_and_merge(Match(leading_matches)),
+ second_segments
+ |> prepend_and_merge(Match(leading_matches)),
+ )
+ leading_matches, trailing_matches -> #(
+ first_segments
+ |> prepend_and_merge(Match(leading_matches))
+ |> append_and_merge(Match(trailing_matches)),
+ second_segments
+ |> prepend_and_merge(Match(leading_matches))
+ |> append_and_merge(Match(trailing_matches)),
+ )
+ }
+
+ ListComparison(
+ first_segments_with_leading_trailing,
+ second_segments_with_leading_trailing,
+ )
+}
+
+fn prepend_and_merge(
+ matches: List(Match(List(a))),
+ match: Match(List(a)),
+) -> List(Match(List(a))) {
+ case matches, match {
+ [], _ -> [match]
+ [Match(first_match), ..rest], Match(_) -> [
+ Match(
+ match.item
+ |> list.append(first_match),
+ ),
+ ..rest
+ ]
+ [NoMatch(first_match), ..rest], NoMatch(_) -> [
+ NoMatch(
+ match.item
+ |> list.append(first_match),
+ ),
+ ..rest
+ ]
+ matches, match -> [match, ..matches]
+ }
+}
+
+fn append_and_merge(
+ matches: List(Match(List(a))),
+ match: Match(List(a)),
+) -> List(Match(List(a))) {
+ case
+ matches
+ |> list.reverse(),
+ match
+ {
+ [], _ -> [match]
+ [Match(first_match), ..rest], Match(_) -> [
+ Match(
+ first_match
+ |> list.append(match.item),
+ ),
+ ..rest
+ ]
+ [NoMatch(first_match), ..rest], NoMatch(_) -> [
+ NoMatch(
+ first_match
+ |> list.append(match.item),
+ ),
+ ..rest
+ ]
+ matches, match -> [match, ..matches]
+ }
+ |> list.reverse()
+}
+
+fn collect_matches(tracking, str: List(a), extract_fun) -> Segments(a) {
+ let matching_indexes =
+ map.keys(tracking)
+ |> list.map(extract_fun)
+ |> set.from_list()
+
+ let matches =
+ str
+ |> list.index_map(fn(index, item) {
+ case set.contains(matching_indexes, index) {
+ True -> Match(item)
+ False -> NoMatch(item)
+ }
+ })
+
+ matches
+ |> list.chunk(fn(match) {
+ case match {
+ Match(_) -> True
+ NoMatch(_) -> False
+ }
+ })
+ |> list.map(fn(match_list) {
+ case match_list {
+ [Match(_), ..] ->
+ Match(list.filter_map(
+ match_list,
+ fn(match) {
+ case match {
+ Match(item) -> Ok(item)
+ NoMatch(_) -> Error(Nil)
+ }
+ },
+ ))
+ [NoMatch(_), ..] ->
+ NoMatch(list.filter_map(
+ match_list,
+ fn(match) {
+ case match {
+ NoMatch(item) -> Ok(item)
+ Match(_) -> Error(Nil)
+ }
+ },
+ ))
+ }
+ })
+}
+
+fn back_track(
+ diff_map: DiffMap(a),
+ first_index: Int,
+ second_index: Int,
+ stack: List(MatchedItem(a)),
+) -> List(MatchedItem(a)) {
+ case first_index == 0 || second_index == 0 {
+ True -> {
+ let this_score =
+ map.get(diff_map, #(first_index, second_index))
+ |> result.unwrap(Score(0, None))
+ case this_score {
+ Score(_, Some(item)) -> [#(#(first_index, second_index), item), ..stack]
+ _ ->
+ case first_index, second_index {
+ 0, a if a > 0 ->
+ back_track(diff_map, first_index, second_index - 1, stack)
+ a, 0 if a > 0 ->
+ back_track(diff_map, first_index - 1, second_index, stack)
+ 0, 0 -> stack
+ _, _ -> back_track(diff_map, first_index - 1, second_index, stack)
+ }
+ }
+ }
+ False -> {
+ let this_score =
+ map.get(diff_map, #(first_index, second_index))
+ |> result.unwrap(Score(0, None))
+ case this_score {
+ Score(_, Some(item)) ->
+ back_track(
+ diff_map,
+ first_index - 1,
+ second_index - 1,
+ [#(#(first_index, second_index), item), ..stack],
+ )
+ Score(_, None) -> {
+ let up =
+ map.get(diff_map, #(first_index, second_index - 1))
+ |> result.unwrap(Score(0, None))
+ let back =
+ map.get(diff_map, #(first_index - 1, second_index))
+ |> result.unwrap(Score(0, None))
+ case int.compare(up.value, back.value) {
+ Gt -> back_track(diff_map, first_index, second_index - 1, stack)
+ Lt -> back_track(diff_map, first_index - 1, second_index, stack)
+ Eq ->
+ case first_index, second_index {
+ 0, a if a > 0 ->
+ back_track(diff_map, first_index, second_index - 1, stack)
+ a, 0 if a > 0 ->
+ back_track(diff_map, first_index - 1, second_index, stack)
+ 0, 0 -> stack
+ _, _ ->
+ back_track(diff_map, first_index - 1, second_index, stack)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+fn build_diff_map(
+ first_item: a,
+ first_index: Int,
+ second_item: a,
+ second_index: Int,
+ diff_map: DiffMap(a),
+) -> DiffMap(a) {
+ let prev_score =
+ map.get(diff_map, #(first_index - 1, second_index - 1))
+ |> result.unwrap(Score(0, None))
+ let derived_score_up =
+ diff_map
+ |> map.get(#(first_index, second_index - 1))
+ |> result.unwrap(Score(0, None))
+ let derived_score_back =
+ diff_map
+ |> map.get(#(first_index - 1, second_index))
+ |> result.unwrap(Score(0, None))
+ let derived_score = int.max(derived_score_up.value, derived_score_back.value)
+ let this_score = case first_item == second_item {
+ True -> Score(prev_score.value + 1, Some(first_item))
+ False -> Score(derived_score, None)
+ }
+ diff_map
+ |> map.insert(#(first_index, second_index), this_score)
+}