Yog.Utils (YogEx v0.97.0)

Copy Markdown View Source

Shared utility functions used across the Yog library.

This module provides common helper functions that are used by multiple modules in the Yog library, such as comparison functions for custom numeric types.

Summary

Functions

Generates all k-combinations of a list.

A standard Gleam-compatible comparison function for numbers in Elixir.

Descending comparison function.

Fisher-Yates shuffle: O(n) unbiased shuffling.

Folds over a map using the fast BIF :maps.fold/3.

Calculates the difference (distance) between two vectors (maps of scores) using the specified norm type.

Safely converts any Elixir term to a string.

Safely converts node data to a string label.

Safely converts edge weight/data to a string label.

Functions

combinations(list, k)

@spec combinations([a], integer()) :: [[a]] when a: var

Generates all k-combinations of a list.

A k-combination is a subset of k distinct elements from the list, where order does not matter.

Examples

iex> Yog.Utils.combinations([1, 2, 3], 2)
[[1, 2], [1, 3], [2, 3]]

iex> Yog.Utils.combinations([1, 2, 3], 0)
[[]]

compare(a, b)

@spec compare(number(), number()) :: :lt | :eq | :gt

A standard Gleam-compatible comparison function for numbers in Elixir.

Many algorithms (like Dijkstra, A*, and centrality measures) require an explicit comparison function that returns :lt, :eq, or :gt to order values in priority queues. Writing this manually can be repetitive.

This function evaluates to:

  • :lt when a < b
  • :eq when a == b
  • :gt when a > b

It works for both integers and floats.

While this function initially was influenced by the Gleamy origin of Yog, it felt more of a direction agnotic way to handle comparisons in algorithms that needed comparators passed into them, for instance, Dijkstra's algorithm could sometimes show compare.(a, b) == true but we would need to remember if it means a < b or a > b (what if the comparator passed in was > instead of < ?).

We could name the parameter less_than and greater_than to address this, but ternary operator felt more explicit, especially in cases where the algorithms need for comparison would be direction agnostic. Having only :lt or :gt would add to confusion as to where it was if a < b ... else ... or if a > b ... else ... so a ternary outcome felt explicit (We have examples in Version and Date)

Examples

iex> Yog.Utils.compare(10, 20)
:lt
iex> Yog.Utils.compare(20, 20)
:eq
iex> Yog.Utils.compare(30, 20)
:gt
iex> Yog.Utils.compare(1.5, 3.2)
:lt

compare_desc(a, b)

@spec compare_desc(number() | :infinity, number() | :infinity) :: :lt | :eq | :gt

Descending comparison function.

This is the reverse of the standard comparison - it treats larger values as "less than" (:lt) smaller values, so that priority queues (min-heaps) will pop the largest value first. It correctly handles :infinity as the maximum possible value.

Used by algorithms that need to maximize a value, such as widest_path/3 or maximum spanning tree algorithms.

Examples

iex> Yog.Utils.compare_desc(100, 50)
:lt
iex> Yog.Utils.compare_desc(50, 100)
:gt
iex> Yog.Utils.compare_desc(:infinity, 100)
:lt
iex> Yog.Utils.compare_desc(100, 100)
:eq

fisher_yates(list, seed \\ :rand.uniform(1_000_000))

@spec fisher_yates([a], integer()) :: [a] when a: var

Fisher-Yates shuffle: O(n) unbiased shuffling.

Uses Erlang's :array for efficient mutable-style operations. Deterministic when given a seed (for reproducibility).

Examples

iex> Yog.Utils.fisher_yates([1, 2, 3, 4, 5], 42)
[3, 2, 5, 4, 1]

iex> Yog.Utils.fisher_yates([], 123)
[]

map_fold(map, acc, fun)

@spec map_fold(map(), acc, (key, value, acc -> acc)) :: acc
when key: any(), value: any(), acc: var

Folds over a map using the fast BIF :maps.fold/3.

This is a wrapper around :maps.fold/3 with a more Elixir-friendly API:

  • Data (map) comes first (like Enum.reduce)
  • Followed by the initial accumulator
  • Then the function with arity 3: (key, value, acc) -> new_acc

This avoids the overhead of Enum.reduce protocol dispatch and eliminates the need for Map.to_list + List.foldl which creates intermediate lists.

Performance Comparison

ApproachSpeedNotes
Yog.Utils.map_fold/3FastestDirect BIF call, no allocation
:maps.fold/3FastestSame as above, but awkward argument order
Enum.reduce(map, ...)SlowerProtocol dispatch overhead
List.foldl(Map.to_list(map), ...)SlowestAllocates intermediate list

Examples

iex> map = %{a: 1, b: 2, c: 3}
iex> Yog.Utils.map_fold(map, 0, fn _k, v, acc -> acc + v end)
6

iex> map = %{x: 10, y: 20}
iex> Yog.Utils.map_fold(map, %{}, fn k, v, acc -> Map.put(acc, k, v * 2) end)
%{x: 20, y: 40}

When to Use

Use this function when:

  • You need to iterate over a map's key-value pairs
  • Performance matters (hot paths, large maps)
  • You don't need the generic Enumerable protocol features

For lists, use List.foldl/3 instead. For other enumerables, use Enum.reduce/3.

norm_diff(m1, m2, type)

@spec norm_diff(map(), map(), :l1 | :l2 | :max) :: float()

Calculates the difference (distance) between two vectors (maps of scores) using the specified norm type.

Supported types:

  • :l1 - Manhattan Distance (Sum of absolute differences)
  • :l2 - Euclidean Distance (Square root of sum of squares)
  • :max - Chebyshev Distance (Maximum absolute difference)

Examples

iex> Utils.norm_diff(%{a: 1, b: 2}, %{a: 3, b: 4}, :l1)
4.0

iex> Utils.norm_diff(%{a: 1, b: 2}, %{a: 3, b: 4}, :l2)
2.8284271247461903

iex> Utils.norm_diff(%{a: 1.1, b: 2}, %{a: 3, b: 4}, :max)
2.0

safe_string(val)

Safely converts any Elixir term to a string.

Uses Kernel.to_string/1 for binaries, atoms, and numbers, and falls back to inspect/1 for complex types like tuples, maps, and lists.

to_label(id, data)

@spec to_label(Yog.node_id(), any()) :: String.t()

Safely converts node data to a string label.

If data is a map (common in GraphML or GDF imports), it looks for common labeling keys ("label", :label). If not found or if the data is empty, it falls back to the node ID.

Examples

iex> Yog.Utils.to_label(1, "Alice")
"Alice"
iex> Yog.Utils.to_label(1, %{"label" => "Alice", "age" => 30})
"Alice"
iex> Yog.Utils.to_label(2, %{})
"2"

to_weight_label(weight)

@spec to_weight_label(any()) :: String.t()

Safely converts edge weight/data to a string label.

If data is a map, it looks for common weight/labeling keys ("weight", :weight, "label", :label). If not found, it returns an empty string.

Examples

iex> Yog.Utils.to_weight_label(5)
"5"
iex> Yog.Utils.to_weight_label(%{"weight" => "10", "type" => "road"})
"10"
iex> Yog.Utils.to_weight_label(%{})
""