Skip to main content

README.md

# Chapter

Parse, compare, and sort the **chapter identifiers scanlation sources
actually use** — into correct reading order.

Chapter "numbers" in the wild aren't numbers. Real identifiers from real
sources: `10.5` (sub-chapter), `129e` / `62b` (letter-suffixed extras and
parts), `062` (zero-padded), `430f`, `v30-c378` (volume-chapter
compounds). Naive string sorting breaks instantly (`"10" < "9"`), naive
number parsing chokes on the rest — and wrong ordering silently corrupts
real behavior: prev/next navigation, resume pointers, new-chapter
notifications, caught-up detection.

```elixir
Chapter.sort(["10.5", "9", "129e", "10", "129"])
#=> ["9", "10", "10.5", "129", "129e"]

Chapter.gt?("v30-c378", "v30-c377.5")   #=> true — is this chapter newer?
Chapter.compare("062", "62")            #=> :eq — zero padding insignificant
Chapter.number("v30-c378")              #=> 378 — display number
Chapter.last_number(~w(1 2 2.5 3))      #=> 3  — count that survives sub-chapters
```

Pure functions, zero dependencies. Identifiers stay opaque strings —
only their *order* is interpreted.

---

## Install

```elixir
def deps do
  [
    {:chapter, "~> 0.1"}
  ]
end
```

## API

| Function | Answers |
|---|---|
| `Chapter.sort_key/1` | a term that orders correctly under `Enum.sort_by` / term comparison |
| `Chapter.sort/1,2` | reading order (`:asc` / `:desc`) |
| `Chapter.compare/2` | `:lt` / `:eq` / `:gt` |
| `Chapter.gt?/2`, `Chapter.before?/2` | "is this chapter newer / already read?" |
| `Chapter.number/1` | display integer (`"v30-c378"` → `378`) |
| `Chapter.last_number/1` | highest display number in a list ("133 chapters" that isn't inflated by `10.5`s) |
| `Chapter.Status.normalize/1` | series publication status → `ongoing / completed / hiatus / unknown` (EN + RU variants) |

## Ordering semantics

`sort_key/1` maps each identifier to a comparison-safe term:

* `vVOL-cNUM` → `{volume, number}` — volume-aware
* numeric-prefixed → `{number, suffix}` — so `129 < 129e`, `062 == 62`
* unparsable (`"extra"`, `"oneshot"`) → `{0, identifier}` — clusters at
  the front, lexical among itself

Cross-format comparisons (a `v-c` compound vs a bare number) are
inherently ambiguous; real series stick to one format, and keys stay
deterministic either way.

## Fits with

Built for comic/manga readers — it's the ordering half of implementing
a [`manhwa`](https://hex.pm/packages/manhwa) /
[`manga`](https://hex.pm/packages/manga) Store (`list_chapters` must be
reading-ordered), next to [`dims`](https://hex.pm/packages/dims) for the
page-dimensions half.

## License

MIT