Parse, compare, and sort the chapter identifiers scanlation sources actually use.
Chapter "numbers" in the wild aren't numbers. Real identifiers off
real sources: "10.5" (sub-chapter), "129e" / "62b" (letter
suffixes for extras and parts), "062" (zero-padded), "v30-c378"
(volume-chapter compounds). Naive string sorting breaks instantly
("10" < "9"), naive number parsing chokes on the rest — and
getting order wrong corrupts real behavior: prev/next navigation,
"which chapter is newer" resume pointers, new-chapter notifications,
caught-up detection.
iex> Chapter.sort(["10.5", "9", "129e", "10", "129"])
["9", "10", "10.5", "129", "129e"]
iex> Chapter.gt?("v30-c378", "v30-c377.5")
true
iex> Chapter.number("v30-c378")
378All functions are pure; identifiers are treated as opaque strings
everywhere except ordering. Series-level publication status
normalization lives in Chapter.Status.
Ordering semantics
sort_key/1 produces a term that orders correctly under Erlang
term comparison:
"vVOL-cNUM"→{volume, number}— volume-aware ordering- numeric-prefixed (
"10.5","129e","062") →{number, suffix}— numeric first, then the suffix lexically, so"129" < "129e" - anything unparsable →
{0, identifier}— clusters at the front, ordered lexically among itself
Comparing across formats (a v-c identifier against a bare number)
is inherently ambiguous; real series stick to one format, and keys
stay deterministic either way.
Summary
Functions
Is a strictly before b in reading order? (E.g. "is this chapter
already read given the user's resume pointer".)
Compare two identifiers: :lt, :eq, or :gt.
Is a ahead of b in reading order? The "did the user advance /
is this chapter newer" primitive.
The highest display number in a list — "133 chapters" style counts
that stay correct when sub-chapters (10.5, 12.5, …) inflate the
raw list length. nil for empty lists or when nothing parses
(callers usually fall back to length/1).
The display number of an identifier — the integer floor of its
chapter component, or nil when nothing parses.
Sort identifiers into reading order (:asc, the default) or reverse
(:desc).
A sort key for a chapter identifier — sort or compare chapters by this key (see the module doc for the semantics).
Types
@type id() :: String.t()
A raw chapter identifier as a source provides it.
Functions
Is a strictly before b in reading order? (E.g. "is this chapter
already read given the user's resume pointer".)
iex> Chapter.before?("10", "10.5")
true
Compare two identifiers: :lt, :eq, or :gt.
iex> Chapter.compare("10", "10.5")
:lt
iex> Chapter.compare("062", "62")
:eq
Is a ahead of b in reading order? The "did the user advance /
is this chapter newer" primitive.
iex> Chapter.gt?("430f", "430")
true
The highest display number in a list — "133 chapters" style counts
that stay correct when sub-chapters (10.5, 12.5, …) inflate the
raw list length. nil for empty lists or when nothing parses
(callers usually fall back to length/1).
iex> Chapter.last_number(["1", "2", "2.5", "3"])
3
iex> Chapter.last_number([])
nil
The display number of an identifier — the integer floor of its
chapter component, or nil when nothing parses.
iex> Chapter.number("v30-c378")
378
iex> Chapter.number("10.5")
10
iex> Chapter.number("extra")
nil
Sort identifiers into reading order (:asc, the default) or reverse
(:desc).
iex> Chapter.sort(["v1-c3", "v1-c1", "v1-c2.5"])
["v1-c1", "v1-c2.5", "v1-c3"]
iex> Chapter.sort(["9", "10", "8"], :desc)
["10", "9", "8"]
A sort key for a chapter identifier — sort or compare chapters by this key (see the module doc for the semantics).
iex> Chapter.sort_key("v2-c10.5")
{2.0, 10.5}
iex> Chapter.sort_key("129e")
{129.0, "e"}
iex> Chapter.sort_key("oneshot")
{0, "oneshot"}