# Shared rules and locales

Several tools answer the same questions: may this person still be alive, are these two names the same name, what date does this text give. Each tool used to keep its own copy of the answer, and the copies disagreed (one counted a person born 105 years ago as living, another did not). `gwt_core.rules` holds one answer for each, and `gwt_core/locales/` holds the words each language needs, so a new country is a table, not a code change in five tools.

It is a Python module, not a command. New tools import it; existing tools move to it one pull request at a time, so until a tool's page says it uses these rules, it still uses its own copy.

```
import gzip, json
from gwt_core.rules import possibly_living, fold, read_date, dedication, find_ids, Index

snapshot = json.load(gzip.open("tree.json.gz"))  # what `gwt-check dump tree.json.gz` wrote
ix = Index(snapshot)                    # build once, then ask about every person
person = snapshot["objects"]["people"][0]
possibly_living(person, ix)             # e.g. (True, "child-born-1990", 1976)
fold("Bermúdez de Castro", "es")        # "vermudez castro"
read_date("12 de marzo de 1890", "es")  # a Gramps Date dict for 1890-03-12
dedication("N.ª S.ª del Rosario", "es") # "nuestra senora del rosario"
find_ids("DNI 12345678Z")               # [("dni", "12345678Z")]
```

## Who may be living

`possibly_living(person, snapshot, years=110, gap=20, this_year=None, undated=True)` returns `(living, reason, bound_year)`. `reason` says why, so a tool can print it next to the person; `bound_year` is the latest year the person can have been born, from that evidence, or `None`. The rules run in this order and the first that applies decides:

| Rule | Result | Reason |
| --- | --- | --- |
| A death, burial or cremation of their own | not living | `death-recorded`, `burial-recorded`, `cremation-recorded` |
| A dated birth, baptism or christening of their own | living if less than `years` ago | `born-1950`, `baptised-1950` |
| A birth «after Y» with Y less than `years` ago | living | `born-after-1950` |
| One end of a DNA association (an association whose relation is `DNA`) | living | `dna-match` |
| Other dated evidence (below) | living if the tightest bound is less than `years` ago | `census-1851`, `child-born-1990` |
| Nothing dated | `undated` | `no-dates` |

Every date is read by the latest year it allows, so an uncertain date can only make a person more likely living: `bet 1900 and 1930` and `from 1900 to 1930` count as 1930, `abt` and `bef` as the year written, and `aft 1900` gives no upper bound at all (it is living only when 1900 itself is inside the window). A date in another calendar (Julian, Hebrew, Islamic, Persian, French Republican) is read through its `sortval`, so an Islamic 1400 counts as 1979.

Other evidence, each the latest year the person can have been born. A relative's year is the earliest dated event of theirs, of any type: a child who died in 1750 was born by 1750.

| Evidence | Bound | Reason |
| --- | --- | --- |
| Any other dated event of their own in Y (census, residence, occupation, will) | Y | `census-1851` |
| A child's event in Y | Y - 14 | `child-born-1990`, `child-death-1750` |
| A grandchild's event in Y | Y - 28 | `grandchild-born-1990` |
| A marriage of their own in Y | Y - 14 | `married-1905` |
| A spouse's event in Y | Y + `gap` (20) | `spouse-born-1800` |
| A sibling's event in Y | Y + `gap` (20) | `sibling-born-1880` |
| A parent's event in Y | Y + 60 | `parent-born-1700` |

Only the person's own events count for the first two rules (role `Primary`, or no role): a godparent at a 2001 baptism was not born in 2001. A DNA match is a person who took a test, so a match with no dates counts as living even with `undated=False`; a recorded date still wins over the association.

`snapshot` is a `gwt-check dump` (`{"objects": {"people": [...], "families": [...], "events": [...]}}`), a gwt-apply snapshot (`{"objects": {"Person": [...], ...}}`), the inner maps alone, or the same maps keyed by handle. Asking about many people, build `Index(snapshot)` once and pass it: given a raw snapshot, every call builds its own index, which on a tree of 20,000 people turns a fraction of a second into minutes.

A tool that prints a list of possibly living people should group it by reason. In a tree with many DNA matches, `dna-match` can be most of the list and bury the people the rule inferred from dates.

## Folding names

`fold(text)` lowercases, removes accents and turns punctuation into spaces. `fold(text, locale)` also drops the locale's particles as whole words (`de`, `van`, `von`) and then applies its spelling pairs, so old and new spellings compare equal: `b`/`v` and `y`/`i` in Spanish, `ij`/`y` in Dutch, `j`/`i` and `v`/`u` in Latin, `tx`/`ch` in Basque, `x`/`j` in Galician. A folded name is for comparing, never for showing.

## Reading dates

`read_date(text)` reads gwt-apply's date grammar and returns the same Gramps Date dict as gwt-apply: `1890`, `1890-03-12`, `abt 1890`, `bef 1890-03`, `est 1890`, `bet 1880 and 1890`, `from 1880 to 1890`. Anything else raises `DateError`, so a date is never stored as text.

With a locale, the date may also be written out: `12 de marzo de 1890` (es), `March 12th, 1890` (en), `le 1er août 1801` (fr), `12. März 1890` (de), `die 3 mensis Ianuarii anno Domini 1701` (la), `7bre 1750` (es, the old abbreviation for September). An `abt`, `bef` or `aft` in front still works. A written date must have exactly one month word, a year of three or four digits and at most one day from 1 to 31; anything more is refused. Not read yet, and refused rather than guessed: numeric forms such as `12/03/1890` and `12-III-1890`, a day in words (`primero de enero`), and `1.º de marzo`. A note may be in a neighbouring language, so a caller that does not know it tries each likely locale in turn (`es`, then `gl`, then `pt`).

## Locale tables

`gwt_core/locales/<code>.toml`, one per language: `es`, `en`, `fr`, `it`, `pt`, `de`, `nl`, `ca`, `eu`, `gl`, `la`. Every file has the same sections, and a test fails when one does not:

| Section | Holds |
| --- | --- |
| `fold` | `pairs` (spelling swaps) and `particles` (words dropped from names) |
| `months` | `"1"` to `"12"`, each with the names and abbreviations a record uses |
| `dates` | `filler`, the words a written date has around its numbers (`de`, `le`, `anno`) |
| `roles` | the words for `godparent`, `witness`, `father`, `mother`, `child`, `spouse`, `priest`, and `of`, the words that join a role to the person it is relative to (`de` in «hija de», `of` in «son of»); `of` may be empty where a suffix does that work (Basque) |
| `events` | the words for `birth`, `baptism`, `marriage`, `death`, `burial` |
| `dedication` | `pairs` of a church-name abbreviation and its full words (`N.ª S.ª` and `Nuestra Señora`, `St.` and `Saint`) |

Words are written as the records write them, with accents; the code folds both sides before comparing. A month word must name one month only within its file.

`dedication(text, locale)` folds a church name and writes out its abbreviations, whole words only, so `Stoke` stays `stoke`.

`find_ids(text, kinds=None)` returns each identity number whose check digit or issuing rules hold: the Spanish DNI and NIE, the French NIR, the Italian codice fiscale and the United States SSN. An identity number belongs to a country, not to the language of the text, so every kind is looked for in every text (a DNI in an English note is found); `kinds=("dni", "nie")` limits the search. A number with a wrong check digit is not reported, so a dated file name such as `20230115B` is not taken for an ID. Only ASCII digits count.

To add a language, copy a file, translate every section, add its code to `LOCALES` in `rules.py`, and run `cd core && python3 -m pytest -q`. To add an identity number, add its pattern and its check to `ID_KINDS` in `rules.py`.
