Packages

A Gleam library for comparing strings/lists and producing a textual (styled) representation of the differences.

Current section

Files

Jump to
gap src gap.gleam
Raw

src/gap.gleam

import gleam/string
import gleam/list
import gleam/map.{Map}
import gleam/result
import gleam/option.{None, Option, Some}
import gleam/int
import gleam/order.{Eq, Gt, Lt}
import gleam/set
import gap/comparison.{
Comparison, ListComparison, Match, NoMatch, Segments, StringComparison,
}
import gap/styled_comparison.{StyledComparison}
import gap/styling.{
first_highlight_default, from_comparison, highlight, no_highlight,
second_highlight_default, to_styled_comparison,
}
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`
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 lists and return a `ListComparison` which will be styled as list
/// when passed to `to_styled`
pub fn compare_lists(
first_sequence: List(a),
second_sequence: List(a),
) -> Comparison(a) {
let diff_map =
second_sequence
|> list.index_fold(
map.new(),
fn(diff_map, item_second, index_second) {
first_sequence
|> 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 tracking =
back_track(
diff_map,
list.length(first_sequence) - 1,
list.length(second_sequence) - 1,
[],
)
|> map.from_list()
let first_segments =
collect_matches(
tracking,
first_sequence,
fn(key) {
let #(first, _) = key
first
},
)
let second_segments =
collect_matches(
tracking,
second_sequence,
fn(key) {
let #(_, second) = key
second
},
)
ListComparison(first_segments, second_segments)
}
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)
}