Skip to content

Commit e73428c

Browse files
authored
Merge pull request #1 from lupodevelop/first-implementation
First implementation
2 parents 46959aa + 7f6bce4 commit e73428c

6 files changed

Lines changed: 710 additions & 2 deletions

File tree

.github/workflows/test.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: test
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
- main
8+
pull_request:
9+
10+
jobs:
11+
test:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: erlef/setup-beam@v1
16+
with:
17+
otp-version: "28"
18+
gleam-version: "1.13.0"
19+
rebar3-version: "3"
20+
# elixir-version: "1"
21+
- run: gleam deps download
22+
- run: gleam test
23+
- run: gleam format --check src test

README.md

Lines changed: 150 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,150 @@
1-
# cmp
2-
Explicit equality & ordering lib for Gleam
1+
# cmp_gleam
2+
3+
[![Package Version](https://img.shields.io/hexpm/v/cmp_gleam)](https://hex.pm/packages/cmp_gleam)
4+
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/cmp_gleam/)
5+
[![CI](https://github.com/lupodevelop/cmp/workflows/test/badge.svg)](https://github.com/lupodevelop/cmp/actions)
6+
7+
cmp_gleam: explicit comparator helpers for Gleam
8+
9+
cmp_gleam is a tiny, focused library for building small, composable comparators in Gleam. It intentionally avoids magic (no derives, no hidden behavior) so that comparison logic remains explicit, easy to test and easy to reason about.
10+
11+
Key goals:
12+
13+
- Keep the API small and predictable
14+
- Provide composable building blocks for lexicographic and tie-breaker ordering
15+
- Allow optional normalization/folding (for example using the `str` library) without adding runtime dependencies
16+
17+
Why this approach?
18+
19+
`cmp` favors explicitness over implicit derivation. By making comparators first-class and composable you get predictable behaviour, easier testing, and straightforward integration with normalization libraries when you need to handle real-world Unicode data.
20+
21+
Install
22+
23+
```sh
24+
gleam add cmp_gleam
25+
```
26+
27+
Quick examples 🔧
28+
29+
1. Sort integers
30+
31+
```gleam
32+
import cmp
33+
import gleam/list
34+
35+
pub fn sort_ints(xs: List(Int)) -> List(Int) {
36+
list.sort(xs, by: cmp.natural_int)
37+
}
38+
```
39+
40+
2. Sort records by a string field (contramap)
41+
42+
```gleam
43+
import cmp
44+
import gleam/list
45+
import gleam/string
46+
47+
type User {
48+
User(name: String, age: Int)
49+
}
50+
51+
pub fn sort_by_name(users: List(User)) -> List(User) {
52+
let cmp_name = cmp.by(fn(u) { case u { User(name, _) -> name } }, string.compare)
53+
list.sort(users, by: cmp_name)
54+
}
55+
```
56+
57+
3. Lexicographic ordering / tie-breakers (chain / then / lazy_then)
58+
59+
```gleam
60+
let cmp = cmp.chain([
61+
cmp.by(fn(u) { case u { User(name, _) -> name } }, string.compare),
62+
cmp.by(fn(u) { case u { User(_, age) -> age } }, cmp.natural_int)
63+
])
64+
list.sort(users, by: cmp)
65+
```
66+
67+
4. Integrating with `str` for normalization (e.g. ASCII-fold)
68+
69+
`str` is optional — `cmp` does not import it. You can pass `str` functions to `cmp` APIs such as `by_normalized_string` to fold/normalize before comparing:
70+
71+
```gleam
72+
import cmp
73+
import gleam/list
74+
import gleam/string
75+
import str.extra
76+
77+
pub fn sort_by_name_ascii_fold(users: List(User)) -> List(User) {
78+
let normalize = str.extra.ascii_fold
79+
let cmp_name = cmp.by_normalized_string(fn(u) { case u { User(name, _) -> name } }, normalize, string.compare)
80+
list.sort(users, by: cmp_name)
81+
}
82+
```
83+
84+
You can also combine normalization steps (for example: fold + lowercase):
85+
86+
```gleam
87+
let normalize = fn(s) { s |> str.extra.ascii_fold |> string.lowercase }
88+
```
89+
90+
5. Using metrics (similarity/distance) to order items (advanced example)
91+
92+
⚠️ **Warning**: similarity/distance metrics don't guarantee transitivity (a < b and b < c doesn't always imply a < c). If you need a total order, sort by the metric value itself rather than using it directly as a comparator.
93+
94+
```gleam
95+
import gleam/order
96+
97+
// Example: sort by similarity to a reference string (careful: not a total order!)
98+
let similarity_cmp = fn(a, b) {
99+
let s = str.similarity(a, b)
100+
case s > 0.8 { True -> order.Lt False -> order.Gt }
101+
}
102+
let cmp_sim = cmp.by(fn(u) { case u { User(name, _) -> name } }, similarity_cmp)
103+
```
104+
105+
Better approach for metrics:
106+
107+
```gleam
108+
// Compute similarity as Float, then sort by that value
109+
let reference = "Alice"
110+
let with_similarity = list.map(users, fn(u) {
111+
let sim = str.similarity(u.name, reference)
112+
#(u, sim)
113+
})
114+
let sorted = list.sort(with_similarity, by: cmp.by(fn(pair) { pair.1 }, float.compare))
115+
```
116+
117+
Unicode & normalization notes ⚠️
118+
119+
- `string.compare` compares strings as-is; composed vs decomposed characters may behave differently if not normalized.
120+
- For user-facing sorting (names, titles), it is recommended to **normalize** (NFC/NFD) or apply folding (remove accents) before comparing.
121+
- `str` provides useful primitives (`str.extra.ascii_fold`, `str.core.normalize_whitespace`, etc.) which you can pass directly to `cmp`.
122+
123+
Performance tip: for large lists, precompute normalized keys
124+
125+
When sorting large lists by normalized strings, calling `normalize` on every comparison is expensive. Use the decorate-sort-undecorate pattern:
126+
127+
```gleam
128+
import gleam/list
129+
130+
// Precompute normalized keys once
131+
let decorated = list.map(users, fn(u) {
132+
let normalized_name = str.extra.ascii_fold(u.name)
133+
#(u, normalized_name)
134+
})
135+
136+
// Sort by the precomputed key
137+
let sorted_decorated = list.sort(decorated, by: cmp.by(fn(pair) { pair.1 }, string.compare))
138+
139+
// Extract the original values
140+
let sorted_users = list.map(sorted_decorated, fn(pair) { pair.0 })
141+
```
142+
143+
Further documentation and examples will be published on <https://hexdocs.pm/cmp_gleam>.
144+
145+
## Development
146+
147+
```sh
148+
gleam run # Run the project
149+
gleam test # Run the tests
150+
```

gleam.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name = "cmp_gleam"
2+
version = "1.0.0"
3+
4+
# Fill out these fields if you intend to generate HTML documentation or publish
5+
# your project to the Hex package manager.
6+
#
7+
description = "Small, explicit comparator utilities for Gleam (Comparator combinators: by, chain, option, list_compare...)"
8+
licences = ["MIT"]
9+
repository = { type = "github", user = "lupodevelop", repo = "cmp" }
10+
links = [{ title = "Repository", href = "https://github.com/lupodevelop/cmp" }]
11+
#
12+
# For a full reference of all the available options, you can have a look at
13+
# https://gleam.run/writing-gleam/gleam-toml/.
14+
15+
[dependencies]
16+
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
17+
18+
[dev-dependencies]
19+
gleeunit = ">= 1.0.0 and < 2.0.0"

manifest.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# This file was generated by Gleam
2+
# You typically do not need to edit this file
3+
4+
packages = [
5+
{ name = "gleam_stdlib", version = "0.67.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6CE3E4189A8B8EC2F73AB61A2FBDE49F159D6C9C61C49E3B3082E439F260D3D0" },
6+
{ name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" },
7+
]
8+
9+
[requirements]
10+
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
11+
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }

0 commit comments

Comments
 (0)