Chapter (chapter v0.1.0)

Copy Markdown View Source

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")
378

All 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

Types

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".)

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

id()

@type id() :: String.t()

A raw chapter identifier as a source provides it.

Functions

before?(a, b)

@spec before?(id(), id()) :: boolean()

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(a, b)

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

Compare two identifiers: :lt, :eq, or :gt.

iex> Chapter.compare("10", "10.5")
:lt

iex> Chapter.compare("062", "62")
:eq

gt?(a, b)

@spec gt?(id(), id()) :: boolean()

Is a ahead of b in reading order? The "did the user advance / is this chapter newer" primitive.

iex> Chapter.gt?("430f", "430")
true

last_number(chapters)

@spec last_number([id()]) :: integer() | nil

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

number(ch)

@spec number(id()) :: integer() | 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(chapters, direction \\ :asc)

@spec sort([id()], :asc | :desc) :: [id()]

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"]

sort_key(ch)

@spec sort_key(id()) :: {number(), number()} | {number(), String.t()} | {0, id()}

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"}