Add the table node, row selection and linked tables, and ship the misc registers as tables #19

Merged
lilleman merged 22 commits from table-node into main 2026-09-17 23:35:27 +02:00
47 changed files with 3379 additions and 291 deletions
+17
View File
@@ -9,3 +9,20 @@ replacement, and each removed path, column or flag.
### Added ### Added
- First release: the CLI, the library and the shipped data set. - First release: the CLI, the library and the shipped data set.
- Table categories: a category JSON naming a `rows` TSV beside it, with the options
`key`, `name`, `weight` and `parent`; a path selects a row by key or name,
`misc.country[SE]`, and descends to a linked table by name; linked tables draw
consistently within one render and draw group. `rows` is an option, so no
template may carry a field of that name. Refused at `New`: a `name` without a
`key`, a name spelling another row's key, a table named like a column of any
table above it, a table whose format or cell references its own family, and,
within one render and draw group, a path drawing a table another path selects a
row of, or two paths pinning different rows of one table.
- `New` refuses a root choice of templates sharing one format and one set of string
fields, naming the rows TSV to write instead.
- `misc.country`, `misc.currency`, `misc.language`, `misc.httpstatus` and
`misc.mimetype` are tables. `misc.country` is every ISO 3166 country that has a
capital, a currency and a TLD, with the columns `calling-code`, `capital`,
`currency`, `flag`, `languages`, `numeric` and `tld` added; `misc.currency` the
current ISO 4217 currencies with a minor unit, with `decimals` and `numeric`
added, and its symbols from CLDR. `DATA-LICENSES.md` lists each source.
+14
View File
@@ -0,0 +1,14 @@
# Data licenses
Every shipped dataset, its source, its licence and the attribution it asks for. A
`data-import/` script rebuilds each sourced table; a curated one is hand-written.
| Table | Source | Licence | Attribution | Rebuild |
|-------|--------|---------|-------------|---------|
| `misc/country.tsv` | [datasets/country-codes](https://github.com/datasets/country-codes) | [PDDL 1.0](https://opendatacommons.org/licenses/pddl/1-0/) | none required | `data-import/country.py` |
| `misc/currency.tsv` | [datasets/currency-codes](https://github.com/datasets/currency-codes); symbols from [Unicode CLDR](https://github.com/unicode-org/cldr) `en.xml` and `root.xml` | PDDL 1.0; [Unicode License v3](https://www.unicode.org/license.txt) | CLDR: "Copyright © 1991-2025 Unicode, Inc. Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the United States and other countries." | `data-import/currency.py` |
| `misc/httpstatus.tsv` | curated (IANA HTTP status codes are facts) | — | — | — |
| `misc/language.tsv` | curated (ISO 639-1 codes are facts) | — | — | — |
| `misc/mimetype.tsv` | curated (IANA media types are facts) | — | — | — |
Every other category is hand-written JSON under [`data/`](data), MIT like the code.
+217 -14
View File
@@ -17,6 +17,7 @@ fejkdata sv_SE.person.last # Eriksson
fejkdata --seed 42 sv_SE.address # the same address every run fejkdata --seed 42 sv_SE.address # the same address every run
fejkdata -n 3 --separator ', ' sv_SE.word # nät, barn, sol fejkdata -n 3 --separator ', ' sv_SE.word # nät, barn, sol
fejkdata --list # every path the data offers fejkdata --list # every path the data offers
fejkdata 'misc.country[SE].capital' # Stockholm — a table's row, selected by key or name
fejkdata --data-path ./mydata sv_SE.word # layer a directory over the shipped data fejkdata --data-path ./mydata sv_SE.word # layer a directory over the shipped data
fejkdata --no-shipped-data -d ./mydata --list # only your data fejkdata --no-shipped-data -d ./mydata --list # only your data
fejkdata 'name: {/sv_SE.person.last}' # name: <a surname> — an inline template fejkdata 'name: {/sv_SE.person.last}' # name: <a surname> — an inline template
@@ -24,7 +25,8 @@ fejkdata '{"format":"name: {x}","x":["bosse","lina"]}' # name: bosse or name: l
``` ```
A path names a category, or a field inside one: each dot segment descends one A path names a category, or a field inside one: each dot segment descends one
level — folders, then the category (a JSON file), then fields. An argument that is level — folders, then the category (a JSON file), then fields — and `[SE]` after a
[table](#table) selects its row. An argument that is
a JSON object, array or string, or that carries a `{` token, is instead an a JSON object, array or string, or that carries a `{` token, is instead an
**inline template**: a format string or a JSON value compiled and rendered on the **inline template**: a format string or a JSON value compiled and rendered on the
spot. Its tokens reach the data by reference from the root — spot. Its tokens reach the data by reference from the root —
@@ -32,8 +34,8 @@ spot. Its tokens reach the data by reference from the root —
available. An inline template sits in no folder, so the folder-relative `{.name}` available. An inline template sits in no folder, so the folder-relative `{.name}`
and `{..name}` are rejected naming the root spelling, and one reference alone — and `{..name}` are rejected naming the root spelling, and one reference alone —
`{/sv_SE.person}` — is the path written as a template, rejected naming the path, as is `{/sv_SE.person}` — is the path written as a template, rejected naming the path, as is
a path written `/sv_SE.person`. A path never contains a brace, a bracket or a quote, a path written `/sv_SE.person`. A path never contains a brace or a quote, and a
so the two cannot collide (see [Decisions](#decisions)). bracket only as a selector after a name, so the two cannot collide (see [Decisions](#decisions)).
| Flag | | | Flag | |
|------|--| |------|--|
@@ -220,7 +222,10 @@ Each locale carries `address`, `color`, `company`, `date`, `email`, `ip`,
`country` (ISO 3166), `creditcard` (Luhn-valid), `currency` (ISO 4217), `emoji`, `country` (ISO 3166), `creditcard` (Luhn-valid), `currency` (ISO 4217), `emoji`,
`httpstatus`, `language` (ISO 639), `mac`, `mimetype`, `objectid`, `timezone` `httpstatus`, `language` (ISO 639), `mac`, `mimetype`, `objectid`, `timezone`
(IANA), `useragent` and `uuid` (v4). Many carry sub-fields — `misc.currency.symbol`, (IANA), `useragent` and `uuid` (v4). Many carry sub-fields — `misc.currency.symbol`,
`misc.country.alpha2`, `misc.httpstatus.code` — which `--list` shows. `misc.country.alpha2`, `misc.httpstatus.code` — which `--list` shows. `country`,
`currency`, `httpstatus`, `language` and `mimetype` are [tables](#table), so
`misc.country[SE].capital` and `misc.currency[Euro].symbol` select a row;
[`DATA-LICENSES.md`](DATA-LICENSES.md) names each table's source and licence.
## Data format ## Data format
@@ -231,6 +236,7 @@ Every value is a **node**, nestable without limit:
| string | `"Malmö"` | its text, with any `{…}` tokens expanded | | string | `"Malmö"` | its text, with any `{…}` tokens expanded |
| choice | `["a", "b", …]` | one item, picked at random | | choice | `["a", "b", …]` | one item, picked at random |
| template | `{"format": "…", …}` | its format, with `{name}` tokens rendering the named fields | | template | `{"format": "…", …}` | its format, with `{name}` tokens rendering the named fields |
| table | `{"format": "…", "rows": "x.tsv", …}` | its format over one row of the TSV beside it ([Table](#table)) |
### Format string ### Format string
@@ -340,10 +346,114 @@ renders a null as `""`. The other items' weights skew its odds:
load: `null` anywhere but a column, naming `""`, and a column whose items hold load: `null` anywhere but a column, naming `""`, and a column whose items hold
different datatypes. different datatypes.
### Table
A table is a category whose rows come from a TSV beside its JSON file: the header
names the columns, each line below it is one row, and the format renders the row
drawn. Save `mydata/country.tsv` and `mydata/country.json`:
```tsv
alpha2 name population
DK Denmark 5900000
NO Norway 5500000
SE Sweden 10500000
```
```json
{ "format": "{name} ({alpha2})", "rows": "country.tsv", "key": "alpha2", "name": "name", "weight": "population" }
```
```sh
fejkdata -d ./mydata country # Sweden (SE), about half the time
fejkdata -d ./mydata country.alpha2 # NO
fejkdata -d ./mydata 'country[SE]' # Sweden (SE)
fejkdata -d ./mydata 'country[Norway].alpha2' # NO
fejkdata -d ./mydata --format csv country # alpha2,name,population → SE,Sweden,10500000
```
`rows` names the TSV beside the category file; `key` names the column a path
selects a row by, `name` a column it also selects by, `weight` a column of positive
numbers that skews the draw, and `parent` the table a column links to
([Linked tables](#linked-tables)). The format's `{tokens}` read the columns, and the
columns are the [record](#records)'s columns, so `--format csv` writes the rows and
`--list` shows `country.alpha2`. A cell is a string node: `1{digits(2)} {digits(2)}`
in a cell draws digits and `{/misc.uuid}` reads a reference, while `{name}` in a cell
is refused, since a cell has no sibling. Each cell may select its own row of another
table, `{/misc.currency[SEK].symbol}` on one row and `{/misc.currency[EUR].symbol}`
on the next, since only one row renders. `New` proves the header, the options and every
cell token, and refuses a TSV no category names, a key that is empty or repeats, a
weight that is not a positive number, and a key or name holding `[`, `]`, `{`, `}`,
`"` or `|`, which a selector cannot spell; the rows are indexed on the first draw that
selects one. A `name` needs a `key`, since a name naming several rows is reported by
their keys, and a name spelling another row's key is refused, since the key would
select first and the name never. The table's options are its own — `rows`, `key`,
`name`, `weight` and `parent` — so a column may be named `name`, as one usually is.
A choice of templates sharing one format and one set of string fields is a table
written by hand, and `New` refuses it in a data file naming the TSV to write; an
inline template has no file beside it, so there it stays a choice.
### Row selection
`[key]` or `[name]` after a table's name selects one row: `misc.country[SE]` and
`misc.country[Sweden]` name one row, and `misc.country[SE].capital` reads its column.
A name naming several rows is an error listing their keys, unless a row selected
before it settles which ([Linked tables](#linked-tables)). A selector is part of the path, so it works
wherever a path does: `Fake`, `FakeRecord`, a `{/misc.country[SE].capital}` reference
and a struct tag. A dot inside the brackets belongs to the key or name, so
`city[St. Louis]` selects it. A path starts with a name, and `[` still opens a JSON
array at the start of a CLI argument, so `'[SE]'` alone names nothing.
### Linked tables
A table's `parent` names a column and, by the same name, the table beside it that the
column links to by key. Save `mydata/city.tsv` and `mydata/city.json` beside the
`country` table above:
```tsv
name country population
Copenhagen DK 660000
Göteborg SE 600000
Oslo NO 710000
Stockholm SE 990000
```
```json
{ "format": "{name}", "rows": "city.tsv", "key": "name", "parent": "country", "weight": "population" }
```
```sh
fejkdata -d ./mydata 'country[SE].city' # Stockholm or Göteborg
fejkdata -d ./mydata country.city.name # a country drawn, then a city inside it
fejkdata -d ./mydata 'city[Oslo].country' # NO — the link column's cell
fejkdata -d ./mydata '{/city.name}, {/country.name}' # Oslo, Norway — one consistent draw
```
A path descends from a row to a linked table by name, at any depth, and `--list`
advertises each direct step. Within one render and [draw group](#draw-group), linked
tables agree: the first table a reference path reads pins its ancestors, and a
descendant read after it is drawn inside them, so `{/city.name}` and
`{/country.name}` are a city and its country whichever is read first. A selected row
pins the render the same way, so every reference path into one family of linked
tables in one render and group selects the same rows: one that selects none beside
one that does is refused naming the spelling that does, `{/country[SE].city.name}`
beside `{/country[SE].name}`, and two selecting different rows are refused naming a
`drawGroup` to draw them apart in. A bare `{/city}` beside a path into its family is
refused too, since a bare reference draws each time, and so is a path that draws a
table another path in the group selects a row of, `{/city.name}` beside
`{/country[SE].name}`, which the first token rendered would otherwise decide. `New`
also refuses a link cell that is no key of the parent, a parent row no child links
to, a chain of parents that closes, a table named like a column of any table above
it, and a cell or format of a table that references a table of its own family, through
any template, a `repeat` or a `drawGroup` included, since a row rendered whole would
draw the family apart from itself: read the family from a template beside it, or add
the value as a column.
### Options and fields ### Options and fields
`format`, `weight`, `repeat`, `separator`, `datatype` and `drawGroup` are the only options; `format`, `weight`, `repeat`, `separator`, `datatype` and `drawGroup` are the only options;
**any other key is a field** (see [Decisions](#decisions)). An object that does nothing a **any other key is a field** (see [Decisions](#decisions)), and `rows` makes a category
a [table](#table), so no template carries a field of that name. An object that does nothing a
string can't — only a `format` — is rejected naming the string, as is a one-item string can't — only a `format` — is rejected naming the string, as is a one-item
choice naming its item. choice naming its item.
@@ -470,10 +580,11 @@ its groups by name; the unnamed group spans them all.
Renders e.g. `Sara Eriksson pays Ebba Lind; signed Eriksson`: the signature reads the Renders e.g. `Sara Eriksson pays Ebba Lind; signed Eriksson`: the signature reads the
payer's draw, while the payee is drawn apart. Rejected at load, each naming nothing: a payer's draw, while the payee is drawn apart. Rejected at load, each naming nothing: a
`drawGroup` of `""` (the default); one naming the draw group its template already draws `drawGroup` of `""` (the default); one naming the draw group its template already draws
in; one on a template that renders no reference path short of a `repeat` or a nested in; one on a template that renders no reference path — a bare reference to a
`drawGroup`, on a `repeat` itself — each iteration renders in no draw group — or on an [table](#table) counts, since the group answers for the family it draws in — short of a
inline template's root, which nothing references. So is a path reading into a level that `repeat` or a nested `drawGroup`, on a `repeat` itself — each iteration renders in no
carries a `drawGroup`. draw group — or on an inline template's root, which nothing references. So is a path
reading into a level that carries a `drawGroup`.
### Correlated fields ### Correlated fields
@@ -538,7 +649,7 @@ a minor only adds, and a major is the only release that changes what exists.
| Surface | Major | Minor | | Surface | Major | Minor |
|---------|-------|-------| |---------|-------|-------|
| Shipped data | remove or rename a path; change a category's format; remove a value, or change a weight or a repeat; add a reference from one shipped category into another | a path outside a record's columns, a locale, a value in a list | | Shipped data | remove or rename a path; change a category's format; remove a value, or change a weight or a repeat; add a reference from one shipped category into another; change a table's key, name, weight or parent column, or remove a row | a path outside a record's columns, a locale, a value in a list, a row |
| Records | remove, rename, retype or add a column; let a column be null | a record, as a new category | | Records | remove, rename, retype or add a column; let a column be null | a record, as a new category |
| Data format | a fence: a spelling `New` rejects that it accepted; a template option, since it reserves a field name | a builtin | | Data format | a fence: a spelling `New` rejects that it accepted; a template option, since it reserves a field name | a builtin |
| CLI | remove or rename a flag, or change its default; change what an exit code means; change the framing a `--format` writes (header, quoting, statement shape), the `--list` layout, or what an error names | a flag, a format | | CLI | remove or rename a flag, or change its default; change what an exit code means; change the framing a `--format` writes (header, quoting, statement shape), the `--list` layout, or what an error names | a flag, a format |
@@ -559,8 +670,8 @@ with no breaking change. From `v2` the module path carries `/vN`, so fences ship
batched into as few majors as possible. batched into as few majors as possible.
[`testdata/shipped_shape.txt`](testdata/shipped_shape.txt) pins every path, each [`testdata/shipped_shape.txt`](testdata/shipped_shape.txt) pins every path, each
template category's format, the categories each category reads, and each column's template category's format, the categories each category reads, each column's
datatype and nullability; a pull request that datatype and nullability, and each table's key, name, weight and parent columns; a pull request that
changes it or `data/` adds its `CHANGELOG.md` entry, which CI checks. A removed, changes it or `data/` adds its `CHANGELOG.md` entry, which CI checks. A removed,
renamed or retyped line is a major. renamed or retyped line is a major.
@@ -796,6 +907,86 @@ renamed or retyped line is a major.
almost always costs an allocation too (a lost pre-size, a per-item map, an extra almost always costs an allocation too (a lost pre-size, a per-item map, an extra
copy). The benchmark suite (see Development) reports time for a human, not as a copy). The benchmark suite (see Development) reports time for a human, not as a
pass/fail gate. pass/fail gate.
- **Rows live in a TSV, the shape in JSON.** `New` allocates once per node, so a
register of thirty thousand rows written as JSON objects would cost it a second;
a TSV is one allocation whose cells are substrings, and the JSON says only how a
row is composed. The TSV sits beside its category file, named by `rows`, so a
data directory stays a directory of categories, and one nothing names is a
load error rather than a file silently ignored.
- **A selector is bracketed, and a dot inside it is literal.** `municipality[0180]`
reads as selection to anyone who has indexed an array, and `[St. Louis]` keeps a
name whole where a colon or a dot-separated spelling could not; zsh needs the
brackets quoted, which the README's examples show. A name that spells another
row's key is refused at load rather than shadowed: a key names one row by
contract, so the name could never select its own, and the check is one lookup per
row against the key index that already exists.
- **`parent` names the link column and the table alike.** One word says both, so a
child table sits in its parent's folder and links on a column of the parent's
name; the geo plan wants exactly that, and a table needing another folder or
another column name would be asking for a second spelling.
- **After a row, a path names a column or a linked table.** `locality[Lund].address`,
a template beside the family under a selected row, is refused today; admitting it
later is additive, since a refused spelling gains a meaning and no accepted one
changes, so the door stays open for the address records the plan describes.
- **A table read into is pinned; a table rendered whole draws afresh.** A path into a
table pins its row for the render and group, as a reference path pins its level,
and a bare `{/city}` draws each time, as a bare reference does; so a bare table
beside a path into its family is refused like a bare reference beside a path into
it. A bare table reference still counts as a read for a `drawGroup`, since the group
is what draws it apart from the family's pins.
- **Every reference path into one family selects the same rows, per render and
group.** A selector pins rows for the render, and a read that draws freely before it
could pin a row the selector contradicts, so accepting both would make the result
depend on which token rendered first. Requiring one selection per family per group
is checkable at load with no data lookup beyond the selectors themselves, and the
error names the rewrite. Two selectors naming one row by key and by name compare
equal, since the load check resolves them.
- **A parent row with no child row is a load error.** A descendant is drawn inside the
nearest pinned ancestor, so every ancestor row must lead to a row at every level
below it, or a render could find nothing to draw. The import script drops or fills
such rows; the alternative, falling back to a free draw, would break the consistency
the link exists for without saying so.
- **The choice-of-rows fence guards a data file's root, and requires string fields.**
A table is a category with a TSV beside its file, so only a root choice has the
spelling the fence names; a nested choice of same-shaped templates and an inline
one keep loading. Fields must all be strings because a cell is a string node: a
choice whose items carry a nested choice, as `misc.car` does, is not one table but
two linked ones, which a later conversion writes.
- **A table is a record of string columns.** Its columns are the CSV header and the
`INSERT` column list, fixed by the TSV header, so a table is a record by
construction; every column is a string until a typed column option earns its place.
- **The key index is built at load, the rest on first draw.** A link is proved
against the parent's keys and a key's uniqueness is a data mistake, so both are
load-time; the name index and the per-parent child lists serve only a draw or a
selection, so they wait for the first one, keeping `New` linear in the bytes read.
- **Two categories may name one TSV.** Each is a view of the file with its own
format and options, at the cost of holding the rows twice, which is what a
category over a register with two natural formats asks for; a TSV nothing names
stays a load error, since that one is a file forgotten rather than shared.
- **The rows of a table are alternatives.** Only one row renders, so a cell in one
row and a cell in another never meet, and each may select its own row of another
table; the cells of one row, and whatever they reach, do meet, and so does the
format beside them. The rule the fence applies is that two reads meet unless
they sit in two rows of one table, or in a row outside a selected ancestor's. A
choice's items get no such treatment yet: two items selecting different rows
are still refused.
- **A table never reaches its own family, by any route.** A `repeat` iteration and a
`drawGroup` each draw apart on purpose, but a row that lists three localities from
other regions is the output the family exists to prevent, so the own-family fence
walks through both rather than stopping where the draw fences do.
- **Tables carrying token cells stay small.** The family fence compares the reads of
every pair of rows that can render together, so a table whose every row's cell
selects a row of another table loads in time quadratic in its rows: about a
second at four thousand rows. No shipped table carries such cells, and a register
is a column set rather than a set of references, so the fence is left as it is
until a real data set needs the indexed form.
- **A path is walked once without drawing before it is walked for real.** A path
that fails below its first level then moves no seeded stream, at the cost of one
draw-free walk per call, which allocates nothing.
- **`List` advertises direct descents only.** `region.municipality.locality` is
listed, and `region.locality` resolves too but is not: the set of every descent
through a chain of five tables is every subsequence of it, and the direct chain is
the one a reader can predict from the tables' parents.
## Development ## Development
@@ -839,6 +1030,15 @@ in its own commit:
REPIN=1 docker compose run --rm --user "$(id -u):$(id -g)" test REPIN=1 docker compose run --rm --user "$(id -u):$(id -g)" test
``` ```
A shipped table built from a source is rebuilt by its script under
[`data-import/`](data-import), one command per dataset, fetching the source named in
[`DATA-LICENSES.md`](DATA-LICENSES.md):
```sh
docker compose run --rm --user "$(id -u):$(id -g)" data-import data-import/country.py
docker compose run --rm --user "$(id -u):$(id -g)" data-import data-import/currency.py
```
To release, head `CHANGELOG.md` with the version's section in place of `Unreleased` To release, head `CHANGELOG.md` with the version's section in place of `Unreleased`
and merge: once `main` passes the gate, CI tags that commit `vX.Y.Z` and publishes and merge: once `main` passes the gate, CI tags that commit `vX.Y.Z` and publishes
the Gitea release with the section as its body. A top heading of `[Unreleased]` the Gitea release with the section as its body. A top heading of `[Unreleased]`
@@ -849,7 +1049,8 @@ publishes nothing.
``` ```
fejkdata.go Generator, New, options, the embedded data set, List fejkdata.go Generator, New, options, the embedded data set, List
node.go the node model and JSON -> node compilation node.go the node model and JSON -> node compilation
path.go the dotted-path walk, and proving a path resolves table.go tables: the rows TSV, its options and links, row selection and draws
path.go the dotted-path walk with its selectors, and proving a path resolves
render.go Fake and the recursive renderer (choices, format strings, expansions) render.go Fake and the recursive renderer (choices, format strings, expansions)
record.go records: Record, the JSON/CSV/SQL serializers, and their entry points record.go records: Record, the JSON/CSV/SQL serializers, and their entry points
struct.go structs: FakeStruct, fake tags, and a field's Go type as its column's datatype struct.go structs: FakeStruct, fake tags, and a field's Go type as its column's datatype
@@ -857,6 +1058,7 @@ inline.go inline templates: Template, NewTemplate, FakeTemplate, IsTemplat
template.go the {token} grammar: scanning, tokens, operands, validation, compiling a format template.go the {token} grammar: scanning, tokens, operands, validation, compiling a format
hold.go the hold: one draw per expansion for paths and operands, and its fences hold.go the hold: one draw per expansion for paths and operands, and its fences
draw.go one reference draw per render and group: draw sets, the group option, and its fence draw.go one reference draw per render and group: draw sets, the group option, and its fence
family.go a family of linked tables: the rows a render pins, and the fence over paths into one family
reference.go reference sigils, and binding references across the tree reference.go reference sigils, and binding references across the tree
graph.go the render graph: edges, cycles, the repeat bound, tree walks graph.go the render graph: edges, cycles, the repeat bound, tree walks
builtins.go the {name()} function registry and its implementations builtins.go the {name()} function registry and its implementations
@@ -865,7 +1067,8 @@ datatype.go column datatypes: DataType, where datatype and null may sit, a c
value.go the value proof: what a typed column or calc operand holds, checked at load value.go the value proof: what a typed column or calc operand holds, checked at load
data.go data loading: fs.FS folders/files -> namespace tree, multi-source merge data.go data loading: fs.FS folders/files -> namespace tree, multi-source merge
cmd/fejkdata/ the fejkdata CLI cmd/fejkdata/ the fejkdata CLI
data/ shipped data (JSON), embedded at build: locale folders + a misc folder data/ shipped data (JSON, and a TSV per table), embedded at build: locale folders + a misc folder
data-import/ the scripts that rebuild each sourced table (see DATA-LICENSES.md)
release-tooling/ the release CI publishes from the changelog heading release-tooling/ the release CI publishes from the changelog heading
testdata/ the pinned shipped shape (see Versioning) testdata/ the pinned shipped shape (see Versioning)
``` ```
+19 -7
View File
@@ -23,16 +23,17 @@ import (
const usage = `Usage: fejkdata [flags] <path|template> const usage = `Usage: fejkdata [flags] <path|template>
<path> a category, or a dotted path into one (person, person.last) <path> a category, or a dotted path into one (person, person.last);
a table's row by key or name: 'misc.country[SE]', 'misc.country[Sweden].tld'
<template> a format string or JSON value to render inline, e.g. <template> a format string or JSON value to render inline, e.g.
'name: {/sv_SE.person.last}' or '{"format":"{x}","x":["bosse","lina"]}' 'name: {/sv_SE.person.last}' or '{"format":"{x}","x":["bosse","lina"]}'
An argument containing a { token, or a JSON object, array or string, is a An argument containing a { token, or a JSON object, array or string, is a
template; any other argument is a path (a path never contains a brace, a bracket template; any other argument is a path (a path never contains a brace or a quote,
or a quote). Templates reach the data by reference from the root — and a bracket only as a [selector] after a table's name). Templates reach the
{/sv_SE.person.last} — whether the data is shipped or layered with --data-path. An data by reference from the root — {/sv_SE.person.last} — whether the data is
argument carrying a bracket, a closing brace or a quote but no valid JSON names shipped or layered with --data-path. An argument carrying a closing brace or a
neither. quote but no valid JSON names neither.
With --format json, ndjson, csv or sql the argument must name a record — a With --format json, ndjson, csv or sql the argument must name a record — a
template whose fields are its columns — and the rows are written as one JSON template whose fields are its columns — and the rows are written as one JSON
@@ -396,7 +397,18 @@ func defaultTable(arg string, kind argKind) string {
if kind == argTemplate { if kind == argTemplate {
return "records" return "records"
} }
segments := strings.Split(arg, ".") var names strings.Builder // the path with its [selectors] cut out
for depth, i := 0, 0; i < len(arg); i++ {
switch {
case arg[i] == '[':
depth++
case arg[i] == ']':
depth--
case depth == 0:
names.WriteByte(arg[i])
}
}
segments := strings.Split(names.String(), ".")
return segments[len(segments)-1] return segments[len(segments)-1]
} }
+37 -4
View File
@@ -310,8 +310,41 @@ func TestClassify(t *testing.T) {
t.Errorf("classify(%q) = %v, %v; want %v", arg, got, err, want) t.Errorf("classify(%q) = %v, %v; want %v", arg, got, err, want)
} }
} }
if _, err := classify("[abc]"); err == nil || !strings.Contains(err.Error(), `holds a "["`) { if _, err := classify("[abc]"); err == nil || !strings.Contains(err.Error(), `"["`) || !strings.Contains(err.Error(), "JSON") {
t.Errorf("classify([abc]) = %v; want it rejected naming the bracket", err) t.Errorf("classify([abc]) = %v; want it rejected naming the leading bracket", err)
}
if got, err := classify("geo.SE.municipality[St. Louis].name"); err != nil || got != argPath {
t.Errorf("classify(a selecting path) = %v, %v; want a path", got, err)
}
}
func TestRunSelectsATableRow(t *testing.T) {
dir := t.TempDir()
for name, content := range map[string]string{
"region.json": `{"format":"{name}","rows":"region.tsv","key":"code","name":"name"}`,
"region.tsv": "code\tname\n01\tStockholms län\n12\tSkåne län\n",
} {
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
if code, out, errb := runOut("--no-shipped-data", "-d", dir, "region[12]"); code != 0 || out != "Skåne län\n" {
t.Fatalf("region[12] = %d, %q, stderr=%q", code, out, errb)
}
if code, out, _ := runOut("--no-shipped-data", "-d", dir, "region[Skåne län].code"); code != 0 || out != "12\n" {
t.Fatalf("region[Skåne län].code = %d, %q", code, out)
}
if code, out, _ := runOut("--no-shipped-data", "-d", dir, "--format", "sql", "region[12]"); code != 0 || out != `INSERT INTO "region" ("code", "name") VALUES ('12', 'Skåne län');`+"\n" {
t.Fatalf("--format sql region[12] = %d, %q, want the table named without its selector", code, out)
}
if code, _, errb := runOut("--no-shipped-data", "-d", dir, "region[99]"); code != 1 || !strings.Contains(errb, `"99"`) {
t.Fatalf("region[99] = %d, stderr=%q, want a runtime error naming the row", code, errb)
}
if code, _, errb := runOut("--no-shipped-data", "-d", dir, "[Skåne län]"); code != 2 || !strings.Contains(errb, "JSON") {
t.Fatalf("[Skåne län] = %d, stderr=%q, want misuse: a leading bracket that is no JSON names nothing", code, errb)
}
if code, out, _ := runOut("--seed", "1", "--format", "csv", "misc.country[SE]"); code != 0 || !strings.HasPrefix(out, "alpha2,") || !strings.Contains(out, "\nSE,SWE,") {
t.Fatalf("--format csv misc.country[SE] = %d, %q", code, out)
} }
} }
@@ -354,11 +387,11 @@ func TestRunInlineTemplate(t *testing.T) {
func TestRunTemplateMisuse(t *testing.T) { func TestRunTemplateMisuse(t *testing.T) {
for arg, want := range map[string]string{ for arg, want := range map[string]string{
"{bad": "unterminated", "{bad": "unterminated",
"[red,green]": "names no template either", "[red,green]": `starts with "["`,
`{"format":"x"}`: "is a string", `{"format":"x"}`: "is a string",
"name: {/no.such.path}": "no entry", "name: {/no.such.path}": "no entry",
"{/sv_SE.person}": "write sv_SE.person", "{/sv_SE.person}": "write sv_SE.person",
"x[1]": "names no template either", "x[1]y": `"]"`,
` ["a","b"] `: "may not be padded", ` ["a","b"] `: "may not be padded",
} { } {
code, out, errb := runOut("--seed", "1", arg) code, out, errb := runOut("--seed", "1", arg)
+7
View File
@@ -64,5 +64,12 @@ services:
<<: *go <<: *go
command: sh command: sh
data-import:
image: python:3.14.7-slim
working_dir: /app
volumes:
- .:/app
entrypoint: python3
volumes: volumes:
gocache: gocache:
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""Rebuild data/misc/country.tsv from datasets/country-codes (PDDL).
data-import/country.py [--source URL_OR_FILE] [--out FILE]
"""
import argparse
import csv
import io
import re
import sys
import urllib.request
from pathlib import Path
SOURCE = "https://raw.githubusercontent.com/datasets/country-codes/main/data/country-codes.csv"
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "country.tsv"
COLUMNS = ["alpha2", "alpha3", "calling-code", "capital", "currency", "flag", "languages", "name", "numeric", "tld"]
# Gaps in the source, keyed by alpha2.
FIXUPS = {"TR": {"currency": "TRY"}}
def read(source):
if re.match(r"^https?://", source):
with urllib.request.urlopen(source, timeout=60) as r:
return r.read().decode("utf-8")
return Path(source).read_text(encoding="utf-8")
def flag(alpha2):
return "".join(chr(0x1F1E6 + ord(c) - ord("A")) for c in alpha2)
def calling_code(dial):
first = re.sub(r"[^0-9-]", "", dial.split(",")[0])
return first if re.match(r"^\d+-\d{3}$", first) else first.split("-")[0]
def languages(field):
return ",".join(p for p in field.split(",") if p)
def rows(text):
for r in csv.DictReader(io.StringIO(text)):
alpha2 = r["ISO3166-1-Alpha-2"]
row = {
"alpha2": alpha2,
"alpha3": r["ISO3166-1-Alpha-3"],
"calling-code": calling_code(r["Dial"]),
"capital": r["Capital"],
"currency": r["ISO4217-currency_alphabetic_code"].split(",")[0],
"flag": flag(alpha2),
"languages": languages(r["Languages"]),
"name": r["CLDR display name"] or r["official_name_en"],
"numeric": r["ISO3166-1-numeric"].zfill(3),
"tld": r["TLD"],
}
row.update(FIXUPS.get(alpha2, {}))
if all(row[c] for c in ("alpha2", "alpha3", "calling-code", "capital", "currency", "name", "tld")):
yield row
def write(out, table):
lines = ["\t".join(COLUMNS)]
for row in sorted(table, key=lambda r: r["alpha2"]):
cells = [row[c] for c in COLUMNS]
assert not any("\t" in c or "\n" in c for c in cells), row
lines.append("\t".join(cells))
Path(out).write_text("\n".join(lines) + "\n", encoding="utf-8")
return len(lines) - 1
def main():
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
p.add_argument("--source", default=SOURCE)
p.add_argument("--out", default=str(OUT))
a = p.parse_args()
n = write(a.out, rows(read(a.source)))
print(f"{a.out}: {n} rows", file=sys.stderr)
if __name__ == "__main__":
main()
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Rebuild data/misc/currency.tsv from datasets/currency-codes (PDDL) and CLDR's symbols (Unicode).
data-import/currency.py [--source URL_OR_FILE] [--symbols URL_OR_FILE ...] [--out FILE]
The symbols come from the first locale file that has one, narrow symbols before wide.
"""
import argparse
import csv
import io
import re
import sys
import urllib.request
import xml.etree.ElementTree as ET
from pathlib import Path
SOURCE = "https://raw.githubusercontent.com/datasets/currency-codes/main/data/codes-all.csv"
SYMBOLS = [
"https://raw.githubusercontent.com/unicode-org/cldr/main/common/main/en.xml",
"https://raw.githubusercontent.com/unicode-org/cldr/main/common/main/root.xml",
]
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "currency.tsv"
COLUMNS = ["code", "decimals", "name", "numeric", "symbol"]
def read(source):
if re.match(r"^https?://", source):
with urllib.request.urlopen(source, timeout=60) as r:
return r.read().decode("utf-8")
return Path(source).read_text(encoding="utf-8")
def symbols(xml_texts):
"""CLDR's symbol per code: the first locale's narrow symbol, else the first locale's wide one."""
narrow, wide = {}, {}
for text in xml_texts:
for c in ET.fromstring(text).iter("currency"):
for s in c.findall("symbol"):
into = narrow if s.get("alt") == "narrow" else wide if s.get("alt") is None else None
if into is not None:
into.setdefault(c.get("type"), s.text)
return {**wide, **narrow}
def rows(text, symbol):
seen = set()
for r in csv.DictReader(io.StringIO(text)):
code = r["AlphabeticCode"]
if not code or code in seen or r["WithdrawalDate"] or r["Entity"].startswith("ZZ") or not r["MinorUnit"].isdigit():
continue
seen.add(code)
yield {
"code": code,
"decimals": r["MinorUnit"],
"name": r["Currency"],
"numeric": r["NumericCode"].zfill(3),
"symbol": symbol.get(code, code),
}
def write(out, table):
lines = ["\t".join(COLUMNS)]
for row in sorted(table, key=lambda r: r["code"]):
cells = [row[c] for c in COLUMNS]
assert all(cells) and not any("\t" in c or "\n" in c for c in cells), row
lines.append("\t".join(cells))
Path(out).write_text("\n".join(lines) + "\n", encoding="utf-8")
return len(lines) - 1
def main():
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
p.add_argument("--source", default=SOURCE)
p.add_argument("--symbols", nargs="+", default=SYMBOLS)
p.add_argument("--out", default=str(OUT))
a = p.parse_args()
n = write(a.out, rows(read(a.source), symbols(read(s) for s in a.symbols)))
print(f"{a.out}: {n} rows", file=sys.stderr)
if __name__ == "__main__":
main()
+42 -5
View File
@@ -62,6 +62,9 @@ func loadData(sources []dataSource) (map[string]node, error) {
if len(root) == 0 { if len(root) == 0 {
return nil, fmt.Errorf("no .json data found") return nil, fmt.Errorf("no .json data found")
} }
if err := linkTables(root); err != nil {
return nil, err
}
if err := linkRefs(root); err != nil { if err := linkRefs(root); err != nil {
return nil, err return nil, err
} }
@@ -83,22 +86,56 @@ func loadDir(src dataSource, dir string) (*folder, error) {
return nil, fmt.Errorf("%s: %w", src.name(dir), err) return nil, fmt.Errorf("%s: %w", src.name(dir), err)
} }
g := &folder{children: map[string]node{}} g := &folder{children: map[string]node{}}
files := &categoryFiles{src: src, dir: dir, tsv: map[string]bool{}}
for _, e := range entries {
if strings.HasSuffix(e.Name(), ".tsv") && !e.IsDir() {
files.tsv[e.Name()] = false
}
}
for _, e := range entries { for _, e := range entries {
if strings.HasPrefix(e.Name(), ".") { // hidden: a checkout or an editor's file, never data if strings.HasPrefix(e.Name(), ".") { // hidden: a checkout or an editor's file, never data
continue continue
} }
full := path.Join(dir, e.Name()) full := path.Join(dir, e.Name())
load := loadFile
if e.IsDir() { if e.IsDir() {
load = loadFolder if err := loadFolder(src, g, full, e.Name()); err != nil {
return nil, err
} }
if err := load(src, g, full, e.Name()); err != nil { continue
}
if err := loadFile(src, g, full, e.Name(), files); err != nil {
return nil, err return nil, err
} }
} }
for name, named := range files.tsv {
if !named && !strings.HasPrefix(name, ".") {
return nil, fmt.Errorf("%s: no category names it in its rows; a table's rows file sits beside a category file naming it", src.name(path.Join(dir, name)))
}
}
return g, nil return g, nil
} }
// categoryFiles is what a category may name beside itself: the rows files of its
// directory, each marked once a category names it.
type categoryFiles struct {
src dataSource
dir string
tsv map[string]bool
}
// rows reads the rows file a category names beside it.
func (c *categoryFiles) rows(name string) (string, error) {
if _, present := c.tsv[name]; !present {
return "", fmt.Errorf("rows names %s, which is not beside it in %s", name, c.src.name(c.dir))
}
c.tsv[name] = true
b, err := fs.ReadFile(c.src.fsys, path.Join(c.dir, name))
if err != nil {
return "", fmt.Errorf("%s: %w", c.src.name(path.Join(c.dir, name)), err)
}
return string(b), nil
}
// loadFolder adds a subdirectory as a nested folder, unless nothing under it is data. // loadFolder adds a subdirectory as a nested folder, unless nothing under it is data.
func loadFolder(src dataSource, g *folder, full, name string) error { func loadFolder(src dataSource, g *folder, full, name string) error {
child, err := loadDir(src, full) child, err := loadDir(src, full)
@@ -117,7 +154,7 @@ func loadFolder(src dataSource, g *folder, full, name string) error {
// loadFile compiles a *.json file into a category named after it; any other file // loadFile compiles a *.json file into a category named after it; any other file
// is skipped. // is skipped.
func loadFile(src dataSource, g *folder, full, file string) error { func loadFile(src dataSource, g *folder, full, file string, files *categoryFiles) error {
if !strings.HasSuffix(file, ".json") { if !strings.HasSuffix(file, ".json") {
return nil return nil
} }
@@ -133,7 +170,7 @@ func loadFile(src dataSource, g *folder, full, file string) error {
if err := json.Unmarshal(b, &raw); err != nil { if err := json.Unmarshal(b, &raw); err != nil {
return fmt.Errorf("%s: %w", src.name(full), err) return fmt.Errorf("%s: %w", src.name(full), err)
} }
n, err := compile(raw) n, err := compileCategory(raw, name, files)
if err != nil { if err != nil {
return fmt.Errorf("%s: %w", src.name(full), err) return fmt.Errorf("%s: %w", src.name(full), err)
} }
+1 -22
View File
@@ -1,22 +1 @@
[ { "format": "{name}", "rows": "country.tsv", "key": "alpha2", "name": "name" }
{ "format": "{name}", "name": "Australia", "alpha2": "AU", "alpha3": "AUS" },
{ "format": "{name}", "name": "Brazil", "alpha2": "BR", "alpha3": "BRA" },
{ "format": "{name}", "name": "Canada", "alpha2": "CA", "alpha3": "CAN" },
{ "format": "{name}", "name": "China", "alpha2": "CN", "alpha3": "CHN" },
{ "format": "{name}", "name": "Denmark", "alpha2": "DK", "alpha3": "DNK" },
{ "format": "{name}", "name": "Finland", "alpha2": "FI", "alpha3": "FIN" },
{ "format": "{name}", "name": "France", "alpha2": "FR", "alpha3": "FRA" },
{ "format": "{name}", "name": "Germany", "alpha2": "DE", "alpha3": "DEU" },
{ "format": "{name}", "name": "India", "alpha2": "IN", "alpha3": "IND" },
{ "format": "{name}", "name": "Italy", "alpha2": "IT", "alpha3": "ITA" },
{ "format": "{name}", "name": "Japan", "alpha2": "JP", "alpha3": "JPN" },
{ "format": "{name}", "name": "Mexico", "alpha2": "MX", "alpha3": "MEX" },
{ "format": "{name}", "name": "Netherlands", "alpha2": "NL", "alpha3": "NLD" },
{ "format": "{name}", "name": "Norway", "alpha2": "NO", "alpha3": "NOR" },
{ "format": "{name}", "name": "Poland", "alpha2": "PL", "alpha3": "POL" },
{ "format": "{name}", "name": "Spain", "alpha2": "ES", "alpha3": "ESP" },
{ "format": "{name}", "name": "Sweden", "alpha2": "SE", "alpha3": "SWE" },
{ "format": "{name}", "name": "Switzerland", "alpha2": "CH", "alpha3": "CHE" },
{ "format": "{name}", "name": "United Kingdom", "alpha2": "GB", "alpha3": "GBR" },
{ "format": "{name}", "name": "United States", "alpha2": "US", "alpha3": "USA" }
]
+242
View File
@@ -0,0 +1,242 @@
alpha2 alpha3 calling-code capital currency flag languages name numeric tld
AD AND 376 Andorra la Vella EUR 🇦🇩 ca Andorra 020 .ad
AE ARE 971 Abu Dhabi AED 🇦🇪 ar-AE,fa,en,hi,ur United Arab Emirates 784 .ae
AF AFG 93 Kabul AFN 🇦🇫 fa-AF,ps,uz-AF,tk Afghanistan 004 .af
AG ATG 1-268 St. John's XCD 🇦🇬 en-AG Antigua & Barbuda 028 .ag
AI AIA 1-264 The Valley XCD 🇦🇮 en-AI Anguilla 660 .ai
AL ALB 355 Tirana ALL 🇦🇱 sq,el Albania 008 .al
AM ARM 374 Yerevan AMD 🇦🇲 hy Armenia 051 .am
AO AGO 244 Luanda AOA 🇦🇴 pt-AO Angola 024 .ao
AR ARG 54 Buenos Aires ARS 🇦🇷 es-AR,en,it,de,fr,gn Argentina 032 .ar
AS ASM 1-684 Pago Pago USD 🇦🇸 en-AS,sm,to American Samoa 016 .as
AT AUT 43 Vienna EUR 🇦🇹 de-AT,hr,hu,sl Austria 040 .at
AU AUS 61 Canberra AUD 🇦🇺 en-AU Australia 036 .au
AW ABW 297 Oranjestad AWG 🇦🇼 nl-AW,pap,es,en Aruba 533 .aw
AX ALA 358 Mariehamn EUR 🇦🇽 sv-AX Åland Islands 248 .ax
AZ AZE 994 Baku AZN 🇦🇿 az,ru,hy Azerbaijan 031 .az
BA BIH 387 Sarajevo BAM 🇧🇦 bs,hr-BA,sr-BA Bosnia 070 .ba
BB BRB 1-246 Bridgetown BBD 🇧🇧 en-BB Barbados 052 .bb
BD BGD 880 Dhaka BDT 🇧🇩 bn-BD,en Bangladesh 050 .bd
BE BEL 32 Brussels EUR 🇧🇪 nl-BE,fr-BE,de-BE Belgium 056 .be
BF BFA 226 Ouagadougou XOF 🇧🇫 fr-BF,mos Burkina Faso 854 .bf
BG BGR 359 Sofia EUR 🇧🇬 bg,tr-BG,rom Bulgaria 100 .bg
BH BHR 973 Manama BHD 🇧🇭 ar-BH,en,fa,ur Bahrain 048 .bh
BI BDI 257 Gitega BIF 🇧🇮 fr-BI,rn Burundi 108 .bi
BJ BEN 229 Porto-Novo XOF 🇧🇯 fr-BJ Benin 204 .bj
BL BLM 590 Gustavia EUR 🇧🇱 fr St. Barthélemy 652 .gp
BM BMU 1-441 Hamilton BMD 🇧🇲 en-BM,pt Bermuda 060 .bm
BN BRN 673 Bandar Seri Begawan BND 🇧🇳 ms-BN,en-BN Brunei 096 .bn
BO BOL 591 Sucre BOB 🇧🇴 es-BO,qu,ay Bolivia 068 .bo
BR BRA 55 Brasilia BRL 🇧🇷 pt-BR,es,en,fr Brazil 076 .br
BS BHS 1-242 Nassau BSD 🇧🇸 en-BS Bahamas 044 .bs
BT BTN 975 Thimphu INR 🇧🇹 dz Bhutan 064 .bt
BW BWA 267 Gaborone BWP 🇧🇼 en-BW,tn-BW Botswana 072 .bw
BY BLR 375 Minsk BYN 🇧🇾 be,ru Belarus 112 .by
BZ BLZ 501 Belmopan BZD 🇧🇿 en-BZ,es Belize 084 .bz
CA CAN 1 Ottawa CAD 🇨🇦 en-CA,fr-CA,iu Canada 124 .ca
CC CCK 61 West Island AUD 🇨🇨 ms-CC,en Cocos Islands 166 .cc
CD COD 243 Kinshasa CDF 🇨🇩 fr-CD,ln,ktu,kg,sw,lua Congo - Kinshasa 180 .cd
CF CAF 236 Bangui XAF 🇨🇫 fr-CF,sg,ln,kg Central African Republic 140 .cf
CG COG 242 Brazzaville XAF 🇨🇬 fr-CG,kg,ln-CG Congo - Brazzaville 178 .cg
CH CHE 41 Bern CHF 🇨🇭 de-CH,fr-CH,it-CH,rm Switzerland 756 .ch
CI CIV 225 Yamoussoukro XOF 🇨🇮 fr-CI Côte d’Ivoire 384 .ci
CK COK 682 Avarua NZD 🇨🇰 en-CK,mi Cook Islands 184 .ck
CL CHL 56 Santiago CLP 🇨🇱 es-CL Chile 152 .cl
CM CMR 237 Yaounde XAF 🇨🇲 en-CM,fr-CM Cameroon 120 .cm
CN CHN 86 Beijing CNY 🇨🇳 zh-CN,yue,wuu,dta,ug,za China 156 .cn
CO COL 57 Bogota COP 🇨🇴 es-CO Colombia 170 .co
CR CRI 506 San Jose CRC 🇨🇷 es-CR,en Costa Rica 188 .cr
CU CUB 53 Havana CUP 🇨🇺 es-CU,pap Cuba 192 .cu
CV CPV 238 Praia CVE 🇨🇻 pt-CV Cape Verde 132 .cv
CW CUW 599 Willemstad XCG 🇨🇼 nl,pap Curaçao 531 .cw
CX CXR 61 Flying Fish Cove AUD 🇨🇽 en,zh,ms-CX Christmas Island 162 .cx
CY CYP 357 Nicosia EUR 🇨🇾 el-CY,tr-CY,en Cyprus 196 .cy
CZ CZE 420 Prague CZK 🇨🇿 cs,sk Czechia 203 .cz
DE DEU 49 Berlin EUR 🇩🇪 de Germany 276 .de
DJ DJI 253 Djibouti DJF 🇩🇯 fr-DJ,ar,so-DJ,aa Djibouti 262 .dj
DK DNK 45 Copenhagen DKK 🇩🇰 da-DK,en,fo,de-DK Denmark 208 .dk
DM DMA 1-767 Roseau XCD 🇩🇲 en-DM Dominica 212 .dm
DO DOM 1-809 Santo Domingo DOP 🇩🇴 es-DO Dominican Republic 214 .do
DZ DZA 213 Algiers DZD 🇩🇿 ar-DZ Algeria 012 .dz
EC ECU 593 Quito USD 🇪🇨 es-EC Ecuador 218 .ec
EE EST 372 Tallinn EUR 🇪🇪 et,ru Estonia 233 .ee
EG EGY 20 Cairo EGP 🇪🇬 ar-EG,en,fr Egypt 818 .eg
EH ESH 212 El-Aaiun MAD 🇪🇭 ar,mey Western Sahara 732 .eh
ER ERI 291 Asmara ERN 🇪🇷 aa-ER,ar,tig,kun,ti-ER Eritrea 232 .er
ES ESP 34 Madrid EUR 🇪🇸 es-ES,ca,gl,eu,oc Spain 724 .es
ET ETH 251 Addis Ababa ETB 🇪🇹 am,en-ET,om-ET,ti-ET,so-ET,sid Ethiopia 231 .et
FI FIN 358 Helsinki EUR 🇫🇮 fi-FI,sv-FI,smn Finland 246 .fi
FJ FJI 679 Suva FJD 🇫🇯 en-FJ,fj Fiji 242 .fj
FK FLK 500 Stanley FKP 🇫🇰 en-FK Falkland Islands 238 .fk
FM FSM 691 Palikir USD 🇫🇲 en-FM,chk,pon,yap,kos,uli,woe,nkr,kpg Micronesia 583 .fm
FO FRO 298 Torshavn DKK 🇫🇴 fo,da-FO Faroe Islands 234 .fo
FR FRA 33 Paris EUR 🇫🇷 fr-FR,frp,br,co,ca,eu,oc France 250 .fr
GA GAB 241 Libreville XAF 🇬🇦 fr-GA Gabon 266 .ga
GB GBR 44 London GBP 🇬🇧 en-GB,cy-GB,gd UK 826 .uk
GD GRD 1-473 St. George's XCD 🇬🇩 en-GD Grenada 308 .gd
GE GEO 995 Tbilisi GEL 🇬🇪 ka,ru,hy,az Georgia 268 .ge
GF GUF 594 Cayenne EUR 🇬🇫 fr-GF French Guiana 254 .gf
GG GGY 44 St Peter Port GBP 🇬🇬 en,nrf Guernsey 831 .gg
GH GHA 233 Accra GHS 🇬🇭 en-GH,ak,ee,tw Ghana 288 .gh
GI GIB 350 Gibraltar GIP 🇬🇮 en-GI,es,it,pt Gibraltar 292 .gi
GL GRL 299 Nuuk DKK 🇬🇱 kl,da-GL,en Greenland 304 .gl
GM GMB 220 Banjul GMD 🇬🇲 en-GM,mnk,wof,wo,ff Gambia 270 .gm
GN GIN 224 Conakry GNF 🇬🇳 fr-GN Guinea 324 .gn
GP GLP 590 Basse-Terre EUR 🇬🇵 fr-GP Guadeloupe 312 .gp
GQ GNQ 240 Ciudad de la Paz XAF 🇬🇶 es-GQ,fr,pt Equatorial Guinea 226 .gq
GR GRC 30 Athens EUR 🇬🇷 el-GR,en,fr Greece 300 .gr
GT GTM 502 Guatemala City GTQ 🇬🇹 es-GT Guatemala 320 .gt
GU GUM 1-671 Hagatna USD 🇬🇺 en-GU,ch-GU Guam 316 .gu
GW GNB 245 Bissau XOF 🇬🇼 pt-GW,pov Guinea-Bissau 624 .gw
GY GUY 592 Georgetown GYD 🇬🇾 en-GY Guyana 328 .gy
HK HKG 852 Hong Kong HKD 🇭🇰 zh-HK,yue,zh,en Hong Kong 344 .hk
HN HND 504 Tegucigalpa HNL 🇭🇳 es-HN,cab,miq Honduras 340 .hn
HR HRV 385 Zagreb EUR 🇭🇷 hr-HR,sr Croatia 191 .hr
HT HTI 509 Port-au-Prince HTG 🇭🇹 ht,fr-HT Haiti 332 .ht
HU HUN 36 Budapest HUF 🇭🇺 hu-HU Hungary 348 .hu
ID IDN 62 Jakarta IDR 🇮🇩 id,en,nl,jv Indonesia 360 .id
IE IRL 353 Dublin EUR 🇮🇪 en-IE,ga-IE Ireland 372 .ie
IL ISR 972 Jerusalem ILS 🇮🇱 he,ar-IL,en-IL Israel 376 .il
IM IMN 44 Douglas GBP 🇮🇲 en,gv Isle of Man 833 .im
IN IND 91 New Delhi INR 🇮🇳 en-IN,hi,bn,te,mr,ta,ur,gu,kn,ml,or,pa,as,bh,sat,ks,ne,sd,kok,doi,mni,sit,sa,fr,lus,inc India 356 .in
IO IOT 246 Diego Garcia USD 🇮🇴 en-IO British Indian Ocean Territory 086 .io
IQ IRQ 964 Baghdad IQD 🇮🇶 ar-IQ,ku,hy Iraq 368 .iq
IR IRN 98 Tehran IRR 🇮🇷 fa-IR,ku Iran 364 .ir
IS ISL 354 Reykjavik ISK 🇮🇸 is,en,de,da,sv,no Iceland 352 .is
IT ITA 39 Rome EUR 🇮🇹 it-IT,de-IT,fr-IT,sc,ca,co,sl Italy 380 .it
JE JEY 44 Saint Helier GBP 🇯🇪 en,fr,nrf Jersey 832 .je
JM JAM 1-876 Kingston JMD 🇯🇲 en-JM Jamaica 388 .jm
JO JOR 962 Amman JOD 🇯🇴 ar-JO,en Jordan 400 .jo
JP JPN 81 Tokyo JPY 🇯🇵 ja Japan 392 .jp
KE KEN 254 Nairobi KES 🇰🇪 en-KE,sw-KE Kenya 404 .ke
KG KGZ 996 Bishkek KGS 🇰🇬 ky,uz,ru Kyrgyzstan 417 .kg
KH KHM 855 Phnom Penh KHR 🇰🇭 km,fr,en Cambodia 116 .kh
KI KIR 686 Tarawa AUD 🇰🇮 en-KI,gil Kiribati 296 .ki
KM COM 269 Moroni KMF 🇰🇲 ar,fr-KM Comoros 174 .km
KN KNA 1-869 Basseterre XCD 🇰🇳 en-KN St. Kitts & Nevis 659 .kn
KP PRK 850 Pyongyang KPW 🇰🇵 ko-KP North Korea 408 .kp
KR KOR 82 Seoul KRW 🇰🇷 ko-KR,en South Korea 410 .kr
KW KWT 965 Kuwait City KWD 🇰🇼 ar-KW,en Kuwait 414 .kw
KY CYM 1-345 George Town KYD 🇰🇾 en-KY Cayman Islands 136 .ky
KZ KAZ 7 Nur-Sultan KZT 🇰🇿 kk,ru Kazakhstan 398 .kz
LA LAO 856 Vientiane LAK 🇱🇦 lo,fr,en Laos 418 .la
LB LBN 961 Beirut LBP 🇱🇧 ar-LB,fr-LB,en,hy Lebanon 422 .lb
LC LCA 1-758 Castries XCD 🇱🇨 en-LC St. Lucia 662 .lc
LI LIE 423 Vaduz CHF 🇱🇮 de-LI Liechtenstein 438 .li
LK LKA 94 Colombo LKR 🇱🇰 si,ta,en Sri Lanka 144 .lk
LR LBR 231 Monrovia LRD 🇱🇷 en-LR Liberia 430 .lr
LS LSO 266 Maseru LSL 🇱🇸 en-LS,st,zu,xh Lesotho 426 .ls
LT LTU 370 Vilnius EUR 🇱🇹 lt,ru,pl Lithuania 440 .lt
LU LUX 352 Luxembourg EUR 🇱🇺 lb,de-LU,fr-LU Luxembourg 442 .lu
LV LVA 371 Riga EUR 🇱🇻 lv,ru,lt Latvia 428 .lv
LY LBY 218 Tripoli LYD 🇱🇾 ar-LY,it,en Libya 434 .ly
MA MAR 212 Rabat MAD 🇲🇦 ar-MA,ber,fr Morocco 504 .ma
MC MCO 377 Monaco EUR 🇲🇨 fr-MC,en,it Monaco 492 .mc
MD MDA 373 Chisinau MDL 🇲🇩 ro,ru,gag,tr Moldova 498 .md
ME MNE 382 Podgorica EUR 🇲🇪 sr,hu,bs,sq,hr,rom Montenegro 499 .me
MF MAF 590 Marigot EUR 🇲🇫 fr St. Martin 663 .gp
MG MDG 261 Antananarivo MGA 🇲🇬 fr-MG,mg Madagascar 450 .mg
MH MHL 692 Majuro USD 🇲🇭 mh,en-MH Marshall Islands 584 .mh
MK MKD 389 Skopje MKD 🇲🇰 mk,sq,tr,rmm,sr North Macedonia 807 .mk
ML MLI 223 Bamako XOF 🇲🇱 fr-ML,bm Mali 466 .ml
MM MMR 95 Nay Pyi Taw MMK 🇲🇲 my Myanmar 104 .mm
MN MNG 976 Ulaanbaatar MNT 🇲🇳 mn,ru Mongolia 496 .mn
MO MAC 853 Macao MOP 🇲🇴 zh,zh-MO,pt Macao 446 .mo
MP MNP 1-670 Saipan USD 🇲🇵 fil,tl,zh,ch-MP,en-MP Northern Mariana Islands 580 .mp
MQ MTQ 596 Fort-de-France EUR 🇲🇶 fr-MQ Martinique 474 .mq
MR MRT 222 Nouakchott MRU 🇲🇷 ar-MR,fuc,snk,fr,mey,wo Mauritania 478 .mr
MS MSR 1-664 Plymouth XCD 🇲🇸 en-MS Montserrat 500 .ms
MT MLT 356 Valletta EUR 🇲🇹 mt,en-MT Malta 470 .mt
MU MUS 230 Port Louis MUR 🇲🇺 en-MU,bho,fr Mauritius 480 .mu
MV MDV 960 Male MVR 🇲🇻 dv,en Maldives 462 .mv
MW MWI 265 Lilongwe MWK 🇲🇼 ny,yao,tum,swk Malawi 454 .mw
MX MEX 52 Mexico City MXN 🇲🇽 es-MX Mexico 484 .mx
MY MYS 60 Kuala Lumpur MYR 🇲🇾 ms-MY,en,zh,ta,te,ml,pa,th Malaysia 458 .my
MZ MOZ 258 Maputo MZN 🇲🇿 pt-MZ,vmw Mozambique 508 .mz
NA NAM 264 Windhoek NAD 🇳🇦 en-NA,af,de,hz,naq Namibia 516 .na
NC NCL 687 Noumea XPF 🇳🇨 fr-NC New Caledonia 540 .nc
NE NER 227 Niamey XOF 🇳🇪 fr-NE,ha,kr,dje Niger 562 .ne
NF NFK 672 Kingston AUD 🇳🇫 en-NF Norfolk Island 574 .nf
NG NGA 234 Abuja NGN 🇳🇬 en-NG,ha,yo,ig,ff Nigeria 566 .ng
NI NIC 505 Managua NIO 🇳🇮 es-NI,en Nicaragua 558 .ni
NL NLD 31 Amsterdam EUR 🇳🇱 nl-NL,fy-NL Netherlands 528 .nl
NO NOR 47 Oslo NOK 🇳🇴 no,nb,nn,se,fi Norway 578 .no
NP NPL 977 Kathmandu NPR 🇳🇵 ne,en Nepal 524 .np
NR NRU 674 Yaren AUD 🇳🇷 na,en-NR Nauru 520 .nr
NU NIU 683 Alofi NZD 🇳🇺 niu,en-NU Niue 570 .nu
NZ NZL 64 Wellington NZD 🇳🇿 en-NZ,mi New Zealand 554 .nz
OM OMN 968 Muscat OMR 🇴🇲 ar-OM,en,bal,ur Oman 512 .om
PA PAN 507 Panama City PAB 🇵🇦 es-PA,en Panama 591 .pa
PE PER 51 Lima PEN 🇵🇪 es-PE,qu,ay Peru 604 .pe
PF PYF 689 Papeete XPF 🇵🇫 fr-PF,ty French Polynesia 258 .pf
PG PNG 675 Port Moresby PGK 🇵🇬 en-PG,ho,meu,tpi Papua New Guinea 598 .pg
PH PHL 63 Manila PHP 🇵🇭 tl,en-PH,fil,ceb,ilo,hil,war,pam,bik,bcl,pag,mrw,tsg,mdh,cbk,krj,sgd,msb,akl,ibg,yka,mta,abx Philippines 608 .ph
PK PAK 92 Islamabad PKR 🇵🇰 ur-PK,en-PK,pa,sd,ps,brh Pakistan 586 .pk
PL POL 48 Warsaw PLN 🇵🇱 pl Poland 616 .pl
PM SPM 508 Saint-Pierre EUR 🇵🇲 fr-PM St. Pierre & Miquelon 666 .pm
PN PCN 870 Adamstown NZD 🇵🇳 en-PN Pitcairn 612 .pn
PR PRI 1 San Juan USD 🇵🇷 en-PR,es-PR Puerto Rico 630 .pr
PT PRT 351 Lisbon EUR 🇵🇹 pt-PT,mwl Portugal 620 .pt
PW PLW 680 Melekeok USD 🇵🇼 pau,sov,en-PW,tox,ja,fil,zh Palau 585 .pw
PY PRY 595 Asuncion PYG 🇵🇾 es-PY,gn Paraguay 600 .py
QA QAT 974 Doha QAR 🇶🇦 ar-QA,es Qatar 634 .qa
RE REU 262 Saint-Denis EUR 🇷🇪 fr-RE Réunion 638 .re
RO ROU 40 Bucharest RON 🇷🇴 ro,hu,rom Romania 642 .ro
RS SRB 381 Belgrade RSD 🇷🇸 sr,hu,bs,rom Serbia 688 .rs
RU RUS 7 Moscow RUB 🇷🇺 ru,tt,xal,cau,ady,kv,ce,tyv,cv,udm,tut,mns,bua,myv,mdf,chm,ba,inh,kbd,krc,av,sah,nog Russia 643 .ru
RW RWA 250 Kigali RWF 🇷🇼 rw,en-RW,fr-RW,sw Rwanda 646 .rw
SA SAU 966 Riyadh SAR 🇸🇦 ar-SA Saudi Arabia 682 .sa
SB SLB 677 Honiara SBD 🇸🇧 en-SB,tpi Solomon Islands 090 .sb
SC SYC 248 Victoria SCR 🇸🇨 en-SC,fr-SC Seychelles 690 .sc
SD SDN 249 Khartoum SDG 🇸🇩 ar-SD,en,fia Sudan 729 .sd
SE SWE 46 Stockholm SEK 🇸🇪 sv-SE,se,sma,fi-SE Sweden 752 .se
SG SGP 65 Singapore SGD 🇸🇬 cmn,en-SG,ms-SG,ta-SG,zh-SG Singapore 702 .sg
SH SHN 290 Jamestown SHP 🇸🇭 en-SH St. Helena 654 .sh
SI SVN 386 Ljubljana EUR 🇸🇮 sl,sh Slovenia 705 .si
SJ SJM 47 Longyearbyen NOK 🇸🇯 no,ru Svalbard & Jan Mayen 744 .sj
SK SVK 421 Bratislava EUR 🇸🇰 sk,hu Slovakia 703 .sk
SL SLE 232 Freetown SLE 🇸🇱 en-SL,men,tem Sierra Leone 694 .sl
SM SMR 378 San Marino EUR 🇸🇲 it-SM San Marino 674 .sm
SN SEN 221 Dakar XOF 🇸🇳 fr-SN,wo,fuc,mnk Senegal 686 .sn
SO SOM 252 Mogadishu SOS 🇸🇴 so-SO,ar-SO,it,en-SO Somalia 706 .so
SR SUR 597 Paramaribo SRD 🇸🇷 nl-SR,en,srn,hns,jv Suriname 740 .sr
SS SSD 211 Juba SSP 🇸🇸 en South Sudan 728 .ss
ST STP 239 Sao Tome STN 🇸🇹 pt-ST São Tomé & Príncipe 678 .st
SV SLV 503 San Salvador SVC 🇸🇻 es-SV El Salvador 222 .sv
SX SXM 1-721 Philipsburg XCG 🇸🇽 nl,en Sint Maarten 534 .sx
SY SYR 963 Damascus SYP 🇸🇾 ar-SY,ku,hy,arc,fr,en Syria 760 .sy
SZ SWZ 268 Mbabane SZL 🇸🇿 en-SZ,ss-SZ Eswatini 748 .sz
TC TCA 1-649 Cockburn Town USD 🇹🇨 en-TC Turks & Caicos Islands 796 .tc
TD TCD 235 N'Djamena XAF 🇹🇩 fr-TD,ar-TD,sre Chad 148 .td
TF ATF 262 Port-aux-Francais EUR 🇹🇫 fr French Southern Territories 260 .tf
TG TGO 228 Lome XOF 🇹🇬 fr-TG,ee,hna,kbp,dag,ha Togo 768 .tg
TH THA 66 Bangkok THB 🇹🇭 th,en Thailand 764 .th
TJ TJK 992 Dushanbe TJS 🇹🇯 tg,ru Tajikistan 762 .tj
TL TLS 670 Dili USD 🇹🇱 tet,pt-TL,id,en Timor-Leste 626 .tl
TM TKM 993 Ashgabat TMT 🇹🇲 tk,ru,uz Turkmenistan 795 .tm
TN TUN 216 Tunis TND 🇹🇳 ar-TN,fr Tunisia 788 .tn
TO TON 676 Nuku'alofa TOP 🇹🇴 to,en-TO Tonga 776 .to
TR TUR 90 Ankara TRY 🇹🇷 tr-TR,ku,diq,az,av Türkiye 792 .tr
TT TTO 1-868 Port of Spain TTD 🇹🇹 en-TT,hns,fr,es,zh Trinidad & Tobago 780 .tt
TV TUV 688 Funafuti AUD 🇹🇻 tvl,en,sm,gil Tuvalu 798 .tv
TW TWN 886 Taipei TWD 🇹🇼 zh-TW,zh,nan,hak Taiwan 158 .tw
TZ TZA 255 Dodoma TZS 🇹🇿 sw-TZ,en,ar Tanzania 834 .tz
UA UKR 380 Kyiv UAH 🇺🇦 uk,ru-UA,rom,pl,hu Ukraine 804 .ua
UG UGA 256 Kampala UGX 🇺🇬 en-UG,lg,sw,ar Uganda 800 .ug
US USA 1 Washington USD 🇺🇸 en-US,es-US,haw,fr US 840 .us
UY URY 598 Montevideo UYU 🇺🇾 es-UY Uruguay 858 .uy
UZ UZB 998 Tashkent UZS 🇺🇿 uz,ru,tg Uzbekistan 860 .uz
VA VAT 39 Vatican City EUR 🇻🇦 la,it,fr Vatican City 336 .va
VC VCT 1-784 Kingstown XCD 🇻🇨 en-VC,fr St. Vincent & Grenadines 670 .vc
VE VEN 58 Caracas VES 🇻🇪 es-VE Venezuela 862 .ve
VG VGB 1-284 Road Town USD 🇻🇬 en-VG British Virgin Islands 092 .vg
VI VIR 1-340 Charlotte Amalie USD 🇻🇮 en-VI U.S. Virgin Islands 850 .vi
VN VNM 84 Hanoi VND 🇻🇳 vi,en,fr,zh,km Vietnam 704 .vn
VU VUT 678 Port Vila VUV 🇻🇺 bi,en-VU,fr-VU Vanuatu 548 .vu
WF WLF 681 Mata Utu XPF 🇼🇫 wls,fud,fr-WF Wallis & Futuna 876 .wf
WS WSM 685 Apia WST 🇼🇸 sm,en-WS Samoa 882 .ws
YE YEM 967 Sanaa YER 🇾🇪 ar-YE Yemen 887 .ye
YT MYT 262 Mamoudzou EUR 🇾🇹 fr-YT Mayotte 175 .yt
ZA ZAF 27 Pretoria ZAR 🇿🇦 zu,xh,af,nso,en-ZA,tn,st,ts,ss,ve,nr South Africa 710 .za
ZM ZMB 260 Lusaka ZMW 🇿🇲 en-ZM,bem,loz,lun,lue,ny,toi Zambia 894 .zm
ZW ZWE 263 Harare ZWG 🇿🇼 en-ZW,sn,nr,nd Zimbabwe 716 .zw
1 alpha2 alpha3 calling-code capital currency flag languages name numeric tld
2 AD AND 376 Andorra la Vella EUR 🇦🇩 ca Andorra 020 .ad
3 AE ARE 971 Abu Dhabi AED 🇦🇪 ar-AE,fa,en,hi,ur United Arab Emirates 784 .ae
4 AF AFG 93 Kabul AFN 🇦🇫 fa-AF,ps,uz-AF,tk Afghanistan 004 .af
5 AG ATG 1-268 St. John's XCD 🇦🇬 en-AG Antigua & Barbuda 028 .ag
6 AI AIA 1-264 The Valley XCD 🇦🇮 en-AI Anguilla 660 .ai
7 AL ALB 355 Tirana ALL 🇦🇱 sq,el Albania 008 .al
8 AM ARM 374 Yerevan AMD 🇦🇲 hy Armenia 051 .am
9 AO AGO 244 Luanda AOA 🇦🇴 pt-AO Angola 024 .ao
10 AR ARG 54 Buenos Aires ARS 🇦🇷 es-AR,en,it,de,fr,gn Argentina 032 .ar
11 AS ASM 1-684 Pago Pago USD 🇦🇸 en-AS,sm,to American Samoa 016 .as
12 AT AUT 43 Vienna EUR 🇦🇹 de-AT,hr,hu,sl Austria 040 .at
13 AU AUS 61 Canberra AUD 🇦🇺 en-AU Australia 036 .au
14 AW ABW 297 Oranjestad AWG 🇦🇼 nl-AW,pap,es,en Aruba 533 .aw
15 AX ALA 358 Mariehamn EUR 🇦🇽 sv-AX Åland Islands 248 .ax
16 AZ AZE 994 Baku AZN 🇦🇿 az,ru,hy Azerbaijan 031 .az
17 BA BIH 387 Sarajevo BAM 🇧🇦 bs,hr-BA,sr-BA Bosnia 070 .ba
18 BB BRB 1-246 Bridgetown BBD 🇧🇧 en-BB Barbados 052 .bb
19 BD BGD 880 Dhaka BDT 🇧🇩 bn-BD,en Bangladesh 050 .bd
20 BE BEL 32 Brussels EUR 🇧🇪 nl-BE,fr-BE,de-BE Belgium 056 .be
21 BF BFA 226 Ouagadougou XOF 🇧🇫 fr-BF,mos Burkina Faso 854 .bf
22 BG BGR 359 Sofia EUR 🇧🇬 bg,tr-BG,rom Bulgaria 100 .bg
23 BH BHR 973 Manama BHD 🇧🇭 ar-BH,en,fa,ur Bahrain 048 .bh
24 BI BDI 257 Gitega BIF 🇧🇮 fr-BI,rn Burundi 108 .bi
25 BJ BEN 229 Porto-Novo XOF 🇧🇯 fr-BJ Benin 204 .bj
26 BL BLM 590 Gustavia EUR 🇧🇱 fr St. Barthélemy 652 .gp
27 BM BMU 1-441 Hamilton BMD 🇧🇲 en-BM,pt Bermuda 060 .bm
28 BN BRN 673 Bandar Seri Begawan BND 🇧🇳 ms-BN,en-BN Brunei 096 .bn
29 BO BOL 591 Sucre BOB 🇧🇴 es-BO,qu,ay Bolivia 068 .bo
30 BR BRA 55 Brasilia BRL 🇧🇷 pt-BR,es,en,fr Brazil 076 .br
31 BS BHS 1-242 Nassau BSD 🇧🇸 en-BS Bahamas 044 .bs
32 BT BTN 975 Thimphu INR 🇧🇹 dz Bhutan 064 .bt
33 BW BWA 267 Gaborone BWP 🇧🇼 en-BW,tn-BW Botswana 072 .bw
34 BY BLR 375 Minsk BYN 🇧🇾 be,ru Belarus 112 .by
35 BZ BLZ 501 Belmopan BZD 🇧🇿 en-BZ,es Belize 084 .bz
36 CA CAN 1 Ottawa CAD 🇨🇦 en-CA,fr-CA,iu Canada 124 .ca
37 CC CCK 61 West Island AUD 🇨🇨 ms-CC,en Cocos Islands 166 .cc
38 CD COD 243 Kinshasa CDF 🇨🇩 fr-CD,ln,ktu,kg,sw,lua Congo - Kinshasa 180 .cd
39 CF CAF 236 Bangui XAF 🇨🇫 fr-CF,sg,ln,kg Central African Republic 140 .cf
40 CG COG 242 Brazzaville XAF 🇨🇬 fr-CG,kg,ln-CG Congo - Brazzaville 178 .cg
41 CH CHE 41 Bern CHF 🇨🇭 de-CH,fr-CH,it-CH,rm Switzerland 756 .ch
42 CI CIV 225 Yamoussoukro XOF 🇨🇮 fr-CI Côte d’Ivoire 384 .ci
43 CK COK 682 Avarua NZD 🇨🇰 en-CK,mi Cook Islands 184 .ck
44 CL CHL 56 Santiago CLP 🇨🇱 es-CL Chile 152 .cl
45 CM CMR 237 Yaounde XAF 🇨🇲 en-CM,fr-CM Cameroon 120 .cm
46 CN CHN 86 Beijing CNY 🇨🇳 zh-CN,yue,wuu,dta,ug,za China 156 .cn
47 CO COL 57 Bogota COP 🇨🇴 es-CO Colombia 170 .co
48 CR CRI 506 San Jose CRC 🇨🇷 es-CR,en Costa Rica 188 .cr
49 CU CUB 53 Havana CUP 🇨🇺 es-CU,pap Cuba 192 .cu
50 CV CPV 238 Praia CVE 🇨🇻 pt-CV Cape Verde 132 .cv
51 CW CUW 599 Willemstad XCG 🇨🇼 nl,pap Curaçao 531 .cw
52 CX CXR 61 Flying Fish Cove AUD 🇨🇽 en,zh,ms-CX Christmas Island 162 .cx
53 CY CYP 357 Nicosia EUR 🇨🇾 el-CY,tr-CY,en Cyprus 196 .cy
54 CZ CZE 420 Prague CZK 🇨🇿 cs,sk Czechia 203 .cz
55 DE DEU 49 Berlin EUR 🇩🇪 de Germany 276 .de
56 DJ DJI 253 Djibouti DJF 🇩🇯 fr-DJ,ar,so-DJ,aa Djibouti 262 .dj
57 DK DNK 45 Copenhagen DKK 🇩🇰 da-DK,en,fo,de-DK Denmark 208 .dk
58 DM DMA 1-767 Roseau XCD 🇩🇲 en-DM Dominica 212 .dm
59 DO DOM 1-809 Santo Domingo DOP 🇩🇴 es-DO Dominican Republic 214 .do
60 DZ DZA 213 Algiers DZD 🇩🇿 ar-DZ Algeria 012 .dz
61 EC ECU 593 Quito USD 🇪🇨 es-EC Ecuador 218 .ec
62 EE EST 372 Tallinn EUR 🇪🇪 et,ru Estonia 233 .ee
63 EG EGY 20 Cairo EGP 🇪🇬 ar-EG,en,fr Egypt 818 .eg
64 EH ESH 212 El-Aaiun MAD 🇪🇭 ar,mey Western Sahara 732 .eh
65 ER ERI 291 Asmara ERN 🇪🇷 aa-ER,ar,tig,kun,ti-ER Eritrea 232 .er
66 ES ESP 34 Madrid EUR 🇪🇸 es-ES,ca,gl,eu,oc Spain 724 .es
67 ET ETH 251 Addis Ababa ETB 🇪🇹 am,en-ET,om-ET,ti-ET,so-ET,sid Ethiopia 231 .et
68 FI FIN 358 Helsinki EUR 🇫🇮 fi-FI,sv-FI,smn Finland 246 .fi
69 FJ FJI 679 Suva FJD 🇫🇯 en-FJ,fj Fiji 242 .fj
70 FK FLK 500 Stanley FKP 🇫🇰 en-FK Falkland Islands 238 .fk
71 FM FSM 691 Palikir USD 🇫🇲 en-FM,chk,pon,yap,kos,uli,woe,nkr,kpg Micronesia 583 .fm
72 FO FRO 298 Torshavn DKK 🇫🇴 fo,da-FO Faroe Islands 234 .fo
73 FR FRA 33 Paris EUR 🇫🇷 fr-FR,frp,br,co,ca,eu,oc France 250 .fr
74 GA GAB 241 Libreville XAF 🇬🇦 fr-GA Gabon 266 .ga
75 GB GBR 44 London GBP 🇬🇧 en-GB,cy-GB,gd UK 826 .uk
76 GD GRD 1-473 St. George's XCD 🇬🇩 en-GD Grenada 308 .gd
77 GE GEO 995 Tbilisi GEL 🇬🇪 ka,ru,hy,az Georgia 268 .ge
78 GF GUF 594 Cayenne EUR 🇬🇫 fr-GF French Guiana 254 .gf
79 GG GGY 44 St Peter Port GBP 🇬🇬 en,nrf Guernsey 831 .gg
80 GH GHA 233 Accra GHS 🇬🇭 en-GH,ak,ee,tw Ghana 288 .gh
81 GI GIB 350 Gibraltar GIP 🇬🇮 en-GI,es,it,pt Gibraltar 292 .gi
82 GL GRL 299 Nuuk DKK 🇬🇱 kl,da-GL,en Greenland 304 .gl
83 GM GMB 220 Banjul GMD 🇬🇲 en-GM,mnk,wof,wo,ff Gambia 270 .gm
84 GN GIN 224 Conakry GNF 🇬🇳 fr-GN Guinea 324 .gn
85 GP GLP 590 Basse-Terre EUR 🇬🇵 fr-GP Guadeloupe 312 .gp
86 GQ GNQ 240 Ciudad de la Paz XAF 🇬🇶 es-GQ,fr,pt Equatorial Guinea 226 .gq
87 GR GRC 30 Athens EUR 🇬🇷 el-GR,en,fr Greece 300 .gr
88 GT GTM 502 Guatemala City GTQ 🇬🇹 es-GT Guatemala 320 .gt
89 GU GUM 1-671 Hagatna USD 🇬🇺 en-GU,ch-GU Guam 316 .gu
90 GW GNB 245 Bissau XOF 🇬🇼 pt-GW,pov Guinea-Bissau 624 .gw
91 GY GUY 592 Georgetown GYD 🇬🇾 en-GY Guyana 328 .gy
92 HK HKG 852 Hong Kong HKD 🇭🇰 zh-HK,yue,zh,en Hong Kong 344 .hk
93 HN HND 504 Tegucigalpa HNL 🇭🇳 es-HN,cab,miq Honduras 340 .hn
94 HR HRV 385 Zagreb EUR 🇭🇷 hr-HR,sr Croatia 191 .hr
95 HT HTI 509 Port-au-Prince HTG 🇭🇹 ht,fr-HT Haiti 332 .ht
96 HU HUN 36 Budapest HUF 🇭🇺 hu-HU Hungary 348 .hu
97 ID IDN 62 Jakarta IDR 🇮🇩 id,en,nl,jv Indonesia 360 .id
98 IE IRL 353 Dublin EUR 🇮🇪 en-IE,ga-IE Ireland 372 .ie
99 IL ISR 972 Jerusalem ILS 🇮🇱 he,ar-IL,en-IL Israel 376 .il
100 IM IMN 44 Douglas GBP 🇮🇲 en,gv Isle of Man 833 .im
101 IN IND 91 New Delhi INR 🇮🇳 en-IN,hi,bn,te,mr,ta,ur,gu,kn,ml,or,pa,as,bh,sat,ks,ne,sd,kok,doi,mni,sit,sa,fr,lus,inc India 356 .in
102 IO IOT 246 Diego Garcia USD 🇮🇴 en-IO British Indian Ocean Territory 086 .io
103 IQ IRQ 964 Baghdad IQD 🇮🇶 ar-IQ,ku,hy Iraq 368 .iq
104 IR IRN 98 Tehran IRR 🇮🇷 fa-IR,ku Iran 364 .ir
105 IS ISL 354 Reykjavik ISK 🇮🇸 is,en,de,da,sv,no Iceland 352 .is
106 IT ITA 39 Rome EUR 🇮🇹 it-IT,de-IT,fr-IT,sc,ca,co,sl Italy 380 .it
107 JE JEY 44 Saint Helier GBP 🇯🇪 en,fr,nrf Jersey 832 .je
108 JM JAM 1-876 Kingston JMD 🇯🇲 en-JM Jamaica 388 .jm
109 JO JOR 962 Amman JOD 🇯🇴 ar-JO,en Jordan 400 .jo
110 JP JPN 81 Tokyo JPY 🇯🇵 ja Japan 392 .jp
111 KE KEN 254 Nairobi KES 🇰🇪 en-KE,sw-KE Kenya 404 .ke
112 KG KGZ 996 Bishkek KGS 🇰🇬 ky,uz,ru Kyrgyzstan 417 .kg
113 KH KHM 855 Phnom Penh KHR 🇰🇭 km,fr,en Cambodia 116 .kh
114 KI KIR 686 Tarawa AUD 🇰🇮 en-KI,gil Kiribati 296 .ki
115 KM COM 269 Moroni KMF 🇰🇲 ar,fr-KM Comoros 174 .km
116 KN KNA 1-869 Basseterre XCD 🇰🇳 en-KN St. Kitts & Nevis 659 .kn
117 KP PRK 850 Pyongyang KPW 🇰🇵 ko-KP North Korea 408 .kp
118 KR KOR 82 Seoul KRW 🇰🇷 ko-KR,en South Korea 410 .kr
119 KW KWT 965 Kuwait City KWD 🇰🇼 ar-KW,en Kuwait 414 .kw
120 KY CYM 1-345 George Town KYD 🇰🇾 en-KY Cayman Islands 136 .ky
121 KZ KAZ 7 Nur-Sultan KZT 🇰🇿 kk,ru Kazakhstan 398 .kz
122 LA LAO 856 Vientiane LAK 🇱🇦 lo,fr,en Laos 418 .la
123 LB LBN 961 Beirut LBP 🇱🇧 ar-LB,fr-LB,en,hy Lebanon 422 .lb
124 LC LCA 1-758 Castries XCD 🇱🇨 en-LC St. Lucia 662 .lc
125 LI LIE 423 Vaduz CHF 🇱🇮 de-LI Liechtenstein 438 .li
126 LK LKA 94 Colombo LKR 🇱🇰 si,ta,en Sri Lanka 144 .lk
127 LR LBR 231 Monrovia LRD 🇱🇷 en-LR Liberia 430 .lr
128 LS LSO 266 Maseru LSL 🇱🇸 en-LS,st,zu,xh Lesotho 426 .ls
129 LT LTU 370 Vilnius EUR 🇱🇹 lt,ru,pl Lithuania 440 .lt
130 LU LUX 352 Luxembourg EUR 🇱🇺 lb,de-LU,fr-LU Luxembourg 442 .lu
131 LV LVA 371 Riga EUR 🇱🇻 lv,ru,lt Latvia 428 .lv
132 LY LBY 218 Tripoli LYD 🇱🇾 ar-LY,it,en Libya 434 .ly
133 MA MAR 212 Rabat MAD 🇲🇦 ar-MA,ber,fr Morocco 504 .ma
134 MC MCO 377 Monaco EUR 🇲🇨 fr-MC,en,it Monaco 492 .mc
135 MD MDA 373 Chisinau MDL 🇲🇩 ro,ru,gag,tr Moldova 498 .md
136 ME MNE 382 Podgorica EUR 🇲🇪 sr,hu,bs,sq,hr,rom Montenegro 499 .me
137 MF MAF 590 Marigot EUR 🇲🇫 fr St. Martin 663 .gp
138 MG MDG 261 Antananarivo MGA 🇲🇬 fr-MG,mg Madagascar 450 .mg
139 MH MHL 692 Majuro USD 🇲🇭 mh,en-MH Marshall Islands 584 .mh
140 MK MKD 389 Skopje MKD 🇲🇰 mk,sq,tr,rmm,sr North Macedonia 807 .mk
141 ML MLI 223 Bamako XOF 🇲🇱 fr-ML,bm Mali 466 .ml
142 MM MMR 95 Nay Pyi Taw MMK 🇲🇲 my Myanmar 104 .mm
143 MN MNG 976 Ulaanbaatar MNT 🇲🇳 mn,ru Mongolia 496 .mn
144 MO MAC 853 Macao MOP 🇲🇴 zh,zh-MO,pt Macao 446 .mo
145 MP MNP 1-670 Saipan USD 🇲🇵 fil,tl,zh,ch-MP,en-MP Northern Mariana Islands 580 .mp
146 MQ MTQ 596 Fort-de-France EUR 🇲🇶 fr-MQ Martinique 474 .mq
147 MR MRT 222 Nouakchott MRU 🇲🇷 ar-MR,fuc,snk,fr,mey,wo Mauritania 478 .mr
148 MS MSR 1-664 Plymouth XCD 🇲🇸 en-MS Montserrat 500 .ms
149 MT MLT 356 Valletta EUR 🇲🇹 mt,en-MT Malta 470 .mt
150 MU MUS 230 Port Louis MUR 🇲🇺 en-MU,bho,fr Mauritius 480 .mu
151 MV MDV 960 Male MVR 🇲🇻 dv,en Maldives 462 .mv
152 MW MWI 265 Lilongwe MWK 🇲🇼 ny,yao,tum,swk Malawi 454 .mw
153 MX MEX 52 Mexico City MXN 🇲🇽 es-MX Mexico 484 .mx
154 MY MYS 60 Kuala Lumpur MYR 🇲🇾 ms-MY,en,zh,ta,te,ml,pa,th Malaysia 458 .my
155 MZ MOZ 258 Maputo MZN 🇲🇿 pt-MZ,vmw Mozambique 508 .mz
156 NA NAM 264 Windhoek NAD 🇳🇦 en-NA,af,de,hz,naq Namibia 516 .na
157 NC NCL 687 Noumea XPF 🇳🇨 fr-NC New Caledonia 540 .nc
158 NE NER 227 Niamey XOF 🇳🇪 fr-NE,ha,kr,dje Niger 562 .ne
159 NF NFK 672 Kingston AUD 🇳🇫 en-NF Norfolk Island 574 .nf
160 NG NGA 234 Abuja NGN 🇳🇬 en-NG,ha,yo,ig,ff Nigeria 566 .ng
161 NI NIC 505 Managua NIO 🇳🇮 es-NI,en Nicaragua 558 .ni
162 NL NLD 31 Amsterdam EUR 🇳🇱 nl-NL,fy-NL Netherlands 528 .nl
163 NO NOR 47 Oslo NOK 🇳🇴 no,nb,nn,se,fi Norway 578 .no
164 NP NPL 977 Kathmandu NPR 🇳🇵 ne,en Nepal 524 .np
165 NR NRU 674 Yaren AUD 🇳🇷 na,en-NR Nauru 520 .nr
166 NU NIU 683 Alofi NZD 🇳🇺 niu,en-NU Niue 570 .nu
167 NZ NZL 64 Wellington NZD 🇳🇿 en-NZ,mi New Zealand 554 .nz
168 OM OMN 968 Muscat OMR 🇴🇲 ar-OM,en,bal,ur Oman 512 .om
169 PA PAN 507 Panama City PAB 🇵🇦 es-PA,en Panama 591 .pa
170 PE PER 51 Lima PEN 🇵🇪 es-PE,qu,ay Peru 604 .pe
171 PF PYF 689 Papeete XPF 🇵🇫 fr-PF,ty French Polynesia 258 .pf
172 PG PNG 675 Port Moresby PGK 🇵🇬 en-PG,ho,meu,tpi Papua New Guinea 598 .pg
173 PH PHL 63 Manila PHP 🇵🇭 tl,en-PH,fil,ceb,ilo,hil,war,pam,bik,bcl,pag,mrw,tsg,mdh,cbk,krj,sgd,msb,akl,ibg,yka,mta,abx Philippines 608 .ph
174 PK PAK 92 Islamabad PKR 🇵🇰 ur-PK,en-PK,pa,sd,ps,brh Pakistan 586 .pk
175 PL POL 48 Warsaw PLN 🇵🇱 pl Poland 616 .pl
176 PM SPM 508 Saint-Pierre EUR 🇵🇲 fr-PM St. Pierre & Miquelon 666 .pm
177 PN PCN 870 Adamstown NZD 🇵🇳 en-PN Pitcairn 612 .pn
178 PR PRI 1 San Juan USD 🇵🇷 en-PR,es-PR Puerto Rico 630 .pr
179 PT PRT 351 Lisbon EUR 🇵🇹 pt-PT,mwl Portugal 620 .pt
180 PW PLW 680 Melekeok USD 🇵🇼 pau,sov,en-PW,tox,ja,fil,zh Palau 585 .pw
181 PY PRY 595 Asuncion PYG 🇵🇾 es-PY,gn Paraguay 600 .py
182 QA QAT 974 Doha QAR 🇶🇦 ar-QA,es Qatar 634 .qa
183 RE REU 262 Saint-Denis EUR 🇷🇪 fr-RE Réunion 638 .re
184 RO ROU 40 Bucharest RON 🇷🇴 ro,hu,rom Romania 642 .ro
185 RS SRB 381 Belgrade RSD 🇷🇸 sr,hu,bs,rom Serbia 688 .rs
186 RU RUS 7 Moscow RUB 🇷🇺 ru,tt,xal,cau,ady,kv,ce,tyv,cv,udm,tut,mns,bua,myv,mdf,chm,ba,inh,kbd,krc,av,sah,nog Russia 643 .ru
187 RW RWA 250 Kigali RWF 🇷🇼 rw,en-RW,fr-RW,sw Rwanda 646 .rw
188 SA SAU 966 Riyadh SAR 🇸🇦 ar-SA Saudi Arabia 682 .sa
189 SB SLB 677 Honiara SBD 🇸🇧 en-SB,tpi Solomon Islands 090 .sb
190 SC SYC 248 Victoria SCR 🇸🇨 en-SC,fr-SC Seychelles 690 .sc
191 SD SDN 249 Khartoum SDG 🇸🇩 ar-SD,en,fia Sudan 729 .sd
192 SE SWE 46 Stockholm SEK 🇸🇪 sv-SE,se,sma,fi-SE Sweden 752 .se
193 SG SGP 65 Singapore SGD 🇸🇬 cmn,en-SG,ms-SG,ta-SG,zh-SG Singapore 702 .sg
194 SH SHN 290 Jamestown SHP 🇸🇭 en-SH St. Helena 654 .sh
195 SI SVN 386 Ljubljana EUR 🇸🇮 sl,sh Slovenia 705 .si
196 SJ SJM 47 Longyearbyen NOK 🇸🇯 no,ru Svalbard & Jan Mayen 744 .sj
197 SK SVK 421 Bratislava EUR 🇸🇰 sk,hu Slovakia 703 .sk
198 SL SLE 232 Freetown SLE 🇸🇱 en-SL,men,tem Sierra Leone 694 .sl
199 SM SMR 378 San Marino EUR 🇸🇲 it-SM San Marino 674 .sm
200 SN SEN 221 Dakar XOF 🇸🇳 fr-SN,wo,fuc,mnk Senegal 686 .sn
201 SO SOM 252 Mogadishu SOS 🇸🇴 so-SO,ar-SO,it,en-SO Somalia 706 .so
202 SR SUR 597 Paramaribo SRD 🇸🇷 nl-SR,en,srn,hns,jv Suriname 740 .sr
203 SS SSD 211 Juba SSP 🇸🇸 en South Sudan 728 .ss
204 ST STP 239 Sao Tome STN 🇸🇹 pt-ST São Tomé & Príncipe 678 .st
205 SV SLV 503 San Salvador SVC 🇸🇻 es-SV El Salvador 222 .sv
206 SX SXM 1-721 Philipsburg XCG 🇸🇽 nl,en Sint Maarten 534 .sx
207 SY SYR 963 Damascus SYP 🇸🇾 ar-SY,ku,hy,arc,fr,en Syria 760 .sy
208 SZ SWZ 268 Mbabane SZL 🇸🇿 en-SZ,ss-SZ Eswatini 748 .sz
209 TC TCA 1-649 Cockburn Town USD 🇹🇨 en-TC Turks & Caicos Islands 796 .tc
210 TD TCD 235 N'Djamena XAF 🇹🇩 fr-TD,ar-TD,sre Chad 148 .td
211 TF ATF 262 Port-aux-Francais EUR 🇹🇫 fr French Southern Territories 260 .tf
212 TG TGO 228 Lome XOF 🇹🇬 fr-TG,ee,hna,kbp,dag,ha Togo 768 .tg
213 TH THA 66 Bangkok THB 🇹🇭 th,en Thailand 764 .th
214 TJ TJK 992 Dushanbe TJS 🇹🇯 tg,ru Tajikistan 762 .tj
215 TL TLS 670 Dili USD 🇹🇱 tet,pt-TL,id,en Timor-Leste 626 .tl
216 TM TKM 993 Ashgabat TMT 🇹🇲 tk,ru,uz Turkmenistan 795 .tm
217 TN TUN 216 Tunis TND 🇹🇳 ar-TN,fr Tunisia 788 .tn
218 TO TON 676 Nuku'alofa TOP 🇹🇴 to,en-TO Tonga 776 .to
219 TR TUR 90 Ankara TRY 🇹🇷 tr-TR,ku,diq,az,av Türkiye 792 .tr
220 TT TTO 1-868 Port of Spain TTD 🇹🇹 en-TT,hns,fr,es,zh Trinidad & Tobago 780 .tt
221 TV TUV 688 Funafuti AUD 🇹🇻 tvl,en,sm,gil Tuvalu 798 .tv
222 TW TWN 886 Taipei TWD 🇹🇼 zh-TW,zh,nan,hak Taiwan 158 .tw
223 TZ TZA 255 Dodoma TZS 🇹🇿 sw-TZ,en,ar Tanzania 834 .tz
224 UA UKR 380 Kyiv UAH 🇺🇦 uk,ru-UA,rom,pl,hu Ukraine 804 .ua
225 UG UGA 256 Kampala UGX 🇺🇬 en-UG,lg,sw,ar Uganda 800 .ug
226 US USA 1 Washington USD 🇺🇸 en-US,es-US,haw,fr US 840 .us
227 UY URY 598 Montevideo UYU 🇺🇾 es-UY Uruguay 858 .uy
228 UZ UZB 998 Tashkent UZS 🇺🇿 uz,ru,tg Uzbekistan 860 .uz
229 VA VAT 39 Vatican City EUR 🇻🇦 la,it,fr Vatican City 336 .va
230 VC VCT 1-784 Kingstown XCD 🇻🇨 en-VC,fr St. Vincent & Grenadines 670 .vc
231 VE VEN 58 Caracas VES 🇻🇪 es-VE Venezuela 862 .ve
232 VG VGB 1-284 Road Town USD 🇻🇬 en-VG British Virgin Islands 092 .vg
233 VI VIR 1-340 Charlotte Amalie USD 🇻🇮 en-VI U.S. Virgin Islands 850 .vi
234 VN VNM 84 Hanoi VND 🇻🇳 vi,en,fr,zh,km Vietnam 704 .vn
235 VU VUT 678 Port Vila VUV 🇻🇺 bi,en-VU,fr-VU Vanuatu 548 .vu
236 WF WLF 681 Mata Utu XPF 🇼🇫 wls,fud,fr-WF Wallis & Futuna 876 .wf
237 WS WSM 685 Apia WST 🇼🇸 sm,en-WS Samoa 882 .ws
238 YE YEM 967 Sanaa YER 🇾🇪 ar-YE Yemen 887 .ye
239 YT MYT 262 Mamoudzou EUR 🇾🇹 fr-YT Mayotte 175 .yt
240 ZA ZAF 27 Pretoria ZAR 🇿🇦 zu,xh,af,nso,en-ZA,tn,st,ts,ss,ve,nr South Africa 710 .za
241 ZM ZMB 260 Lusaka ZMW 🇿🇲 en-ZM,bem,loz,lun,lue,ny,toi Zambia 894 .zm
242 ZW ZWE 263 Harare ZWG 🇿🇼 en-ZW,sn,nr,nd Zimbabwe 716 .zw
+1 -18
View File
@@ -1,18 +1 @@
[ { "format": "{code}", "rows": "currency.tsv", "key": "code", "name": "name" }
{ "format": "{code}", "code": "AUD", "name": "Australian Dollar", "symbol": "$" },
{ "format": "{code}", "code": "BRL", "name": "Brazilian Real", "symbol": "R$" },
{ "format": "{code}", "code": "CAD", "name": "Canadian Dollar", "symbol": "$" },
{ "format": "{code}", "code": "CHF", "name": "Swiss Franc", "symbol": "CHF" },
{ "format": "{code}", "code": "CNY", "name": "Chinese Yuan", "symbol": "¥" },
{ "format": "{code}", "code": "DKK", "name": "Danish Krone", "symbol": "kr" },
{ "format": "{code}", "code": "EUR", "name": "Euro", "symbol": "€" },
{ "format": "{code}", "code": "GBP", "name": "Pound Sterling", "symbol": "£" },
{ "format": "{code}", "code": "INR", "name": "Indian Rupee", "symbol": "₹" },
{ "format": "{code}", "code": "JPY", "name": "Japanese Yen", "symbol": "¥" },
{ "format": "{code}", "code": "MXN", "name": "Mexican Peso", "symbol": "$" },
{ "format": "{code}", "code": "NOK", "name": "Norwegian Krone", "symbol": "kr" },
{ "format": "{code}", "code": "PLN", "name": "Polish Zloty", "symbol": "zł" },
{ "format": "{code}", "code": "SEK", "name": "Swedish Krona", "symbol": "kr" },
{ "format": "{code}", "code": "USD", "name": "US Dollar", "symbol": "$" },
{ "format": "{code}", "code": "ZAR", "name": "South African Rand", "symbol": "R" }
]
+166
View File
@@ -0,0 +1,166 @@
code decimals name numeric symbol
AED 2 UAE Dirham 784 AED
AFN 2 Afghani 971 ؋
ALL 2 Lek 008 ALL
AMD 2 Armenian Dram 051 ֏
AOA 2 Kwanza 973 Kz
ARS 2 Argentine Peso 032 $
AUD 2 Australian Dollar 036 $
AWG 2 Aruban Florin 533 AWG
AZN 2 Azerbaijan Manat 944 ₼
BAM 2 Convertible Mark 977 KM
BBD 2 Barbados Dollar 052 $
BDT 2 Taka 050 ৳
BHD 3 Bahraini Dinar 048 BHD
BIF 0 Burundi Franc 108 BIF
BMD 2 Bermudian Dollar 060 $
BND 2 Brunei Dollar 096 $
BOB 2 Boliviano 068 Bs
BOV 2 Mvdol 984 BOV
BRL 2 Brazilian Real 986 R$
BSD 2 Bahamian Dollar 044 $
BTN 2 Ngultrum 064 BTN
BWP 2 Pula 072 P
BYN 2 Belarusian Ruble 933 BYN
BZD 2 Belize Dollar 084 $
CAD 2 Canadian Dollar 124 $
CDF 2 Congolese Franc 976 CDF
CHE 2 WIR Euro 947 CHE
CHF 2 Swiss Franc 756 Fr.
CHW 2 WIR Franc 948 CHW
CLF 4 Unidad de Fomento 990 CLF
CLP 0 Chilean Peso 152 $
CNY 2 Yuan Renminbi 156 ¥
COP 2 Colombian Peso 170 $
COU 2 Unidad de Valor Real 970 COU
CRC 2 Costa Rican Colon 188 ₡
CUP 2 Cuban Peso 192 $
CVE 2 Cabo Verde Escudo 132 CVE
CZK 2 Czech Koruna 203 Kč
DJF 0 Djibouti Franc 262 DJF
DKK 2 Danish Krone 208 kr
DOP 2 Dominican Peso 214 $
DZD 2 Algerian Dinar 012 DZD
EGP 2 Egyptian Pound 818 E£
ERN 2 Nakfa 232 ERN
ETB 2 Ethiopian Birr 230 ETB
EUR 2 Euro 978 €
FJD 2 Fiji Dollar 242 $
FKP 2 Falkland Islands Pound 238 £
GBP 2 Pound Sterling 826 £
GEL 2 Lari 981 ₾
GHS 2 Ghana Cedi 936 GH₵
GIP 2 Gibraltar Pound 292 £
GMD 2 Dalasi 270 GMD
GNF 0 Guinean Franc 324 FG
GTQ 2 Quetzal 320 Q
GYD 2 Guyana Dollar 328 $
HKD 2 Hong Kong Dollar 344 $
HNL 2 Lempira 340 L
HTG 2 Gourde 332 HTG
HUF 2 Forint 348 Ft
IDR 2 Rupiah 360 Rp
ILS 2 New Israeli Sheqel 376 ₪
INR 2 Indian Rupee 356 ₹
IQD 3 Iraqi Dinar 368 IQD
IRR 2 Iranian Rial 364 IRR
ISK 0 Iceland Krona 352 kr
JMD 2 Jamaican Dollar 388 $
JOD 3 Jordanian Dinar 400 JOD
JPY 0 Yen 392 ¥
KES 2 Kenyan Shilling 404 KES
KGS 2 Som 417 ⃀
KHR 2 Riel 116 ៛
KMF 0 Comorian Franc 174 CF
KPW 2 North Korean Won 408 ₩
KRW 0 Won 410 ₩
KWD 3 Kuwaiti Dinar 414 KWD
KYD 2 Cayman Islands Dollar 136 $
KZT 2 Tenge 398 ₸
LAK 2 Lao Kip 418 ₭
LBP 2 Lebanese Pound 422 L£
LKR 2 Sri Lanka Rupee 144 Rs
LRD 2 Liberian Dollar 430 $
LSL 2 Loti 426 LSL
LYD 3 Libyan Dinar 434 LYD
MAD 2 Moroccan Dirham 504 MAD
MDL 2 Moldovan Leu 498 MDL
MGA 2 Malagasy Ariary 969 Ar
MKD 2 Denar 807 MKD
MMK 2 Kyat 104 K
MNT 2 Tugrik 496 ₮
MOP 2 Pataca 446 MOP
MRU 2 Ouguiya 929 MRU
MUR 2 Mauritius Rupee 480 Rs
MVR 2 Rufiyaa 462 MVR
MWK 2 Malawi Kwacha 454 MWK
MXN 2 Mexican Peso 484 $
MXV 2 Mexican Unidad de Inversion (UDI) 979 MXV
MYR 2 Malaysian Ringgit 458 RM
MZN 2 Mozambique Metical 943 MZN
NAD 2 Namibia Dollar 516 $
NGN 2 Naira 566 ₦
NIO 2 Cordoba Oro 558 C$
NOK 2 Norwegian Krone 578 kr
NPR 2 Nepalese Rupee 524 Rs
NZD 2 New Zealand Dollar 554 $
OMR 3 Rial Omani 512 OMR
PAB 2 Balboa 590 PAB
PEN 2 Sol 604 PEN
PGK 2 Kina 598 PGK
PHP 2 Philippine Peso 608 ₱
PKR 2 Pakistan Rupee 586 Rs
PLN 2 Zloty 985 zł
PYG 0 Guarani 600 ₲
QAR 2 Qatari Rial 634 QAR
RON 2 Romanian Leu 946 lei
RSD 2 Serbian Dinar 941 RSD
RUB 2 Russian Ruble 643 ₽
RWF 0 Rwanda Franc 646 RF
SAR 2 Saudi Riyal 682 SAR
SBD 2 Solomon Islands Dollar 090 $
SCR 2 Seychelles Rupee 690 SCR
SDG 2 Sudanese Pound 938 SDG
SEK 2 Swedish Krona 752 kr
SGD 2 Singapore Dollar 702 $
SHP 2 Saint Helena Pound 654 £
SLE 2 Leone 925 SLE
SOS 2 Somali Shilling 706 SOS
SRD 2 Surinam Dollar 968 $
SSP 2 South Sudanese Pound 728 £
STN 2 Dobra 930 Db
SVC 2 El Salvador Colon 222 SVC
SYP 2 Syrian Pound 760 £
SZL 2 Lilangeni 748 SZL
THB 2 Baht 764 ฿
TJS 2 Somoni 972 TJS
TMT 2 Turkmenistan New Manat 934 TMT
TND 3 Tunisian Dinar 788 TND
TOP 2 Pa’anga 776 T$
TRY 2 Turkish Lira 949 ₺
TTD 2 Trinidad and Tobago Dollar 780 $
TWD 2 New Taiwan Dollar 901 $
TZS 2 Tanzanian Shilling 834 TZS
UAH 2 Hryvnia 980 ₴
UGX 0 Uganda Shilling 800 UGX
USD 2 US Dollar 840 $
USN 2 US Dollar (Next day) 997 USN
UYI 0 Uruguay Peso en Unidades Indexadas (UI) 940 UYI
UYU 2 Peso Uruguayo 858 $
UYW 4 Unidad Previsional 927 UYW
UZS 2 Uzbekistan Sum 860 UZS
VED 2 Bolívar Soberano 926 VED
VES 2 Bolívar Soberano 928 VES
VND 0 Dong 704 ₫
VUV 0 Vatu 548 VUV
WST 2 Tala 882 WST
XAD 2 Arab Accounting Dinar 396 XAD
XAF 0 CFA Franc BEAC 950 FCFA
XCD 2 East Caribbean Dollar 951 $
XCG 2 Caribbean Guilder 532 Cg.
XOF 0 CFA Franc BCEAO 952 F CFA
XPF 0 CFP Franc 953 CFPF
YER 2 Yemeni Rial 886 YER
ZAR 2 Rand 710 R
ZMW 2 Zambian Kwacha 967 ZK
ZWG 2 Zimbabwe Gold 924 ZWG
1 code decimals name numeric symbol
2 AED 2 UAE Dirham 784 AED
3 AFN 2 Afghani 971 ؋
4 ALL 2 Lek 008 ALL
5 AMD 2 Armenian Dram 051 ֏
6 AOA 2 Kwanza 973 Kz
7 ARS 2 Argentine Peso 032 $
8 AUD 2 Australian Dollar 036 $
9 AWG 2 Aruban Florin 533 AWG
10 AZN 2 Azerbaijan Manat 944 ₼
11 BAM 2 Convertible Mark 977 KM
12 BBD 2 Barbados Dollar 052 $
13 BDT 2 Taka 050 ৳
14 BHD 3 Bahraini Dinar 048 BHD
15 BIF 0 Burundi Franc 108 BIF
16 BMD 2 Bermudian Dollar 060 $
17 BND 2 Brunei Dollar 096 $
18 BOB 2 Boliviano 068 Bs
19 BOV 2 Mvdol 984 BOV
20 BRL 2 Brazilian Real 986 R$
21 BSD 2 Bahamian Dollar 044 $
22 BTN 2 Ngultrum 064 BTN
23 BWP 2 Pula 072 P
24 BYN 2 Belarusian Ruble 933 BYN
25 BZD 2 Belize Dollar 084 $
26 CAD 2 Canadian Dollar 124 $
27 CDF 2 Congolese Franc 976 CDF
28 CHE 2 WIR Euro 947 CHE
29 CHF 2 Swiss Franc 756 Fr.
30 CHW 2 WIR Franc 948 CHW
31 CLF 4 Unidad de Fomento 990 CLF
32 CLP 0 Chilean Peso 152 $
33 CNY 2 Yuan Renminbi 156 ¥
34 COP 2 Colombian Peso 170 $
35 COU 2 Unidad de Valor Real 970 COU
36 CRC 2 Costa Rican Colon 188 ₡
37 CUP 2 Cuban Peso 192 $
38 CVE 2 Cabo Verde Escudo 132 CVE
39 CZK 2 Czech Koruna 203 Kč
40 DJF 0 Djibouti Franc 262 DJF
41 DKK 2 Danish Krone 208 kr
42 DOP 2 Dominican Peso 214 $
43 DZD 2 Algerian Dinar 012 DZD
44 EGP 2 Egyptian Pound 818 E£
45 ERN 2 Nakfa 232 ERN
46 ETB 2 Ethiopian Birr 230 ETB
47 EUR 2 Euro 978 €
48 FJD 2 Fiji Dollar 242 $
49 FKP 2 Falkland Islands Pound 238 £
50 GBP 2 Pound Sterling 826 £
51 GEL 2 Lari 981 ₾
52 GHS 2 Ghana Cedi 936 GH₵
53 GIP 2 Gibraltar Pound 292 £
54 GMD 2 Dalasi 270 GMD
55 GNF 0 Guinean Franc 324 FG
56 GTQ 2 Quetzal 320 Q
57 GYD 2 Guyana Dollar 328 $
58 HKD 2 Hong Kong Dollar 344 $
59 HNL 2 Lempira 340 L
60 HTG 2 Gourde 332 HTG
61 HUF 2 Forint 348 Ft
62 IDR 2 Rupiah 360 Rp
63 ILS 2 New Israeli Sheqel 376 ₪
64 INR 2 Indian Rupee 356 ₹
65 IQD 3 Iraqi Dinar 368 IQD
66 IRR 2 Iranian Rial 364 IRR
67 ISK 0 Iceland Krona 352 kr
68 JMD 2 Jamaican Dollar 388 $
69 JOD 3 Jordanian Dinar 400 JOD
70 JPY 0 Yen 392 ¥
71 KES 2 Kenyan Shilling 404 KES
72 KGS 2 Som 417 ⃀
73 KHR 2 Riel 116 ៛
74 KMF 0 Comorian Franc 174 CF
75 KPW 2 North Korean Won 408 ₩
76 KRW 0 Won 410 ₩
77 KWD 3 Kuwaiti Dinar 414 KWD
78 KYD 2 Cayman Islands Dollar 136 $
79 KZT 2 Tenge 398 ₸
80 LAK 2 Lao Kip 418 ₭
81 LBP 2 Lebanese Pound 422 L£
82 LKR 2 Sri Lanka Rupee 144 Rs
83 LRD 2 Liberian Dollar 430 $
84 LSL 2 Loti 426 LSL
85 LYD 3 Libyan Dinar 434 LYD
86 MAD 2 Moroccan Dirham 504 MAD
87 MDL 2 Moldovan Leu 498 MDL
88 MGA 2 Malagasy Ariary 969 Ar
89 MKD 2 Denar 807 MKD
90 MMK 2 Kyat 104 K
91 MNT 2 Tugrik 496 ₮
92 MOP 2 Pataca 446 MOP
93 MRU 2 Ouguiya 929 MRU
94 MUR 2 Mauritius Rupee 480 Rs
95 MVR 2 Rufiyaa 462 MVR
96 MWK 2 Malawi Kwacha 454 MWK
97 MXN 2 Mexican Peso 484 $
98 MXV 2 Mexican Unidad de Inversion (UDI) 979 MXV
99 MYR 2 Malaysian Ringgit 458 RM
100 MZN 2 Mozambique Metical 943 MZN
101 NAD 2 Namibia Dollar 516 $
102 NGN 2 Naira 566 ₦
103 NIO 2 Cordoba Oro 558 C$
104 NOK 2 Norwegian Krone 578 kr
105 NPR 2 Nepalese Rupee 524 Rs
106 NZD 2 New Zealand Dollar 554 $
107 OMR 3 Rial Omani 512 OMR
108 PAB 2 Balboa 590 PAB
109 PEN 2 Sol 604 PEN
110 PGK 2 Kina 598 PGK
111 PHP 2 Philippine Peso 608 ₱
112 PKR 2 Pakistan Rupee 586 Rs
113 PLN 2 Zloty 985 zł
114 PYG 0 Guarani 600 ₲
115 QAR 2 Qatari Rial 634 QAR
116 RON 2 Romanian Leu 946 lei
117 RSD 2 Serbian Dinar 941 RSD
118 RUB 2 Russian Ruble 643 ₽
119 RWF 0 Rwanda Franc 646 RF
120 SAR 2 Saudi Riyal 682 SAR
121 SBD 2 Solomon Islands Dollar 090 $
122 SCR 2 Seychelles Rupee 690 SCR
123 SDG 2 Sudanese Pound 938 SDG
124 SEK 2 Swedish Krona 752 kr
125 SGD 2 Singapore Dollar 702 $
126 SHP 2 Saint Helena Pound 654 £
127 SLE 2 Leone 925 SLE
128 SOS 2 Somali Shilling 706 SOS
129 SRD 2 Surinam Dollar 968 $
130 SSP 2 South Sudanese Pound 728 £
131 STN 2 Dobra 930 Db
132 SVC 2 El Salvador Colon 222 SVC
133 SYP 2 Syrian Pound 760 £
134 SZL 2 Lilangeni 748 SZL
135 THB 2 Baht 764 ฿
136 TJS 2 Somoni 972 TJS
137 TMT 2 Turkmenistan New Manat 934 TMT
138 TND 3 Tunisian Dinar 788 TND
139 TOP 2 Pa’anga 776 T$
140 TRY 2 Turkish Lira 949 ₺
141 TTD 2 Trinidad and Tobago Dollar 780 $
142 TWD 2 New Taiwan Dollar 901 $
143 TZS 2 Tanzanian Shilling 834 TZS
144 UAH 2 Hryvnia 980 ₴
145 UGX 0 Uganda Shilling 800 UGX
146 USD 2 US Dollar 840 $
147 USN 2 US Dollar (Next day) 997 USN
148 UYI 0 Uruguay Peso en Unidades Indexadas (UI) 940 UYI
149 UYU 2 Peso Uruguayo 858 $
150 UYW 4 Unidad Previsional 927 UYW
151 UZS 2 Uzbekistan Sum 860 UZS
152 VED 2 Bolívar Soberano 926 VED
153 VES 2 Bolívar Soberano 928 VES
154 VND 0 Dong 704 ₫
155 VUV 0 Vatu 548 VUV
156 WST 2 Tala 882 WST
157 XAD 2 Arab Accounting Dinar 396 XAD
158 XAF 0 CFA Franc BEAC 950 FCFA
159 XCD 2 East Caribbean Dollar 951 $
160 XCG 2 Caribbean Guilder 532 Cg.
161 XOF 0 CFA Franc BCEAO 952 F CFA
162 XPF 0 CFP Franc 953 CFPF
163 YER 2 Yemeni Rial 886 YER
164 ZAR 2 Rand 710 R
165 ZMW 2 Zambian Kwacha 967 ZK
166 ZWG 2 Zimbabwe Gold 924 ZWG
+1 -18
View File
@@ -1,18 +1 @@
[ { "format": "{code} {reason}", "rows": "httpstatus.tsv", "key": "code", "name": "reason" }
{ "format": "{code} {reason}", "code": "200", "reason": "OK" },
{ "format": "{code} {reason}", "code": "201", "reason": "Created" },
{ "format": "{code} {reason}", "code": "204", "reason": "No Content" },
{ "format": "{code} {reason}", "code": "301", "reason": "Moved Permanently" },
{ "format": "{code} {reason}", "code": "302", "reason": "Found" },
{ "format": "{code} {reason}", "code": "304", "reason": "Not Modified" },
{ "format": "{code} {reason}", "code": "400", "reason": "Bad Request" },
{ "format": "{code} {reason}", "code": "401", "reason": "Unauthorized" },
{ "format": "{code} {reason}", "code": "403", "reason": "Forbidden" },
{ "format": "{code} {reason}", "code": "404", "reason": "Not Found" },
{ "format": "{code} {reason}", "code": "409", "reason": "Conflict" },
{ "format": "{code} {reason}", "code": "422", "reason": "Unprocessable Entity" },
{ "format": "{code} {reason}", "code": "429", "reason": "Too Many Requests" },
{ "format": "{code} {reason}", "code": "500", "reason": "Internal Server Error" },
{ "format": "{code} {reason}", "code": "502", "reason": "Bad Gateway" },
{ "format": "{code} {reason}", "code": "503", "reason": "Service Unavailable" }
]
+17
View File
@@ -0,0 +1,17 @@
code reason
200 OK
201 Created
204 No Content
301 Moved Permanently
302 Found
304 Not Modified
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
409 Conflict
422 Unprocessable Entity
429 Too Many Requests
500 Internal Server Error
502 Bad Gateway
503 Service Unavailable
1 code reason
2 200 OK
3 201 Created
4 204 No Content
5 301 Moved Permanently
6 302 Found
7 304 Not Modified
8 400 Bad Request
9 401 Unauthorized
10 403 Forbidden
11 404 Not Found
12 409 Conflict
13 422 Unprocessable Entity
14 429 Too Many Requests
15 500 Internal Server Error
16 502 Bad Gateway
17 503 Service Unavailable
+1 -18
View File
@@ -1,18 +1 @@
[ { "format": "{name}", "rows": "language.tsv", "key": "code", "name": "name" }
{ "format": "{name}", "name": "Arabic", "code": "ar" },
{ "format": "{name}", "name": "Chinese", "code": "zh" },
{ "format": "{name}", "name": "Danish", "code": "da" },
{ "format": "{name}", "name": "Dutch", "code": "nl" },
{ "format": "{name}", "name": "English", "code": "en" },
{ "format": "{name}", "name": "Finnish", "code": "fi" },
{ "format": "{name}", "name": "French", "code": "fr" },
{ "format": "{name}", "name": "German", "code": "de" },
{ "format": "{name}", "name": "Italian", "code": "it" },
{ "format": "{name}", "name": "Japanese", "code": "ja" },
{ "format": "{name}", "name": "Norwegian", "code": "no" },
{ "format": "{name}", "name": "Polish", "code": "pl" },
{ "format": "{name}", "name": "Portuguese", "code": "pt" },
{ "format": "{name}", "name": "Russian", "code": "ru" },
{ "format": "{name}", "name": "Spanish", "code": "es" },
{ "format": "{name}", "name": "Swedish", "code": "sv" }
]
+17
View File
@@ -0,0 +1,17 @@
code name
ar Arabic
zh Chinese
da Danish
nl Dutch
en English
fi Finnish
fr French
de German
it Italian
ja Japanese
no Norwegian
pl Polish
pt Portuguese
ru Russian
es Spanish
sv Swedish
1 code name
2 ar Arabic
3 zh Chinese
4 da Danish
5 nl Dutch
6 en English
7 fi Finnish
8 fr French
9 de German
10 it Italian
11 ja Japanese
12 no Norwegian
13 pl Polish
14 pt Portuguese
15 ru Russian
16 es Spanish
17 sv Swedish
+1 -17
View File
@@ -1,17 +1 @@
[ { "format": "{type}", "rows": "mimetype.tsv", "key": "type", "name": "ext" }
{ "format": "{type}", "type": "application/gzip", "ext": ".gz" },
{ "format": "{type}", "type": "application/json", "ext": ".json" },
{ "format": "{type}", "type": "application/pdf", "ext": ".pdf" },
{ "format": "{type}", "type": "application/xml", "ext": ".xml" },
{ "format": "{type}", "type": "application/zip", "ext": ".zip" },
{ "format": "{type}", "type": "audio/mpeg", "ext": ".mp3" },
{ "format": "{type}", "type": "image/gif", "ext": ".gif" },
{ "format": "{type}", "type": "image/jpeg", "ext": ".jpg" },
{ "format": "{type}", "type": "image/png", "ext": ".png" },
{ "format": "{type}", "type": "image/svg+xml", "ext": ".svg" },
{ "format": "{type}", "type": "image/webp", "ext": ".webp" },
{ "format": "{type}", "type": "text/csv", "ext": ".csv" },
{ "format": "{type}", "type": "text/html", "ext": ".html" },
{ "format": "{type}", "type": "text/plain", "ext": ".txt" },
{ "format": "{type}", "type": "video/mp4", "ext": ".mp4" }
]
+16
View File
@@ -0,0 +1,16 @@
ext type
.gz application/gzip
.json application/json
.pdf application/pdf
.xml application/xml
.zip application/zip
.mp3 audio/mpeg
.gif image/gif
.jpg image/jpeg
.png image/png
.svg image/svg+xml
.webp image/webp
.csv text/csv
.html text/html
.txt text/plain
.mp4 video/mp4
1 ext type
2 .gz application/gzip
3 .json application/json
4 .pdf application/pdf
5 .xml application/xml
6 .zip application/zip
7 .mp3 audio/mpeg
8 .gif image/gif
9 .jpg image/jpeg
10 .png image/png
11 .svg image/svg+xml
12 .webp image/webp
13 .csv text/csv
14 .html text/html
15 .txt text/plain
16 .mp4 video/mp4
+2 -2
View File
@@ -40,7 +40,7 @@ func TestDatatypeAndNullSitOnlyInAColumn(t *testing.T) {
func TestDatatypeRejectsAValueItsTypeRejects(t *testing.T) { func TestDatatypeRejectsAValueItsTypeRejects(t *testing.T) {
tree := map[string]string{ tree := map[string]string{
"cat": `[{"format":"{code}","code":"200"},{"format":"{code}","code":"2x"}]`, "cat": `[{"format":"{code}","code":"200"},{"format":"{code}","code":"2x","note":"n"}]`,
"src": `{"format":"","code":[null,"200","2x"],"flag":{"format":"{b}","b":["true","false"],"datatype":"boolean"},"score":[null,{"format":"{int(1,9)}","datatype":"integer"}]}`, "src": `{"format":"","code":[null,"200","2x"],"flag":{"format":"{b}","b":["true","false"],"datatype":"boolean"},"score":[null,{"format":"{int(1,9)}","datatype":"integer"}]}`,
} }
for _, c := range []struct{ name, column, want string }{ for _, c := range []struct{ name, column, want string }{
@@ -114,7 +114,7 @@ func TestDatatypeRejectsAValueItsTypeRejects(t *testing.T) {
var jsonInteger = regexp.MustCompile(`^(0|-?[1-9][0-9]*)$`) var jsonInteger = regexp.MustCompile(`^(0|-?[1-9][0-9]*)$`)
func TestDatatypeAcceptsAColumnThatAlwaysParses(t *testing.T) { func TestDatatypeAcceptsAColumnThatAlwaysParses(t *testing.T) {
cat := `[{"format":"{code}","code":"200"},{"format":"{code}","code":"404"}]` cat := `[{"format":"{code}","code":"200"},{"format":"{code}","code":"404","reason":"Not Found"}]`
for _, column := range []string{ for _, column := range []string{
`{"format":"{int(1,99)}","datatype":"integer"}`, `{"format":"{int(1,99)}","datatype":"integer"}`,
`{"format":"{int(-9,-1)}","datatype":"integer"}`, `{"format":"{int(-9,-1)}","datatype":"integer"}`,
+152 -17
View File
@@ -13,21 +13,23 @@ type drawSet struct {
} }
// drawScope is where a render reads its reference paths: a draw set, in the draw group of the // drawScope is where a render reads its reference paths: a draw set, in the draw group of the
// template rendering. // template rendering, and the row a table's format is rendering, which its columns read.
type drawScope struct { type drawScope struct {
set *drawSet set *drawSet
group string group string
t *table
row int
} }
// newDrawSet makes the unnamed group's maps where the set is declared, keeping them on that frame's // newDrawSet makes the unnamed group's maps where the set is declared, keeping them on that frame's
// stack for a render that reads through them; a zero drawSet makes them on its first read instead. // stack for a render that reads through them; a zero drawSet makes them on its first read instead.
func newDrawSet() drawSet { func newDrawSet(s *session) drawSet {
return drawSet{unnamed: draws{variant: map[string]node{}, value: map[string]draw{}}} return drawSet{unnamed: draws{variant: map[string]node{}, value: map[string]draw{}, s: s}}
} }
// renderOnce renders n as one render, over draws of its own. // renderOnce renders n as one render, over draws of its own.
func renderOnce(s *session, n node) string { func renderOnce(s *session, n node) string {
var set drawSet set := drawSet{unnamed: draws{s: s}}
return render(s, n, drawScope{set: &set}) return render(s, n, drawScope{set: &set})
} }
@@ -39,8 +41,9 @@ func (sc drawScope) in(t *template) drawScope {
return sc return sc
} }
// draws is the set's draws for sc's draw group. // draws is the set's draws for sc's draw group. The session is passed in rather than
func (sc drawScope) draws() *draws { // read out of the set: copying it from there would leak the set's maps to the heap.
func (sc drawScope) draws(s *session) *draws {
if sc.group == "" { if sc.group == "" {
return &sc.set.unnamed return &sc.set.unnamed
} }
@@ -49,7 +52,7 @@ func (sc drawScope) draws() *draws {
if sc.set.named == nil { if sc.set.named == nil {
sc.set.named = map[string]*draws{} sc.set.named = map[string]*draws{}
} }
d = &draws{variant: map[string]node{}, value: map[string]draw{}} d = &draws{variant: map[string]node{}, value: map[string]draw{}, s: s}
sc.set.named[strings.Clone(sc.group)] = d // a key from sc would leak sc, and with it every render's draw set, to the heap sc.set.named[strings.Clone(sc.group)] = d // a key from sc would leak sc, and with it every render's draw set, to the heap
} }
return d return d
@@ -134,7 +137,13 @@ func (c *drawCheck) checkDrawGroup(path string, n node) error {
func (c *drawCheck) checkDraws(path string, n node) error { func (c *drawCheck) checkDraws(path string, n node) error {
t, ok := n.(*template) t, ok := n.(*template)
if !ok || !c.readsPath(t) { if !ok {
return nil
}
if err := checkOwnFamily(t); err != nil {
return fmt.Errorf("%s: %w", path, err)
}
if !c.readsPath(t) {
return nil return nil
} }
w := newDrawWalk() w := newDrawWalk()
@@ -147,6 +156,43 @@ func (c *drawCheck) checkDraws(path string, n node) error {
return nil return nil
} }
// checkOwnFamily refuses a table's format or cell that reads, however many templates
// away and through a repeat or a draw group too, a table of its own family: a row
// rendered whole draws its row without pinning it, so the family would draw apart
// from the row being rendered, whichever draws the reaching template holds.
func checkOwnFamily(t *template) error {
own := t.table
if own == nil {
own = t.cellOf
}
if own == nil {
return nil
}
seen := map[node]bool{}
var find func(n node) (renderEdge, *table, bool)
find = func(n node) (renderEdge, *table, bool) {
if seen[n] {
return renderEdge{}, nil, false
}
seen[n] = true
for _, e := range renderEdges(n) {
if a, isRef := refRead(n, e.label); isRef {
if head, isTable := n.(*template).fields[a.key].(*table); isTable && head.family() == own.family() {
return e, head, true
}
}
if e, head, found := find(e.to); found {
return e, head, true
}
}
return renderEdge{}, nil, false
}
if e, head, found := find(t); found {
return fmt.Errorf("%s reads %s, a table of its own family, which a row of %s rendered whole would draw apart from; read the family from a template beside it, or add the value as a column", e.reached(), head.category, own.category)
}
return nil
}
// checkRecordDraws fences the columns of a record — a template compiled at the top without a // checkRecordDraws fences the columns of a record — a template compiled at the top without a
// repeat — so a load proves the record view of it as well as the string view. // repeat — so a load proves the record view of it as well as the string view.
func (c *drawCheck) checkRecordDraws(path string, n node) error { func (c *drawCheck) checkRecordDraws(path string, n node) error {
@@ -186,7 +232,7 @@ func (c *drawCheck) reads(n node, stopAtGroup bool) bool {
} }
r := false r := false
for _, e := range renderEdges(n) { for _, e := range renderEdges(n) {
if a, isRef := refRead(n, e.label); (isRef && len(a.tail) > 0) || (!repeats(e.to) && !(stopAtGroup && grouped(e.to)) && c.reads(e.to, stopAtGroup)) { if a, isRef := refRead(n, e.label); (isRef && (len(a.tail) > 0 || readsTable(n, a))) || (!repeats(e.to) && !(stopAtGroup && grouped(e.to)) && c.reads(e.to, stopAtGroup)) {
r = true r = true
break break
} }
@@ -208,6 +254,17 @@ func grouped(n node) bool {
return isTemplate && t.drawGroupKey != "" return isTemplate && t.drawGroupKey != ""
} }
// readsTable reports whether a reference of n names a table, whose draw the render's
// group answers for even when read whole.
func readsTable(n node, a arm) bool {
t, isTemplate := n.(*template)
if !isTemplate {
return false
}
_, isTable := t.fields[a.key].(*table)
return isTable
}
// refRead is the reference an edge of n reads; false when the edge reads none. // refRead is the reference an edge of n reads; false when the edge reads none.
func refRead(n node, label string) (arm, bool) { func refRead(n node, label string) (arm, bool) {
t, isTemplate := n.(*template) t, isTemplate := n.(*template)
@@ -234,10 +291,55 @@ type drawWalk struct {
seen map[drawVisit]bool seen map[drawVisit]bool
} }
// drawAt is where a walk stands: the draw group it draws in, and how the render's root reached it. // drawAt is where a walk stands: the draw group it draws in, how the render's root reached it, and
// the table rows it entered.
type drawAt struct { type drawAt struct {
group string group string
route drawRoute route drawRoute
alt rowSet
}
// rowSet is the rows a walk entered, one per table: only one row of a table renders, so reads in
// two rows of one table never meet, while reads in one row, across its columns and whatever they
// reach, do.
type rowSet []tablePin
func (s rowSet) rowOf(t *table) (int, bool) {
for _, p := range s {
if p.t == t {
return p.row, true
}
}
return 0, false
}
// enter is s with row r of t, where t is not in it yet; the set is copied, since walks branch.
func (s rowSet) enter(t *table, r int) rowSet {
if _, in := s.rowOf(t); in {
return s
}
out := append(append(make(rowSet, 0, len(s)+1), s...), tablePin{t, r})
sort.Slice(out, func(i, j int) bool { return out[i].t.category < out[j].t.category })
return out
}
// key spells the set for a map, by the tables' identities.
func (s rowSet) key() string {
var b strings.Builder
for _, p := range s {
fmt.Fprintf(&b, "%p[%d]", p.t, p.row)
}
return b.String()
}
// alternatives reports whether two reads sit in different rows of one table.
func alternatives(a, b drawAt) bool {
for _, p := range a.alt {
if r, in := b.alt.rowOf(p.t); in && r != p.row {
return true
}
}
return false
} }
// drawRoute is how a render reaches a draw: as its author spells it, and the root edge's label. // drawRoute is how a render reaches a draw: as its author spells it, and the root edge's label.
@@ -247,16 +349,19 @@ type drawRoute struct{ spelling, label string }
type pathRead struct { type pathRead struct {
at drawAt at drawAt
a arm a arm
tr *tableRead // set where the reference names a table
} }
type drawVisit struct { type drawVisit struct {
n node n node
group string group string
alt string
} }
type drawKey struct { type drawKey struct {
group string group string
path string path string
alt string
} }
func newDrawWalk() *drawWalk { func newDrawWalk() *drawWalk {
@@ -266,7 +371,7 @@ func newDrawWalk() *drawWalk {
// walk follows what rendering n renders. A repeat renders over draws of its own, so the walk stops // walk follows what rendering n renders. A repeat renders over draws of its own, so the walk stops
// there. // there.
func (w *drawWalk) walk(n node, at drawAt) { func (w *drawWalk) walk(n node, at drawAt) {
v := drawVisit{n, at.group} v := drawVisit{n, at.group, at.alt.key()}
if w.seen[v] { if w.seen[v] {
return return
} }
@@ -278,23 +383,50 @@ func (w *drawWalk) walk(n node, at drawAt) {
at.group = t.drawGroupKey at.group = t.drawGroupKey
} }
for _, e := range renderEdges(n) { for _, e := range renderEdges(n) {
if cell, isCell := e.to.(*template); isCell && cell.cellOf != nil {
if at.alt.excludes(cell) {
continue
}
w.edge(n, e, drawAt{at.group, at.route, at.alt.enter(cell.cellOf, cell.cellRow)})
continue
}
w.edge(n, e, at) w.edge(n, e, at)
} }
} }
// excludes reports whether a cell's row cannot render with the rows entered: another
// row of its table, or a row outside an entered ancestor's.
func (s rowSet) excludes(cell *template) bool {
for _, p := range s {
if p.t == cell.cellOf {
return p.row != cell.cellRow
}
if cell.cellOf.descends(p.t) && !cell.cellOf.under(cell.cellRow, p.t, p.row) {
return true
}
}
return false
}
// edge records the reference an edge reads, then walks on with every row the read
// pins entered, so a selected row renders only its own cells.
func (w *drawWalk) edge(from node, e renderEdge, at drawAt) { func (w *drawWalk) edge(from node, e renderEdge, at drawAt) {
if a, reads := refRead(from, e.label); reads { if a, reads := refRead(from, e.label); reads {
if k := (drawKey{at.group, a.path}); !w.read[k] { tr := tableReadOf(from.(*template).fields[a.key], a, e.to)
if k := (drawKey{at.group, a.path, at.alt.key()}); !w.read[k] {
w.read[k] = true w.read[k] = true
w.reads = append(w.reads, pathRead{at, a}) w.reads = append(w.reads, pathRead{at, a, tr})
}
if tr != nil {
tr.pins.each(func(t *table, r int) { at.alt = at.alt.enter(t, r) })
} }
} }
w.walk(e.to, at) w.walk(e.to, at)
} }
// check refuses what one draw per reference path cannot answer for: a read of a level beside a path // check refuses what one draw per reference path cannot answer for: a read of a level beside a path
// another read takes into it. Reads are compared in path order, so which pair is reported does not // another read takes into it, and two reads of one table family that select different rows. Reads
// vary. // are compared in path order, so which pair is reported does not vary.
func (w *drawWalk) check() error { func (w *drawWalk) check() error {
sort.SliceStable(w.reads, func(i, j int) bool { sort.SliceStable(w.reads, func(i, j int) bool {
if w.reads[i].at.group != w.reads[j].at.group { if w.reads[i].at.group != w.reads[j].at.group {
@@ -304,12 +436,15 @@ func (w *drawWalk) check() error {
}) })
for i, level := range w.reads { for i, level := range w.reads {
for _, into := range w.reads[i+1:] { for _, into := range w.reads[i+1:] {
if into.at.group == level.at.group && strings.HasPrefix(into.a.path, level.a.path+".") { if into.at.group != level.at.group || alternatives(level.at, into.at) {
continue
}
if strings.HasPrefix(into.a.path, level.a.path+".") && !(level.tr != nil && level.tr.whole) {
return overlapError(level.at.route, level.a.name, into) return overlapError(level.at.route, level.a.name, into)
} }
} }
} }
return nil return checkFamilies(w.reads)
} }
func overlapError(route drawRoute, ref string, into pathRead) error { func overlapError(route drawRoute, ref string, into pathRead) error {
+319
View File
@@ -0,0 +1,319 @@
package fejkdata
import (
"fmt"
"sort"
"strings"
)
// tablePin is one table's pinned row.
type tablePin struct {
t *table
row int
}
// pinned is the row the render pinned for t, if any.
func (d *draws) pinned(t *table) (int, bool) {
for _, p := range d.pins[:d.npins] {
if p.t == t {
return p.row, true
}
}
r, ok := d.more[t]
return r, ok
}
// mustRow is the row pinned for t, which the walk reaching a column pinned.
func (d *draws) mustRow(t *table) int {
r, ok := d.pinned(t)
if !ok {
panic(fmt.Sprintf("fejkdata: a column of %s is rendered with no row pinned", t.category))
}
return r
}
// pin pins row r of t, and the rows of t's ancestors it links to.
func (d *draws) pin(t *table, r int) {
for {
if _, done := d.pinned(t); done {
return
}
if d.npins < len(d.pins) {
d.pins[d.npins] = tablePin{t, r}
d.npins++
} else {
if d.more == nil {
d.more = map[*table]int{}
}
d.more[t] = r
}
if t.parentT == nil {
return
}
t, r = t.parentT, t.parentRow(r)
}
}
// each calls fn for every pinned row, in pin order, the spilled ones by table name.
func (d *draws) each(fn func(t *table, r int)) {
for _, p := range d.pins[:d.npins] {
fn(p.t, p.row)
}
spilled := make([]*table, 0, len(d.more))
for t := range d.more {
spilled = append(spilled, t)
}
sort.Slice(spilled, func(i, j int) bool { return spilled[i].category < spilled[j].category })
for _, t := range spilled {
fn(t, d.more[t])
}
}
// rowOf is the render's row of t: the one pinned, else one drawn inside the
// nearest pinned ancestor — its parent drawn inside that first where the ancestor
// is further up — or over the whole table, and pinned with its ancestors.
func (d *draws) rowOf(t *table) int {
if r, ok := d.pinned(t); ok {
return r
}
r := -1
for a := t.parentT; a != nil && r < 0; a = a.parentT {
if _, ok := d.pinned(a); !ok {
continue
}
if t.parentT != a {
d.rowOf(t.parentT)
}
pr, _ := d.pinned(t.parentT)
r = t.drawUnder(d.s, pr)
}
if r < 0 {
r = t.draw(d.s)
}
d.pin(t, r)
return r
}
// pinRow pins row r of t where it agrees with the rows pinned before it.
func (d *draws) pinRow(t *table, r int) error {
if pr, ok := d.pinned(t); ok && pr != r {
return fmt.Errorf("%s and %s are two rows of %s", t.selectorSpelling(pr), t.selectorSpelling(r), t.category)
}
for a := t.parentT; a != nil; a = a.parentT {
if pa, ok := d.pinned(a); ok && !t.under(r, a, pa) {
return fmt.Errorf("%s is not inside %s", t.selectorSpelling(r), a.selectorSpelling(pa))
}
}
d.pin(t, r)
return nil
}
// selectRow pins the row a selector names.
func (d *draws) selectRow(t *table, sel string) error {
r, err := t.find(sel, d)
if err != nil {
return err
}
return d.pinRow(t, r)
}
// inside keeps the rows of t that sit inside every pinned ancestor.
func (d *draws) inside(t *table, rows []int) []int {
for a := t.parentT; a != nil; a = a.parentT {
pa, ok := d.pinned(a)
if !ok {
continue
}
var kept []int
for _, r := range rows {
if t.under(r, a, pa) {
kept = append(kept, r)
}
}
rows = kept
}
return rows
}
// tableRead is what a reference path reads of a table family: the table its head
// names, the rows its selectors pin, the tables it draws — those it walks with no
// row pinned, and their unpinned ancestors — each selector's spelling, and whether
// it lands on a row rendered whole.
type tableRead struct {
head *table
pins draws
drawn map[*table]bool
sels []tableSel
whole bool
}
// tableSel is one selector on the way: the table it selects a row of, and the path
// as written up to and including it.
type tableSel struct {
t *table
spelling string
}
// tableReadOf replays a reference's selectors through the walk a render uses, so
// two paths pinning one row by different routes compare equal. checkPath proved
// each selector names a row.
func tableReadOf(head node, a arm, leaf node) *tableRead {
t, isTable := head.(*table)
if !isTable {
return nil
}
tr := &tableRead{head: t, drawn: map[*table]bool{}}
_, _ = walkPath(t, a.tail, pathWalk{pins: &tr.pins})
written := a.name[:len(a.name)-len(joinSegments(a.tail))]
cur := t
tr.draws(cur)
for i, seg := range a.tail {
switch d := cur.descendant(seg); {
case isSelector(seg):
tr.sels = append(tr.sels, tableSel{cur, written + joinSegments(a.tail[:i+1])})
case d != nil:
cur = d
tr.draws(cur)
}
}
_, tr.whole = leaf.(*row)
return tr
}
// draws marks t and its ancestors drawn, up to the first the read pins.
func (r *tableRead) draws(t *table) {
for ; t != nil; t = t.parentT {
if _, pinned := r.pins.pinned(t); pinned {
return
}
r.drawn[t] = true
}
}
// joinSegments spells segments as a path: a selector attaches to the name before it.
func joinSegments(segs []string) string {
var b strings.Builder
for _, s := range segs {
if b.Len() > 0 && !isSelector(s) {
b.WriteByte('.')
}
b.WriteString(s)
}
return b.String()
}
// family is the table a chain of parents ends at.
func (t *table) family() *table {
for t.parentT != nil {
t = t.parentT
}
return t
}
// selected is the selector in r on t, or on the nearest ancestor of t it selects.
func (r *tableRead) selected(t *table) (tableSel, bool) {
for ; t != nil; t = t.parentT {
for _, s := range r.sels {
if s.t == t {
return s, true
}
}
}
return tableSel{}, false
}
// checkFamilies refuses reads of one table family in one draw group that cannot
// read one consistent draw: a table rendered whole beside a path into the family,
// a table one read draws that another pins, and two reads pinning different rows.
// The reads come sorted by group and path, so which pair is reported does not vary.
func checkFamilies(reads []pathRead) error {
for i, r := range reads {
if r.tr == nil {
continue
}
for _, o := range reads[:i] {
if o.tr == nil || o.at.group != r.at.group || alternatives(o.at, r.at) || o.tr.head.family() != r.tr.head.family() {
continue
}
if err := checkFamilyPair(o, r); err != nil {
return err
}
}
}
return replayPairs(reads)
}
// replayPairs replays every two reads of one draw group that can render together into
// one draws, the earlier read first. A pin conflicts with one earlier pin, never with a
// combination, so pairs find every conflict a full replay would.
func replayPairs(reads []pathRead) error {
for i, r := range reads {
if r.tr == nil {
continue
}
for _, o := range reads[i+1:] {
if o.tr == nil || o.at.group != r.at.group || alternatives(r.at, o.at) {
continue
}
var d draws
if err := r.tr.replay(&d); err != nil {
return conflict(r, err)
}
if err := o.tr.replay(&d); err != nil {
return conflict(o, err)
}
}
}
return nil
}
func conflict(r pathRead, err error) error {
return fmt.Errorf("%s: %w; select the same rows in every path into the family, or draw them apart with a drawGroup", r.at.route.spelled(r.a.name), err)
}
// replay pins the read's rows into d, where they agree with the rows pinned before.
func (r *tableRead) replay(d *draws) error {
var err error
r.pins.each(func(t *table, row int) {
if err == nil {
err = d.pinRow(t, row)
}
})
return err
}
// checkFamilyPair refuses a table read whole beside a path into its family, and a
// table one read draws that the other pins, since which token renders first would
// then decide the row.
func checkFamilyPair(a, b pathRead) error {
for _, pair := range [][2]pathRead{{a, b}, {b, a}} {
x, y := pair[0], pair[1]
if len(x.a.tail) == 0 && len(y.a.tail) > 0 {
return overlapError(x.at.route, x.a.name, y)
}
if drawn := x.tr.drawnOf(&y.tr.pins); drawn != nil {
if s, ok := y.tr.selected(x.tr.head); ok && len(x.tr.sels) == 0 {
tail := x.a.tail
if s.t != x.tr.head {
tail = append([]string{x.tr.head.category}, tail...)
}
return fmt.Errorf("%s draws %s, which %s selects a row of; write {%s.%s}, or draw them apart with a drawGroup",
x.at.route.spelled(x.a.name), drawn.category, y.at.route.spelled(y.a.name), s.spelling, joinSegments(tail))
}
return fmt.Errorf("%s draws %s, which %s selects a row of; select that row in both, or draw them apart with a drawGroup",
x.at.route.spelled(x.a.name), drawn.category, y.at.route.spelled(y.a.name))
}
}
return nil
}
// drawnOf is a table the read draws that pins holds a row of, if any.
func (r *tableRead) drawnOf(pins *draws) *table {
var found *table
pins.each(func(t *table, _ int) {
if found == nil && r.drawn[t] {
found = t
}
})
return found
}
+24
View File
@@ -46,6 +46,8 @@ type Generator struct {
mu sync.Mutex mu sync.Mutex
rand *session rand *session
categories map[string]node categories map[string]node
root folder // the categories as the node a path walks from, owned here so a walk allocates none
set drawSet // one Fake's draws, owned here so a walk pinning rows keeps them off the heap
records map[node]recordShape records map[node]recordShape
structs map[reflect.Type]structResult structs map[reflect.Type]structResult
} }
@@ -166,6 +168,10 @@ func paths(n node) []string {
return out return out
case *null: case *null:
return []string{""} return []string{""}
case *table:
return tablePaths(n)
case *column, *row:
return []string{""}
case *choice: case *choice:
out := []string{""} out := []string{""}
for p := range n.shared { for p := range n.shared {
@@ -176,6 +182,24 @@ func paths(n node) []string {
return nil return nil
} }
// tablePaths is a table's columns, then each table linked to it under its name: the
// direct descents, a step at a time.
func tablePaths(t *table) []string {
out := append([]string{""}, t.columns...)
sort.Strings(out[1:])
children := make([]string, 0, len(t.children))
for name := range t.children {
children = append(children, name)
}
sort.Strings(children)
for _, name := range children {
for _, p := range paths(t.children[name]) {
out = append(out, join(name, p))
}
}
return out
}
// sharedPaths is the sub-paths every item carries — the only ones a path may step // sharedPaths is the sub-paths every item carries — the only ones a path may step
// through a choice to reach. It intersects, bailing as soon as the set // through a choice to reach. It intersects, bailing as soon as the set
// is empty, which is immediate for a choice of plain strings. // is empty, which is immediate for a choice of plain strings.
+22 -3
View File
@@ -3,7 +3,6 @@ package fejkdata
import ( import (
"fmt" "fmt"
"sort" "sort"
"strings"
) )
// walkNodes calls fn once per contained node, passing the dot path that reaches it, // walkNodes calls fn once per contained node, passing the dot path that reaches it,
@@ -61,6 +60,16 @@ func contained(n node) []namedNode {
return out return out
case *template: case *template:
return named(n.fields) return named(n.fields)
case *table:
return append([]namedNode{{node: n.format}}, named(n.fields)...)
case *column:
var out []namedNode
for r := 0; r < n.t.rows(); r++ {
if cell := n.t.cellNode(r, n.i); cell != nil {
out = append(out, namedNode{node: cell})
}
}
return out
default: default:
return nil return nil
} }
@@ -142,12 +151,22 @@ func renderEdges(n node) []renderEdge {
} }
return nil return nil
} }
for _, name := range strings.Split(t.body, "|") { for _, name := range splitOutside(t.body, '|') {
add(name, "") add(name, "")
} }
return nil return nil
}) })
return es return es
case *table:
return []renderEdge{{to: n.format, label: "format"}}
case *row:
return []renderEdge{{to: n.t.format, label: "format"}}
case *column:
var es []renderEdge
for _, c := range contained(n) {
es = append(es, renderEdge{to: c.node, label: n.t.columns[n.i]})
}
return es
default: default:
return nil return nil
} }
@@ -158,7 +177,7 @@ func renderEdges(n node) []renderEdge {
// already proved the tail resolves in every variant. // already proved the tail resolves in every variant.
func pathLeaves(n node, tail []string) []node { func pathLeaves(n node, tail []string) []node {
var out []node var out []node
_ = walkPath(n, tail, pathWalk{ _, _ = walkPath(n, tail, pathWalk{
choice: func(c *choice, _ []string) ([]node, error) { return c.items, nil }, choice: func(c *choice, _ []string) ([]node, error) { return c.items, nil },
leaf: func(n node) error { out = append(out, n); return nil }, leaf: func(n node) error { out = append(out, n); return nil },
}) })
+15 -8
View File
@@ -109,7 +109,7 @@ func operandReader(t *template, head string) string {
// coverPath collects what holding one path pins: every choice level the path // coverPath collects what holding one path pins: every choice level the path
// passes through, whole, and the leaf it renders. // passes through, whole, and the leaf it renders.
func coverPath(n node, tail []string, into map[node]bool) { func coverPath(n node, tail []string, into map[node]bool) {
_ = walkPath(n, tail, pathWalk{ _, _ = walkPath(n, tail, pathWalk{
choice: func(c *choice, _ []string) ([]node, error) { cover(c, into, false); return nil, nil }, choice: func(c *choice, _ []string) ([]node, error) { cover(c, into, false); return nil, nil },
leaf: func(n node) error { cover(n, into, false); return nil }, leaf: func(n node) error { cover(n, into, false); return nil },
}) })
@@ -262,6 +262,10 @@ func checkNoRepeatedRead(format string, c formatOps, refs map[string]refBinding)
type draws struct { type draws struct {
variant map[string]node variant map[string]node
value map[string]draw value map[string]draw
pins [8]tablePin // the rows pinned, inline so a render over a country's five-deep geo tree stays off the heap
npins int
more map[*table]int // the rows pinned past the inline eight
s *session // what draws a row; nil where a walk only proves selectors
} }
// draw is what one read drew: its text, and whether it landed on a null. // draw is what one read drew: its text, and whether it landed on a null.
@@ -284,12 +288,11 @@ func readField(s *session, t *template, held *draws, sc drawScope, a arm) draw {
} }
return draw{text: render(s, t.fields[a.key], sc)} return draw{text: render(s, t.fields[a.key], sc)}
} }
d := readScope(held, sc, a) d := readScope(s, held, sc, a)
if r, done := d.value[a.path]; done { if r, done := d.value[a.path]; done {
return r return r
} }
var r draw leaf, err := walkPath(t.fields[a.key], a.tail, pathWalk{
_ = walkPath(t.fields[a.key], a.tail, pathWalk{
// Hold the draw at every level passed through, so two paths sharing a // Hold the draw at every level passed through, so two paths sharing a
// prefix share it. // prefix share it.
choice: func(c *choice, rest []string) ([]node, error) { choice: func(c *choice, rest []string) ([]node, error) {
@@ -307,8 +310,12 @@ func readField(s *session, t *template, held *draws, sc drawScope, a arm) draw {
} }
return []node{n}, nil return []node{n}, nil
}, },
leaf: func(n node) error { r = renderLeaf(s, n, sc); return nil }, pins: d,
}) })
if err != nil {
panic(fmt.Sprintf("fejkdata: %q: %v; a fence should have refused this at New", a.name, err))
}
r := renderLeaf(s, leaf, sc)
if d.value == nil { if d.value == nil {
d.value = map[string]draw{} d.value = map[string]draw{}
} }
@@ -319,9 +326,9 @@ func readField(s *session, t *template, held *draws, sc drawScope, a arm) draw {
// readScope is the draws a held read keeps its draw in: for a reference that reads a path, // readScope is the draws a held read keeps its draw in: for a reference that reads a path,
// the render's draws for its group, so its draw spans the render; for a sibling, or a // the render's draws for its group, so its draw spans the render; for a sibling, or a
// reference read whole, held. // reference read whole, held.
func readScope(held *draws, sc drawScope, a arm) *draws { func readScope(s *session, held *draws, sc drawScope, a arm) *draws {
if isRef(a.key) && len(a.tail) > 0 { if isRef(a.key) && len(a.tail) > 0 {
return sc.draws() return sc.draws(s)
} }
return held return held
} }
@@ -335,7 +342,7 @@ func renderLeaf(s *session, n node, sc drawScope) draw {
} }
r := draw{text: render(s, n, sc)} r := draw{text: render(s, n, sc)}
if t, _ := n.(*template); t != nil && t.readsColumn != nil { if t, _ := n.(*template); t != nil && t.readsColumn != nil {
r.null = sc.in(t).draws().value[t.readsColumn.a.path].null r.null = sc.in(t).draws(s).value[t.readsColumn.a.path].null
} }
return r return r
} }
+7 -1
View File
@@ -69,9 +69,15 @@ func isTemplate(arg string) (bool, error) {
} }
return true, nil return true, nil
} }
if i := strings.IndexAny(arg, `[]}"`); i >= 0 { if i := strings.IndexAny(arg, `}"`); i >= 0 {
return false, fmt.Errorf("%q holds a %q, which no path may, and it is not valid JSON, so it names no template either", arg, arg[i:i+1]) return false, fmt.Errorf("%q holds a %q, which no path may, and it is not valid JSON, so it names no template either", arg, arg[i:i+1])
} }
if strings.HasPrefix(strings.TrimSpace(arg), "[") {
return false, fmt.Errorf(`%q starts with "[" and is not valid JSON, so it names no template; a path starts with a name`, arg)
}
if _, err := splitPath(arg); err != nil {
return false, err
}
if path := strings.TrimLeft(arg, "/"); path != arg && path != "" { if path := strings.TrimLeft(arg, "/"); path != arg && path != "" {
return false, pathAdvice(path, fmt.Sprintf("path %s starts with /, and every path starts at the root already", arg)) return false, pathAdvice(path, fmt.Sprintf("path %s starts with /, and every path starts at the root already", arg))
} }
+8 -4
View File
@@ -55,7 +55,7 @@ func TestFakeTemplateJSONArray(t *testing.T) {
func TestFakeTemplateCorrelatedReferences(t *testing.T) { func TestFakeTemplateCorrelatedReferences(t *testing.T) {
dir := writeData(t, map[string]string{ dir := writeData(t, map[string]string{
"person": `[{"format":"{first} {last}","first":"Ada","last":"Lovelace"},{"format":"{first} {last}","first":"Bo","last":"Ek"}]`, "person": `[{"format":"{first} {last}","first":"Ada","last":"Lovelace"},{"format":"{first} {last}","first":"Bo","last":"Ek","born":"1990"}]`,
}) })
f := newGenerator(t, dir, WithSeed(1)) f := newGenerator(t, dir, WithSeed(1))
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
@@ -166,15 +166,19 @@ func TestIsTemplate(t *testing.T) {
"{uppercase(/a)}": true, "{uppercase(/a)}": true,
"{{/a}}": true, "{{/a}}": true,
`"{/a} x"`: true, `"{/a} x"`: true,
"x[1]": false, // a row selected by key or name
"x[St. Louis].y": false,
} { } {
if got, err := IsTemplate(arg); err != nil || got != want { if got, err := IsTemplate(arg); err != nil || got != want {
t.Errorf("IsTemplate(%q) = %v, %v; want %v", arg, got, err, want) t.Errorf("IsTemplate(%q) = %v, %v; want %v", arg, got, err, want)
} }
} }
for arg, want := range map[string]string{ for arg, want := range map[string]string{
"[abc]": `holds a "["`, "[abc]": `starts with "["`,
"[abc].field": `holds a "["`, "[abc].field": `starts with "["`,
"x[1]": `holds a "["`, "x[1]y": `"]"`,
"x[[1]]": `"["`,
"x[]": "empty",
"a]b": `holds a "]"`, "a]b": `holds a "]"`,
"a}b": `holds a "}"`, "a}b": `holds a "}"`,
`"abc`: `holds a "\""`, `"abc`: `holds a "\""`,
+1 -1
View File
@@ -154,7 +154,7 @@ func TestListedPathsAllRender(t *testing.T) {
} }
} }
} }
for _, p := range []string{"misc.car.maker", "misc.country.alpha2", "misc.currency.symbol", "misc.httpstatus.code", "misc.mimetype.ext"} { for _, p := range []string{"misc.car.maker", "misc.country.alpha2", "misc.country.numeric", "misc.currency.symbol", "misc.httpstatus.code", "misc.mimetype.ext"} {
if !slices.Contains(paths, p) { if !slices.Contains(paths, p) {
t.Errorf("List() omits %q, which the README advertises and Fake renders", p) t.Errorf("List() omits %q, which the README advertises and Fake renders", p)
} }
+78 -2
View File
@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"math" "math"
"slices"
"sort" "sort"
"strings" "strings"
) )
@@ -59,9 +60,13 @@ type template struct {
// held is every name drawn once per expansion: the bound levels above, plus the // held is every name drawn once per expansion: the bound levels above, plus the
// siblings a {calc()} reads. nil when the format holds nothing (see expand). // siblings a {calc()} reads. nil when the format holds nothing (see expand).
held map[string]bool held map[string]bool
heldLocal bool // some held name is kept by the expansion itself, so expand makes its draws
fromString bool // written as a JSON string rather than an object fromString bool // written as a JSON string rather than an object
readsColumn *columnRead // set when the format is one reference alone reading a record's column readsColumn *columnRead // set when the format is one reference alone reading a record's column
record bool // compiled at the top without a repeat, so its fields are record columns record bool // compiled at the top without a repeat, so its fields are record columns
table *table // the table whose format this is, whose columns are the fields
cellOf *table // the table whose cell this is
cellRow int // the row the cell sits in
drawGroup string // the draw group it draws in, as written; "" keeps its caller's drawGroup string // the draw group it draws in, as written; "" keeps its caller's
drawGroupKey string // its draw group keyed by its category once linked: what a render reads its reference paths under drawGroupKey string // its draw group keyed by its category once linked: what a render reads its reference paths under
} }
@@ -83,6 +88,70 @@ func compile(v any) (node, error) {
return compileAt(v, atTop) return compileAt(v, atTop)
} }
// compileCategory compiles a data file's value, which may be a table over a rows
// file beside it, and refuses a choice that is a table written as templates.
func compileCategory(v any, name string, files *categoryFiles) (node, error) {
if m, ok := v.(map[string]any); ok {
if _, isTable := m["rows"]; isTable {
return compileTable(m, name, files)
}
}
if items, ok := v.([]any); ok {
if err := checkNotRows(items, name); err != nil {
return nil, err
}
}
return compile(v)
}
// checkNotRows refuses a choice of templates sharing one format and one set of
// string fields: each item is a row, and the rows file is the spelling for that.
func checkNotRows(items []any, name string) error {
var format string
var fields []string
weighted := false
for i, raw := range items {
f, keys, w, isRow := rowShape(raw)
if !isRow || i > 0 && (f != format || !slices.Equal(keys, fields)) {
return nil
}
format, fields, weighted = f, keys, weighted || w
}
if len(items) < 2 || len(fields) == 0 {
return nil
}
weight := ""
if weighted {
weight = `, a weight column, and "weight" naming it`
}
return fmt.Errorf("a choice of %d templates with one format and the fields %v is a table; write the rows in %s.tsv with the header %q%s, and the category as {\"format\": %q, \"rows\": \"%s.tsv\"}",
len(items), fields, name, strings.Join(fields, "\t"), weight, format, name)
}
// rowShape reads a choice item as a row: a template whose every field is a string,
// with its format, its sorted field names, and whether it carries a weight.
func rowShape(raw any) (format string, fields []string, weighted, isRow bool) {
m, ok := raw.(map[string]any)
if !ok {
return "", nil, false, false
}
format, _ = m["format"].(string)
for k, v := range m {
if k == "weight" {
weighted = true
continue
}
if _, isString := v.(string); !isString || isOption(k) && k != "format" {
return "", nil, false, false
}
if k != "format" {
fields = append(fields, k)
}
}
sort.Strings(fields)
return format, fields, weighted, true
}
// compileAt compiles a node that is no choice's item. Only a choice's items carry a // compileAt compiles a node that is no choice's item. Only a choice's items carry a
// weight, so one here would be inert whatever its type. // weight, so one here would be inert whatever its type.
func compileAt(v any, pos position) (node, error) { func compileAt(v any, pos position) (node, error) {
@@ -140,7 +209,7 @@ func compileString(s string) (node, error) {
// applies the fences that need the compiled reads. // applies the fences that need the compiled reads.
func (t *template) compileFormat() error { func (t *template) compileFormat() error {
c := compileOps(t.format, t.refs) c := compileOps(t.format, t.refs)
t.ops, t.grow, t.bound, t.held = c.ops, c.grow, c.bound, c.held t.ops, t.grow, t.bound, t.held, t.heldLocal = c.ops, c.grow, c.bound, c.held, c.heldLocal
t.fixed = true t.fixed = true
for _, o := range t.ops { for _, o := range t.ops {
if o.kind != 'l' { if o.kind != 'l' {
@@ -231,6 +300,9 @@ func checkNoRepeatedItem(items []any) error {
} }
func compileTemplate(m map[string]any, pos position) (node, error) { func compileTemplate(m map[string]any, pos position) (node, error) {
if _, isTable := m["rows"]; isTable {
return nil, fmt.Errorf("rows names a TSV beside a category's file, so only a category is a table; an inline template has no file beside it")
}
o, err := readOptions(m, pos) o, err := readOptions(m, pos)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -407,7 +479,11 @@ func checkName(name string) error {
// checkPathNames rejects a dotted path with a segment no name may be. // checkPathNames rejects a dotted path with a segment no name may be.
func checkPathNames(path string) error { func checkPathNames(path string) error {
for _, seg := range strings.Split(path, ".") { segs, err := splitPath(path)
if err != nil {
return err
}
for _, seg := range names(segs) {
if err := checkName(seg); err != nil { if err := checkName(seg); err != nil {
return fmt.Errorf("path %w", err) return fmt.Errorf("path %w", err)
} }
+248 -26
View File
@@ -6,61 +6,280 @@ import (
"strings" "strings"
) )
// splitPath splits a dotted path into segments, a selector — [key or name] after a
// table's name — becoming a segment of its own, brackets kept. A dot inside a
// selector is part of the key or name.
func splitPath(path string) ([]string, error) {
segs := make([]string, 0, strings.Count(path, ".")+2*strings.Count(path, "[")+1)
start, open := 0, -1
for i := 0; i < len(path); i++ {
switch path[i] {
case '[':
if err := checkOpen(path, i, start, open); err != nil {
return nil, err
}
segs = append(segs, path[start:i])
start, open = i, i
case ']':
if err := checkClose(path, i, open); err != nil {
return nil, err
}
segs = append(segs, path[start:i+1])
start, open = i+2, -1
i++
case '.':
if open < 0 {
segs = append(segs, path[start:i])
start = i + 1
}
}
}
if open >= 0 {
return nil, fmt.Errorf(`%q opens a selector with "[" and never closes it with "]"`, path)
}
if start <= len(path) {
segs = append(segs, path[start:])
}
return segs, nil
}
// checkOpen refuses a "[" at i that opens no selector: one inside a selector, or one
// following no name.
func checkOpen(path string, i, start, open int) error {
switch {
case open >= 0:
return fmt.Errorf(`%q holds a "[" inside a selector, which no key or name may`, path)
case i == 0:
return fmt.Errorf(`%q starts with "["; a path starts with a name, and a selector follows a table's name`, path)
case i == start:
return fmt.Errorf(`%q: a selector follows its table's name; write %s`, path, path[:i-1]+path[i:])
}
return nil
}
// checkClose refuses a "]" at i that closes no selector, closes an empty one, or is
// followed by anything but a dot or the end.
func checkClose(path string, i, open int) error {
switch {
case open < 0:
return fmt.Errorf(`%q holds a "]" that closes no "["`, path)
case i == open+1:
return fmt.Errorf("%q holds an empty selector; a selector names a row by key or name", path)
case i+1 < len(path) && path[i+1] != '.':
return fmt.Errorf(`%q: a "]" ends its selector, so a dot or the end must follow it`, path)
}
return nil
}
// indexOutside is the first c in s outside a [selector], or -1.
func indexOutside(s string, c byte) int {
depth := 0
for i := 0; i < len(s); i++ {
switch {
case s[i] == '[':
depth++
case s[i] == ']' && depth > 0:
depth--
case s[i] == c && depth == 0:
return i
}
}
return -1
}
// splitOutside splits s on c outside any [selector].
func splitOutside(s string, c byte) []string {
var parts []string
for {
i := indexOutside(s, c)
if i < 0 {
return append(parts, s)
}
parts, s = append(parts, s[:i]), s[i+1:]
}
}
// isSelector reports whether a segment is a [key or name] rather than a name.
func isSelector(seg string) bool { return strings.HasPrefix(seg, "[") }
func hasSelector(segs []string) bool {
for _, s := range segs {
if isSelector(s) {
return true
}
}
return false
}
// selectorOf is the key or name a selector segment holds.
func selectorOf(seg string) string { return seg[1 : len(seg)-1] }
// names is the segments of a path that are names, its selectors left out.
func names(segs []string) []string {
out := segs[:0:0]
for _, s := range segs {
if !isSelector(s) {
out = append(out, s)
}
}
return out
}
// pathWalk is what one walk of a dotted path does at each kind of level: choice // pathWalk is what one walk of a dotted path does at each kind of level: choice
// returns the variants to continue into (none stops the walk); level runs at each // returns the variants to continue into (none stops the walk), and where it is nil
// template a segment descends into; leaf runs where the tail ends. A nil action // the walk draws one with the pins' session, or stops; level runs at each template
// is skipped. // a segment descends into; leaf runs where the tail ends; pins is where the walk
// pins the table rows it selects and draws, nil skipping tables. A nil action is
// skipped. A render's walk sets only pins, so it allocates nothing.
type pathWalk struct { type pathWalk struct {
choice func(c *choice, rest []string) ([]node, error) choice func(c *choice, rest []string) ([]node, error)
level func(t *template, rest []string) error level func(t *template, rest []string) error
leaf func(n node) error leaf func(n node) error
pins *draws
} }
// walkPath descends tail from n: a folder or template by its next segment, a // walkPath descends tail from n and returns the node it ends at: a folder or
// choice by w.choice, which consumes no segment. A missing segment is an error, // template by its next segment, a choice by w.choice, which consumes no segment, a
// so no walk reaches past what the data holds. A table-shaped dispatch, one case // table by walkTable. A missing segment is an error, so no walk reaches past what
// per node kind, kept whole on purpose. // the data holds.
func walkPath(n node, tail []string, w pathWalk) error { func walkPath(n node, tail []string, w pathWalk) (node, error) {
if len(tail) == 0 { if len(tail) == 0 {
if w.leaf != nil { return n, w.atLeaf(n)
return w.leaf(n)
} }
return nil if t, isTable := n.(*table); isTable {
return walkTable(t, tail, w, false)
}
if isSelector(tail[0]) {
return nil, fmt.Errorf("%s is not a table, so it has no row to select", selectorOf(tail[0]))
} }
switch n := n.(type) { switch n := n.(type) {
case *folder: case *folder:
child, ok := n.children[tail[0]] child, ok := n.children[tail[0]]
if !ok { if !ok {
return fmt.Errorf("no entry %q", tail[0]) return nil, fmt.Errorf("no entry %q", tail[0])
} }
return walkPath(child, tail[1:], w) return walkPath(child, tail[1:], w)
case *template: case *template:
if w.level != nil { if w.level != nil {
if err := w.level(n, tail); err != nil { if err := w.level(n, tail); err != nil {
return err return nil, err
} }
} }
child, ok := n.field(tail[0]) child, ok := n.field(tail[0])
if !ok { if !ok {
return fmt.Errorf("no field %q", tail[0]) return nil, fmt.Errorf("no field %q", tail[0])
} }
return walkPath(child, tail[1:], w) return walkPath(child, tail[1:], w)
case *choice: case *choice:
return walkChoice(n, tail, w)
case *column:
return nil, fmt.Errorf("no field %q: %q is a column, and a cell holds no fields", tail[0], n.t.columns[n.i])
}
return nil, fmt.Errorf("no field %q", tail[0])
}
func (w pathWalk) atLeaf(n node) error {
if w.leaf != nil {
return w.leaf(n)
}
return nil
}
// walkChoice continues a walk into the variants w.choice returns, or into one drawn
// by the pins' session where the walk names no choice action.
func walkChoice(c *choice, tail []string, w pathWalk) (node, error) {
if w.choice == nil { if w.choice == nil {
return nil if w.pins == nil {
return nil, nil
} }
next, err := w.choice(n, tail) if err := carriedByAll(c, tail); err != nil {
return nil, err
}
if w.pins.s == nil { // a probe: every variant carries the tail, so any one proves it
return walkPath(c.items[0], tail, w)
}
return walkPath(pick(w.pins.s, c), tail, w)
}
next, err := w.choice(c, tail)
if err != nil { if err != nil {
return err return nil, err
} }
var last node
for _, item := range next { for _, item := range next {
if err := walkPath(item, tail, w); err != nil { if last, err = walkPath(item, tail, w); err != nil {
return err return nil, err
} }
} }
return last, nil
}
// walkTable descends tail from a table: a selector first, then a column or a
// linked table by name. The walk reads a row wherever a segment follows or the
// table was reached from another; a table reached whole, with no selector, is left
// to a render's own draw.
func walkTable(t *table, tail []string, w pathWalk, descended bool) (node, error) {
sel, tail, err := t.selector(tail)
if err != nil {
return nil, err
}
column, child, err := t.step(tail)
if err != nil {
return nil, err
}
// A selector further down pins this table by ancestry, so the walk draws only
// where none follows; drawing first could pick a row the selector is not inside.
if w.pins != nil {
if err := readRow(w.pins, t, sel, (descended || len(tail) > 0) && !hasSelector(tail)); err != nil {
return nil, err
}
}
switch {
case len(tail) == 0 && sel == "" && !descended:
return walkPath(t, nil, w)
case len(tail) == 0:
return walkPath(t.whole, nil, w)
case child != nil:
return walkTable(child, tail[1:], w, true)
}
return walkPath(column, tail[1:], w)
}
// selector splits the selector a tail starts with from the rest of it.
func (t *table) selector(tail []string) (sel string, rest []string, err error) {
if len(tail) == 0 || !isSelector(tail[0]) {
return "", tail, nil
}
sel, rest = selectorOf(tail[0]), tail[1:]
if len(rest) > 0 && isSelector(rest[0]) {
return "", nil, fmt.Errorf("%s[%s] is selected twice; one selector names its row", t.category, sel)
}
return sel, rest, nil
}
// step is what a tail's first segment names in t: a column, or a table linked to it.
func (t *table) step(tail []string) (column node, child *table, err error) {
if len(tail) == 0 {
return nil, nil, nil
}
if i, ok := t.col[tail[0]]; ok {
return t.fields[t.columns[i]], nil, nil
}
if child = t.descendant(tail[0]); child == nil {
return nil, nil, fmt.Errorf("no column or linked table %q in %s", tail[0], t.category)
}
return nil, child, nil
}
// readRow pins the row a path reads of t: the one its selector names, or, where the
// walk draws, one drawn where the path reads into the table.
func readRow(d *draws, t *table, sel string, draw bool) error {
if sel != "" {
return d.selectRow(t, sel)
}
if draw && d.s != nil {
d.rowOf(t)
}
return nil return nil
}
return fmt.Errorf("no field %q", tail[0])
} }
// carriedByAll is the choice rule a path that must resolve on every call obeys: // carriedByAll is the choice rule a path that must resolve on every call obeys:
@@ -88,12 +307,13 @@ func unreachableInChoice(c *choice, want string) error {
} }
// checkPath proves a dotted tail resolves whichever way the draws go — a choice // checkPath proves a dotted tail resolves whichever way the draws go — a choice
// must carry the rest of the path in the set every variant shares — and that no // must carry the rest of the path in the set every variant shares, a selector must
// level a path reads carries a repeat or a drawGroup, which one draw of it could not // name one row inside the rows selected before it — and that no level a path reads
// apply. So a path that validates here resolves on every render, and a typo is a // carries a repeat or a drawGroup, which one draw of it could not apply. So a path
// New-time error. // that validates here resolves on every render, and a typo is a New-time error.
func checkPath(n node, tail []string, level string) error { func checkPath(n node, tail []string, level string) error {
return walkPath(n, tail, pathWalk{ var pins draws
_, err := walkPath(n, tail, pathWalk{
choice: func(c *choice, rest []string) ([]node, error) { choice: func(c *choice, rest []string) ([]node, error) {
if err := carriedByAll(c, rest); err != nil { if err := carriedByAll(c, rest); err != nil {
return nil, err return nil, err
@@ -110,5 +330,7 @@ func checkPath(n node, tail []string, level string) error {
} }
return nil return nil
}, },
pins: &pins,
}) })
return err
} }
+4 -4
View File
@@ -13,14 +13,14 @@ func TestWalkPathStopsAtAMissingSegment(t *testing.T) {
level: func(tm *template, rest []string) error { seen = append(seen, "level:"+rest[0]); return nil }, level: func(tm *template, rest []string) error { seen = append(seen, "level:"+rest[0]); return nil },
leaf: func(n node) error { seen = append(seen, "leaf"); return nil }, leaf: func(n node) error { seen = append(seen, "leaf"); return nil },
} }
if err := walkPath(n, []string{"a", "b"}, walk); err != nil { if _, err := walkPath(n, []string{"a", "b"}, walk); err != nil {
t.Fatalf("walkPath(a.b) = %v", err) t.Fatalf("walkPath(a.b) = %v", err)
} }
if want := []string{"level:a", "level:b", "leaf"}; !slices.Equal(seen, want) { if want := []string{"level:a", "level:b", "leaf"}; !slices.Equal(seen, want) {
t.Errorf("walk visited %v, want %v", seen, want) t.Errorf("walk visited %v, want %v", seen, want)
} }
seen = nil seen = nil
err := walkPath(n, []string{"a", "nope", "deeper"}, walk) _, err := walkPath(n, []string{"a", "nope", "deeper"}, walk)
if err == nil || !strings.Contains(err.Error(), `no field "nope"`) { if err == nil || !strings.Contains(err.Error(), `no field "nope"`) {
t.Errorf("walkPath(a.nope.deeper) = %v, want the missing segment named", err) t.Errorf("walkPath(a.nope.deeper) = %v, want the missing segment named", err)
} }
@@ -32,7 +32,7 @@ func TestWalkPathStopsAtAMissingSegment(t *testing.T) {
func TestWalkPathChoiceConsumesNoSegment(t *testing.T) { func TestWalkPathChoiceConsumesNoSegment(t *testing.T) {
n := compiled(t, `[{"format":"{f}","f":"1"},{"format":"{f}","f":"2"}]`) n := compiled(t, `[{"format":"{f}","f":"1"},{"format":"{f}","f":"2"}]`)
var leaves []node var leaves []node
err := walkPath(n, []string{"f"}, pathWalk{ _, err := walkPath(n, []string{"f"}, pathWalk{
choice: func(c *choice, rest []string) ([]node, error) { choice: func(c *choice, rest []string) ([]node, error) {
if len(rest) != 1 || rest[0] != "f" { if len(rest) != 1 || rest[0] != "f" {
t.Errorf("choice saw rest %v, want [f]", rest) t.Errorf("choice saw rest %v, want [f]", rest)
@@ -74,7 +74,7 @@ func TestDescendIntoStringErrors(t *testing.T) {
// one call and failing on the next: every variant must carry the rest of the path. // one call and failing on the next: every variant must carry the rest of the path.
func TestPathThroughChoice(t *testing.T) { func TestPathThroughChoice(t *testing.T) {
dir := writeData(t, map[string]string{ dir := writeData(t, map[string]string{
"every": `[{"format":"{f}","f":"1"},{"format":"{f}","f":"2"}]`, "every": `[{"format":"{f}","f":"1"},{"format":"{f}","f":"2","g":"x"}]`,
"notall": `[{"format":"{f}","f":"1"},"plain"]`, "notall": `[{"format":"{f}","f":"1"},"plain"]`,
"some": `[{"format":"{f}","f":"1"},{"format":"{f}","f":"2","extra":"x"}]`, "some": `[{"format":"{f}","f":"1"},{"format":"{f}","f":"2","extra":"x"}]`,
}) })
+43
View File
@@ -74,6 +74,49 @@ func TestNoReferenceAllocRegression(t *testing.T) {
} }
} }
// A table read pins a row in the render's draws and reads its cells in place.
func TestNoTableAllocRegression(t *testing.T) {
f, err := New(WithoutShippedData(), WithDataFS(fstest.MapFS{
"region.json": {Data: []byte(`{"format":"{name}","rows":"region.tsv","key":"code","weight":"population"}`)},
"region.tsv": {Data: []byte("code\tname\tpopulation\n01\tStockholms län\t2400000\n12\tSkåne län\t1400000\n14\tVästra Götalands län\t1750000\n")},
"x.json": {Data: []byte(`"{/region.name}, {/region.code}"`)},
}))
if err != nil {
t.Fatal(err)
}
for _, s := range []struct {
name, path string
base float64
}{
{"a table drawn", "region", 2},
{"a row selected", "region[12]", 2},
{"two columns of one draw", "x", 10},
} {
if allocs := testing.AllocsPerRun(10000, func() { f.Fake(s.path) }); allocs > s.base*1.10 {
t.Errorf("%s: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); a row index built per draw is the usual cause", s.name, allocs, s.base*1.10, s.base)
}
}
// Five linked tables, the depth a country's geo tree has: every pin must stay inline.
deep := fstest.MapFS{"addr.json": {Data: []byte(`"{/e.v} {/d.v} {/c.v} {/b.v} {/a.v}"`)}}
for i, name := range []string{"a", "b", "c", "d", "e"} {
rows, category := "k\tv\n1\tx\n2\ty\n", `{"format":"{v}","rows":"`+name+`.tsv","key":"k"}`
if i > 0 {
parent := string(rune('a' + i - 1))
rows = "k\tv\t" + parent + "\n1\tx\t1\n2\ty\t2\n"
category = `{"format":"{v}","rows":"` + name + `.tsv","key":"k","parent":"` + parent + `"}`
}
deep[name+".json"], deep[name+".tsv"] = &fstest.MapFile{Data: []byte(category)}, &fstest.MapFile{Data: []byte(rows)}
}
f, err = New(WithoutShippedData(), WithDataFS(deep))
if err != nil {
t.Fatal(err)
}
const base = 13.0
if allocs := testing.AllocsPerRun(10000, func() { f.Fake("addr") }); allocs > base*1.10 {
t.Errorf("five linked tables: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); a pin spilling past the inline set is the usual cause", allocs, base*1.10, base)
}
}
// A record's fences read the compiled tree, so they belong to New, not to a draw. // A record's fences read the compiled tree, so they belong to New, not to a draw.
func TestNoRecordAllocRegression(t *testing.T) { func TestNoRecordAllocRegression(t *testing.T) {
for _, s := range []struct{ name, json string }{ for _, s := range []struct{ name, json string }{
+67 -4
View File
@@ -8,7 +8,43 @@ import (
"testing" "testing"
) )
var jsonBlock = regexp.MustCompile("(?s)```json\n(.*?)```") var (
jsonBlock = regexp.MustCompile("(?s)```json\n(.*?)```")
tsvBlock = regexp.MustCompile("(?s)```tsv\n(.*?)```")
rowsFile = regexp.MustCompile(`"rows":\s*"([^"]+)"`)
parentName = regexp.MustCompile(`"parent":\s*"([^"]+)"`)
)
// exampleFiles is a README json block as a data directory's files: the category, the
// rows TSV it names, taken from the nearest tsv block above it, and the parent table it
// names, taken from the nearest json block above it whose rows file is the parent's.
func exampleFiles(t *testing.T, src string, at int, body string) map[string]string {
t.Helper()
files := map[string]string{"example.json": body}
if m := rowsFile.FindStringSubmatch(body); m != nil {
tsv := tsvBlock.FindAllStringSubmatch(src[:at], -1)
if tsv == nil {
t.Fatalf("README example names %s with no tsv block above it", m[1])
}
files[m[1]] = tsv[len(tsv)-1][1]
}
if m := parentName.FindStringSubmatch(body); m != nil {
blocks := jsonBlock.FindAllStringSubmatchIndex(src[:at], -1)
for i := len(blocks) - 1; i >= 0; i-- {
parent := src[blocks[i][2]:blocks[i][3]]
if strings.Contains(parent, `"rows": "`+m[1]+`.tsv"`) {
for name, content := range exampleFiles(t, src, blocks[i][0], parent) {
if name == "example.json" {
name = m[1] + ".json"
}
files[name] = content
}
break
}
}
}
return files
}
func readme(t *testing.T) string { func readme(t *testing.T) string {
t.Helper() t.Helper()
@@ -21,12 +57,13 @@ func readme(t *testing.T) string {
func TestReadmeExamplesLoadAndRender(t *testing.T) { func TestReadmeExamplesLoadAndRender(t *testing.T) {
n := 0 n := 0
for _, m := range jsonBlock.FindAllStringSubmatch(readme(t), -1) { src := readme(t)
body := m[1] for _, at := range jsonBlock.FindAllStringSubmatchIndex(src, -1) {
body := src[at[2]:at[3]]
if strings.Contains(body, "…") { if strings.Contains(body, "…") {
continue continue
} }
f, err := New(WithDataPath(writeData(t, map[string]string{"example": body})), WithSeed(1)) f, err := New(WithDataPath(writeFiles(t, exampleFiles(t, src, at[0], body))), WithSeed(1))
if err != nil { if err != nil {
t.Errorf("README example does not load: %v\n%s", err, body) t.Errorf("README example does not load: %v\n%s", err, body)
continue continue
@@ -128,3 +165,29 @@ func TestReadmeDatatypeExample(t *testing.T) {
t.Errorf("README Datatype example shows %v, want an integer, a number and a boolean column", shown) t.Errorf("README Datatype example shows %v, want an integer, a number and a boolean column", shown)
} }
} }
func TestReadmeTableExample(t *testing.T) {
src := readme(t)
i := strings.Index(src, "### Table")
if i < 0 {
t.Fatal("README lost the Table section")
}
at := jsonBlock.FindStringSubmatchIndex(src[i:])
if at == nil {
t.Fatal("README Table section has no json block")
}
body := src[i:][at[2]:at[3]]
f, err := New(WithDataPath(writeFiles(t, exampleFiles(t, src, i+at[0], body))), WithSeed(1))
if err != nil {
t.Fatal(err)
}
for path, want := range map[string]string{"example[SE]": "Sweden (SE)", "example[Norway].alpha2": "NO"} {
if got := fake(t, f, path); got != want {
t.Errorf("README table example: Fake(%q) = %q, want %q", path, got, want)
}
}
r, err := f.FakeRecord("example")
if err != nil || r.CSVHeader() != "alpha2,name,population" {
t.Fatalf("README table example as a record: %q, %v", r.CSVHeader(), err)
}
}
+44 -8
View File
@@ -122,18 +122,47 @@ func literal(c Column, quote func(string) string, nullText string) string {
func (f *Generator) FakeRecord(path string) (*Record, error) { func (f *Generator) FakeRecord(path string) (*Record, error) {
f.mu.Lock() f.mu.Lock()
defer f.mu.Unlock() defer f.mu.Unlock()
_, n, tail, err := resolveCategory(f.categories, strings.Split(path, ".")) segments, err := splitPath(path)
if err != nil {
return nil, fmt.Errorf("fejkdata: %w", err)
}
_, n, tail, err := resolveCategory(f.categories, segments)
if err != nil { if err != nil {
return nil, fmt.Errorf("fejkdata: %s: %w", path, err) return nil, fmt.Errorf("fejkdata: %s: %w", path, err)
} }
if len(tail) > 0 { set := newDrawSet(f.rand)
sc := drawScope{set: &set}
if t, isTable := n.(*table); isTable {
if n, err = tableRecord(f.rand, t, tail, sc); err != nil {
return nil, fmt.Errorf("fejkdata: %s: %w", path, err)
}
} else if len(tail) > 0 {
return nil, fmt.Errorf("fejkdata: %s descends into %q, a field; only a category-level template is a record", path, tail[0]) return nil, fmt.Errorf("fejkdata: %s descends into %q, a field; only a category-level template is a record", path, tail[0])
} }
shape := f.recordShapeOf(n) shape := f.recordShapeOf(n)
if shape.err != nil { if shape.err != nil {
return nil, fmt.Errorf("fejkdata: %s %w", path, shape.err) return nil, fmt.Errorf("fejkdata: %s %w", path, shape.err)
} }
return renderRecord(f.rand, shape.t, shape.columns), nil return renderRecord(f.rand, shape.t, shape.columns, sc), nil
}
// tableRecord walks a path's tail from a table to the table whose row is the record,
// pinning the rows it selects or draws.
func tableRecord(s *session, t *table, tail []string, sc drawScope) (node, error) {
n, err := descend(s, t, tail, sc)
if err != nil {
return nil, err
}
switch n := n.(type) {
case *table:
sc.draws(s).rowOf(n)
return n, nil
case *row:
return n.t, nil
case *column:
return nil, fmt.Errorf("descends into %q, a column; a record is a table's row", n.t.columns[n.i])
}
return n, nil
} }
// recordShape is what recordOf settled about a node: the template to project, its // recordShape is what recordOf settled about a node: the template to project, its
@@ -171,7 +200,8 @@ type RecordTemplate struct {
func (t *RecordTemplate) Fake() *Record { func (t *RecordTemplate) Fake() *Record {
t.g.mu.Lock() t.g.mu.Lock()
defer t.g.mu.Unlock() defer t.g.mu.Unlock()
return renderRecord(t.g.rand, t.t, t.columns) set := newDrawSet(t.g.rand)
return renderRecord(t.g.rand, t.t, t.columns, drawScope{set: &set})
} }
// NewRecordTemplate compiles an inline record — a JSON object with a format and // NewRecordTemplate compiles an inline record — a JSON object with a format and
@@ -200,6 +230,9 @@ func (f *Generator) FakeRecordTemplate(input string) (*Record, error) {
// recordOf is the fence both record entry points pass. The columns come back with // recordOf is the fence both record entry points pass. The columns come back with
// the template, fixed for every draw the caller goes on to make. // the template, fixed for every draw the caller goes on to make.
func recordOf(n node) (*template, []Column, error) { func recordOf(n node) (*template, []Column, error) {
if tb, isTable := n.(*table); isTable {
n = tb.format
}
t, ok := n.(*template) t, ok := n.(*template)
if !ok { if !ok {
return nil, nil, errors.New("names a choice, not a template; a record is a template whose fields are its columns") return nil, nil, errors.New("names a choice, not a template; a record is a template whose fields are its columns")
@@ -219,10 +252,13 @@ func recordOf(n node) (*template, []Column, error) {
return t, columns, nil return t, columns, nil
} }
// renderRecord draws each column once, in the name order recordOf fixed, as one render. // renderRecord draws each column once, in the name order recordOf fixed, as one render
func renderRecord(s *session, t *template, columns []Column) *Record { // over sc's draws; a table's columns read the row pinned there.
set := newDrawSet() func renderRecord(s *session, t *template, columns []Column, sc drawScope) *Record {
sc := drawScope{set: &set}.in(t) sc = sc.in(t)
if t.table != nil {
sc.t, sc.row = t.table, sc.draws(s).mustRow(t.table)
}
r := &Record{columns: append([]Column(nil), columns...)} r := &Record{columns: append([]Column(nil), columns...)}
for i := range r.columns { for i := range r.columns {
column := renderLeaf(s, t.fields[r.columns[i].Name], sc) column := renderLeaf(s, t.fields[r.columns[i].Name], sc)
+3 -3
View File
@@ -257,7 +257,7 @@ func TestRecordRejectsAColumnReadingItsOwnRecord(t *testing.T) {
func TestRecordBareReferenceStaysIndependentAsAnOperand(t *testing.T) { func TestRecordBareReferenceStaysIndependentAsAnOperand(t *testing.T) {
dir := writeData(t, map[string]string{ dir := writeData(t, map[string]string{
"cur": `[{"format":"{code}","code":"aud"},{"format":"{code}","code":"eur"}]`, "cur": `[{"format":"{code}","code":"aud"},{"format":"{code}","code":"eur","symbol":"€"}]`,
"row": `{"format":"","up":"{uppercase(/cur)}","low":"{lowercase(/cur)}"}`, "row": `{"format":"","up":"{uppercase(/cur)}","low":"{lowercase(/cur)}"}`,
}) })
f := newGenerator(t, dir, WithSeed(1)) f := newGenerator(t, dir, WithSeed(1))
@@ -305,7 +305,7 @@ func TestRecordSQLInsert(t *testing.T) {
func TestRecordRejectsFieldDescent(t *testing.T) { func TestRecordRejectsFieldDescent(t *testing.T) {
dir := writeData(t, map[string]string{ dir := writeData(t, map[string]string{
"cat": `{"format":"{sub}","sub":{"format":"{x}","x":"1"}}`, "cat": `{"format":"{sub}","sub":{"format":"{x}","x":"1"}}`,
"row": `[{"format":"{x}","x":"1"},{"format":"{x}","x":"2"}]`, "row": `[{"format":"{x}","x":"1"},{"format":"{x}","x":"2","y":"b"}]`,
}) })
f := newGenerator(t, dir, WithSeed(1)) f := newGenerator(t, dir, WithSeed(1))
for _, path := range []string{"cat.sub", "row.x"} { for _, path := range []string{"cat.sub", "row.x"} {
@@ -317,7 +317,7 @@ func TestRecordRejectsFieldDescent(t *testing.T) {
func TestRecordSharesAReferenceAcrossColumns(t *testing.T) { func TestRecordSharesAReferenceAcrossColumns(t *testing.T) {
dir := writeData(t, map[string]string{ dir := writeData(t, map[string]string{
"currency": `[{"format":"{code}","code":"AUD","symbol":"$"},{"format":"{code}","code":"EUR","symbol":"€"}]`, "currency": `[{"format":"{code}","code":"AUD","symbol":"$"},{"format":"{code}","code":"EUR","symbol":"€","name":"Euro"}]`,
"price": `{"format":"{code} {symbol}","code":"{/currency.code}","symbol":"{/currency.symbol}"}`, "price": `{"format":"{code} {symbol}","code":"{/currency.code}","symbol":"{/currency.symbol}"}`,
}) })
f := newGenerator(t, dir, WithSeed(1)) f := newGenerator(t, dir, WithSeed(1))
+10 -2
View File
@@ -35,7 +35,11 @@ func refShape(name string) (sigil, rest string, err error) {
if strings.HasPrefix(rest, ".") { if strings.HasPrefix(rest, ".") {
return "", "", fmt.Errorf("a reference starts with / (the root), . (this folder) or .. (the folder above)") return "", "", fmt.Errorf("a reference starts with / (the root), . (this folder) or .. (the folder above)")
} }
for _, seg := range strings.Split(rest, ".") { segs, err := splitPath(rest)
if err != nil {
return "", "", err
}
for _, seg := range segs {
if seg == "" { if seg == "" {
return "", "", fmt.Errorf("path has an empty segment") return "", "", fmt.Errorf("path has an empty segment")
} }
@@ -59,7 +63,8 @@ func refSegments(name string, folder []string) ([]string, error) {
} }
base = folder[:len(folder)-1] base = folder[:len(folder)-1]
} }
return append(append([]string{}, base...), strings.Split(rest, ".")...), nil segs, _ := splitPath(rest) // refShape proved it splits
return append(append([]string{}, base...), segs...), nil
} }
// linkRefs resolves every reference in the assembled tree. The head of the path — // linkRefs resolves every reference in the assembled tree. The head of the path —
@@ -199,6 +204,9 @@ func resolveCategory(root map[string]node, segments []string) (head []string, ta
if !ok { if !ok {
break break
} }
if isSelector(segments[i]) {
return nil, nil, nil, fmt.Errorf("%s is a folder, not a table, so it has no row to select", strings.Join(segments[:i], "."))
}
child, ok := g.children[segments[i]] child, ok := g.children[segments[i]]
if !ok { if !ok {
return nil, nil, nil, fmt.Errorf("no entry %q", segments[i]) return nil, nil, nil, fmt.Errorf("no entry %q", segments[i])
+4 -4
View File
@@ -220,7 +220,7 @@ func TestNewErrorPathIsCanonical(t *testing.T) {
func TestReferencePathIsHeld(t *testing.T) { func TestReferencePathIsHeld(t *testing.T) {
dir := writeData(t, map[string]string{ dir := writeData(t, map[string]string{
"person": `[{"format":"{first} {last}","first":"Anna","last":"Andersson"},{"format":"{first} {last}","first":"Bo","last":"Berg"}]`, "person": `[{"format":"{first} {last}","first":"Anna","last":"Andersson"},{"format":"{first} {last}","first":"Bo","last":"Berg","born":"1980"}]`,
"card": `"{/person.first} {/person.last}"`, "card": `"{/person.first} {/person.last}"`,
}) })
f := newGenerator(t, dir, WithSeed(3)) f := newGenerator(t, dir, WithSeed(3))
@@ -291,7 +291,7 @@ func TestReferenceOverlapIsRejected(t *testing.T) {
} }
} }
const drawPeople = `[{"format":"{first} {last}","first":"Ada","last":"Lovelace"},{"format":"{first} {last}","first":"Bo","last":"Ek"},{"format":"{first} {last}","first":"Cy","last":"Young"}]` const drawPeople = `[{"format":"{first} {last}","first":"Ada","last":"Lovelace"},{"format":"{first} {last}","first":"Bo","last":"Ek"},{"format":"{first} {last}","first":"Cy","last":"Young","born":"1867"}]`
// onePerson reports whether name is the first name and surname of one drawPeople row. // onePerson reports whether name is the first name and surname of one drawPeople row.
func onePerson(name string) bool { func onePerson(name string) bool {
@@ -381,7 +381,7 @@ func TestRelativeReferences(t *testing.T) {
func TestRelativeAndRootSpellingsBindOneDraw(t *testing.T) { func TestRelativeAndRootSpellingsBindOneDraw(t *testing.T) {
dir := writeData(t, map[string]string{ dir := writeData(t, map[string]string{
"sv_SE/person": `[{"format":"{first} {last}","first":"Anna","last":"Andersson"},{"format":"{first} {last}","first":"Bo","last":"Berg"}]`, "sv_SE/person": `[{"format":"{first} {last}","first":"Anna","last":"Andersson"},{"format":"{first} {last}","first":"Bo","last":"Berg","born":"1980"}]`,
"sv_SE/card": `"{.person.first} {/sv_SE.person.last}"`, "sv_SE/card": `"{.person.first} {/sv_SE.person.last}"`,
}) })
f := newGenerator(t, dir, WithSeed(3)) f := newGenerator(t, dir, WithSeed(3))
@@ -408,7 +408,7 @@ func TestReferenceSigilErrors(t *testing.T) {
} }
func TestSpellingsOfOneReferenceAreOneLevel(t *testing.T) { func TestSpellingsOfOneReferenceAreOneLevel(t *testing.T) {
person := `[{"format":"{first} {last}","first":"Ada","last":"Byron"},{"format":"{first} {last}","first":"Bo","last":"Ek"}]` person := `[{"format":"{first} {last}","first":"Ada","last":"Byron"},{"format":"{first} {last}","first":"Bo","last":"Ek","born":"1990"}]`
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{ _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{
"sv_SE/person": person, "sv_SE/person": person,
"sv_SE/mail": `"{.person} <{/sv_SE.person.first}>"`, "sv_SE/mail": `"{.person} <{/sv_SE.person.first}>"`,
+41 -25
View File
@@ -2,7 +2,6 @@ package fejkdata
import ( import (
"fmt" "fmt"
"sort"
"strings" "strings"
) )
@@ -20,33 +19,36 @@ type rng interface {
func (f *Generator) Fake(path string) (string, error) { func (f *Generator) Fake(path string) (string, error) {
f.mu.Lock() f.mu.Lock()
defer f.mu.Unlock() defer f.mu.Unlock()
n, err := descend(f.rand, &folder{children: f.categories}, strings.Split(path, ".")) segments, err := splitPath(path)
if err != nil {
return "", fmt.Errorf("fejkdata: %w", err)
}
f.set = drawSet{unnamed: draws{s: f.rand}}
sc := drawScope{set: &f.set}
f.root.children = f.categories
n, err := descend(f.rand, &f.root, segments, sc)
if err != nil { if err != nil {
return "", fmt.Errorf("fejkdata: %s: %w", path, err) return "", fmt.Errorf("fejkdata: %s: %w", path, err)
} }
if _, ok := n.(*folder); ok { if _, ok := n.(*folder); ok {
return "", fmt.Errorf("fejkdata: %s names a folder, not a value", path) return "", fmt.Errorf("fejkdata: %s names a folder, not a value", path)
} }
return renderOnce(f.rand, n), nil return render(f.rand, n, sc), nil
} }
// descend walks named fields to the node a path names. It is the one render-side // descend walks named fields to the node a path names, pinning the table rows it
// step that can fail, because the path comes from the caller and may name a field // selects or draws in sc. It is the one render-side step that can fail, because the
// that does not exist. A choice consumes no segment, so the rest of the path must // path comes from the caller and may name a field or a row that does not exist. A
// be one every variant carries before a variant is picked — a path that resolves // choice consumes no segment, so the rest of the path must be one every variant
// at all resolves on every call. // carries before a variant is picked — a path that resolves at all resolves on
func descend(s *session, root node, segments []string) (node, error) { // every call.
var found node func descend(s *session, root node, segments []string, sc drawScope) (node, error) {
err := walkPath(root, segments, pathWalk{ // Walked once without drawing first, so a path that fails moves no seeded stream.
choice: func(c *choice, rest []string) ([]node, error) { var probe draws
if err := carriedByAll(c, rest); err != nil { if _, err := walkPath(root, segments, pathWalk{pins: &probe}); err != nil {
return nil, err return nil, err
} }
return []node{pick(s, c)}, nil return walkPath(root, segments, pathWalk{pins: sc.draws(s)})
},
leaf: func(n node) error { found = n; return nil },
})
return found, err
} }
// render evaluates a compiled node to a string. compile validates every node up // render evaluates a compiled node to a string. compile validates every node up
@@ -58,6 +60,20 @@ func render(s *session, n node, sc drawScope) string {
return render(s, pick(s, n), sc) return render(s, pick(s, n), sc)
case *null: case *null:
return "" return ""
case *table:
sc.t, sc.row = n, n.draw(s)
return expand(s, n.format, sc)
case *row:
sc.t, sc.row = n.t, sc.draws(s).mustRow(n.t)
return expand(s, n.t.format, sc)
case *column:
if sc.t != n.t {
sc.t, sc.row = n.t, sc.draws(s).mustRow(n.t)
}
if cell := n.t.cellNode(sc.row, n.i); cell != nil {
return render(s, cell, sc)
}
return n.t.cell(sc.row, n.i)
case *template: case *template:
sc = sc.in(n) sc = sc.in(n)
if n.repeat == 1 { if n.repeat == 1 {
@@ -85,20 +101,20 @@ func render(s *session, n node, sc drawScope) string {
// //
//go:noinline //go:noinline
func expandAnew(s *session, t *template) string { func expandAnew(s *session, t *template) string {
var set drawSet set := drawSet{unnamed: draws{s: s}}
return expand(s, t, drawScope{set: &set}) return expand(s, t, drawScope{set: &set})
} }
// pick selects one item. Uniform choices are O(1); weighted choices are an // pick selects one item. Uniform choices are O(1); weighted choices are an
// O(log n) search over precomputed cumulative weights. compile guarantees a // O(log n) search over precomputed cumulative weights. compile guarantees a
// non-empty choice and a finite positive total, so the index is always in range. // non-empty choice and a finite positive total, so the index is always in range.
func pick(r rng, c *choice) node { // The session is concrete rather than the rng interface, which would make the
// walk that draws through it leak its draw set to the heap.
func pick(s *session, c *choice) node {
if c.cum == nil { if c.cum == nil {
return c.items[r.IntN(len(c.items))] return c.items[s.IntN(len(c.items))]
} }
x := r.Float64() * c.cum[len(c.cum)-1] return c.items[pickCum(s, c.cum)]
i := sort.Search(len(c.cum), func(i int) bool { return c.cum[i] > x })
return c.items[i]
} }
// expand renders a template's compiled ops. compile validated every token, so this // expand renders a template's compiled ops. compile validated every token, so this
@@ -110,7 +126,7 @@ func expand(s *session, t *template, sc drawScope) string {
// repeat iteration get their own, since each is its own expansion. A reference // repeat iteration get their own, since each is its own expansion. A reference
// path reads the render's draws in sc instead. // path reads the render's draws in sc instead.
var held *draws var held *draws
if len(t.held) > 0 { if t.heldLocal {
held = &draws{ held = &draws{
variant: make(map[string]node, len(t.held)), variant: make(map[string]node, len(t.held)),
value: make(map[string]draw, len(t.held)), value: make(map[string]draw, len(t.held)),
+38
View File
@@ -43,6 +43,11 @@ func shippedShape(f *Generator) string {
} }
case *choice: case *choice:
facts[prefix] = reads(n) facts[prefix] = reads(n)
case *table:
facts[prefix] = "\tformat " + strconv.Quote(n.format.format) + tableFacts(n) + reads(n)
for _, name := range n.columns {
facts[join(prefix, name)] = "\tstring"
}
case *template: case *template:
facts[prefix] = "\tformat " + strconv.Quote(n.format) + reads(n) facts[prefix] = "\tformat " + strconv.Quote(n.format) + reads(n)
if _, columns, err := recordOf(n); err == nil { if _, columns, err := recordOf(n); err == nil {
@@ -66,6 +71,20 @@ func shippedShape(f *Generator) string {
return b.String() return b.String()
} }
// tableFacts names the columns a table's options read.
func tableFacts(t *table) string {
var b strings.Builder
for _, o := range []struct {
name string
col int
}{{"key", t.key}, {"name", t.name}, {"weight", t.weight}, {"parent", t.parent}} {
if o.col >= 0 {
b.WriteString("\t" + o.name + " " + t.columns[o.col])
}
}
return b.String()
}
// reads names the categories any template under n references, sorted. // reads names the categories any template under n references, sorted.
func reads(n node) string { func reads(n node) string {
set := map[string]bool{} set := map[string]bool{}
@@ -76,6 +95,11 @@ func reads(n node) string {
for _, it := range n.items { for _, it := range n.items {
collect(it) collect(it)
} }
case *table:
collect(n.format)
for _, cell := range n.tokens {
collect(cell)
}
case *template: case *template:
for _, b := range n.refs { for _, b := range n.refs {
set[strings.TrimPrefix(b.key, "/")] = true set[strings.TrimPrefix(b.key, "/")] = true
@@ -112,3 +136,17 @@ func TestShippedShapeNamesReads(t *testing.T) {
t.Fatalf("shippedShape =\n%s\nwant\n%s", got, want) t.Fatalf("shippedShape =\n%s\nwant\n%s", got, want)
} }
} }
func TestShippedShapeNamesTables(t *testing.T) {
f := newGenerator(t, writeFiles(t, map[string]string{
"w.json": `"x"`,
"r.json": `{"format":"{name}","rows":"r.tsv","key":"code","name":"name","weight":"w"}`,
"r.tsv": "code\tname\tw\n1\ta\t2\n2\tb\t3\n",
"m.json": `{"format":"{name} {/w}","rows":"m.tsv","key":"code","parent":"r"}`,
"m.tsv": "code\tname\tr\n10\tc\t1\n20\td\t2\n",
}))
want := "m\tformat \"{name} {/w}\"\tkey code\tparent r\treads w\nm.code\tstring\nm.name\tstring\nm.r\tstring\nr\tformat \"{name}\"\tkey code\tname name\tweight w\nr.code\tstring\nr.m\nr.m.code\nr.m.name\nr.m.r\nr.name\tstring\nr.w\tstring\nw\tformat \"x\"\n"
if got := shippedShape(f); got != want {
t.Fatalf("shippedShape =\n%s\nwant\n%s", got, want)
}
}
+2 -1
View File
@@ -349,7 +349,8 @@ func (p *valueProof) checkField(label string, ft reflect.Type, column node) erro
// fill draws the record into v's tagged fields, then each nested struct as a record of its own. // fill draws the record into v's tagged fields, then each nested struct as a record of its own.
func (s *structShape) fill(sess *session, v reflect.Value) { func (s *structShape) fill(sess *session, v reflect.Value) {
if s.record != nil { if s.record != nil {
for i, c := range renderRecord(sess, s.record, s.columns).columns { set := newDrawSet(sess)
for i, c := range renderRecord(sess, s.record, s.columns, drawScope{set: &set}).columns {
setColumn(fieldAt(v, s.fields[i]), c) setColumn(fieldAt(v, s.fields[i]), c)
} }
} }
+3 -3
View File
@@ -52,8 +52,8 @@ type structLink struct {
func structData(t *testing.T) *Generator { func structData(t *testing.T) *Generator {
t.Helper() t.Helper()
return newGenerator(t, writeData(t, map[string]string{ return newGenerator(t, writeData(t, map[string]string{
"person": `[{"format":"{first} {last}","first":"Ada","last":"Lovelace"},{"format":"{first} {last}","first":"Bo","last":"Ek"}]`, "person": `[{"format":"{first} {last}","first":"Ada","last":"Lovelace"},{"format":"{first} {last}","first":"Bo","last":"Ek","born":"1815"}]`,
"place": `[{"format":"{city}","city":"Stockholm","zip":"111 22"},{"format":"{city}","city":"Tranås","zip":"573 31"}]`, "place": `[{"format":"{city}","city":"Stockholm","zip":"111 22"},{"format":"{city}","city":"Tranås","zip":"573 31","region":"F"}]`,
"mid": `{"format":"","score":["{/src.score}",{"format":"5","datatype":"integer"}]}`, "mid": `{"format":"","score":["{/src.score}",{"format":"5","datatype":"integer"}]}`,
"src": `{"format":"","code":[null,"200","404"],"del":null,"score":[null,{"format":"{int(1,9)}","datatype":"integer"}]}`, "src": `{"format":"","code":[null,"200","404"],"del":null,"score":[null,{"format":"{int(1,9)}","datatype":"integer"}]}`,
"trip": `{"format":"","leg":[{"format":"{to}","to":"Oslo"},{"format":"{to}","to":"Rome"}]}`, "trip": `{"format":"","leg":[{"format":"{to}","to":"Oslo"},{"format":"{to}","to":"Rome"}]}`,
@@ -275,7 +275,7 @@ func TestFakeStructErrors(t *testing.T) {
}{}, "is empty"}, }{}, "is empty"},
{&struct { {&struct {
A string `fake:"[abc]"` A string `fake:"[abc]"`
}{}, `holds a "["`}, }{}, `"["`},
{&struct { {&struct {
A string `fake:"{.person.first} x"` A string `fake:"{.person.first} x"`
}{}, "write {/person.first}"}, }{}, "write {/person.first}"},
+521
View File
@@ -0,0 +1,521 @@
package fejkdata
import (
"fmt"
"math"
"sort"
"strconv"
"strings"
"sync"
)
// table is a category whose rows come from a TSV beside it: the header names the
// columns, each row is one draw, and the format renders the drawn row. A column is a
// cell of the row a render pinned; a cell carrying tokens compiles to a string node.
type table struct {
category string
file string
format *template // fields are the column nodes
columns []string
col map[string]int
fields map[string]node // column nodes, the format's fields
whole *row // the pinned row rendered by the format
cells []string // rows × columns, flat
tokens map[int]*template
key int // column index, or -1
name int
weight int
parent int
cum []float64 // cumulative weights, nil when uniform
byKey map[string]int
parentT *table
children map[string]*table
once sync.Once
index tableIndex
}
// tableIndex is what selection and linked draws look up, built on the first draw
// that needs it.
type tableIndex struct {
byName map[string][]int
children map[string][]int // rows by parent key
childCum map[string][]float64 // cumulative weights per parent key, nil when uniform
}
func (*table) isNode() {}
// column is one column of a table, rendered as the cell of the row the render pinned.
type column struct {
t *table
i int
}
func (*column) isNode() {}
// row is the row of a table the render pinned, rendered by the table's format.
type row struct{ t *table }
func (*row) isNode() {}
func (t *table) rows() int { return len(t.cells) / len(t.columns) }
func (t *table) cell(row, col int) string { return t.cells[row*len(t.columns)+col] }
// cellNode is what a cell renders: its compiled template where it carries tokens,
// else nil for its text.
func (t *table) cellNode(row, col int) *template { return t.tokens[row*len(t.columns)+col] }
// tableOptions are the keys a table object takes; every other key is refused.
var tableOptions = []string{"format", "key", "name", "parent", "rows", "weight"}
func isTableOption(name string) bool {
for _, o := range tableOptions {
if o == name {
return true
}
}
return false
}
// inSelector is what a key or name cell may not contain: the path grammar reserves
// brackets and braces, and a quote or a bar would make the selector no path and no
// token arm.
const inSelector = `[]{}"|`
// compileTable compiles a category object naming a rows file into a table.
func compileTable(m map[string]any, category string, files *categoryFiles) (*table, error) {
o, err := readTableOptions(m)
if err != nil {
return nil, err
}
data, err := files.rows(o.rows)
if err != nil {
return nil, err
}
t := &table{category: category, file: o.rows, key: -1, name: -1, weight: -1, parent: -1}
if err := t.parseRows(data); err != nil {
return nil, fmt.Errorf("%s: %w", o.rows, err)
}
if err := t.bindOptions(o); err != nil {
return nil, err
}
if err := t.checkCells(); err != nil {
return nil, fmt.Errorf("%s: %w", o.rows, err)
}
if err := t.compileFormat(o.format); err != nil {
return nil, err
}
return t, nil
}
type tableOptionValues struct {
format, key, name, parent, rows, weight string
}
func readTableOptions(m map[string]any) (tableOptionValues, error) {
var o tableOptionValues
for k := range m {
if !isTableOption(k) {
return o, fmt.Errorf("a table takes %s; %q is none of them", strings.Join(tableOptions, ", "), k)
}
}
for k, into := range map[string]*string{"format": &o.format, "key": &o.key, "name": &o.name, "parent": &o.parent, "rows": &o.rows, "weight": &o.weight} {
v, ok := m[k]
if !ok {
continue
}
s, isString := v.(string)
if !isString {
return o, fmt.Errorf("%s must be a string, got %T", k, v)
}
if k != "format" && s == "" {
return o, fmt.Errorf("%s names a column, so it cannot be empty; drop it", k)
}
*into = s
}
if _, ok := m["format"]; !ok {
return o, fmt.Errorf("template object missing string \"format\"")
}
if !strings.HasSuffix(o.rows, ".tsv") || strings.Contains(o.rows, "/") {
return o, fmt.Errorf("rows names a .tsv file beside the category, not %q", o.rows)
}
if o.key != "" && o.key == o.name {
return o, fmt.Errorf("name names the key column %q, which a selector already reads; drop it", o.name)
}
return o, nil
}
// parseRows reads the TSV: the header line names the columns, each following line
// is a row of as many cells. Cells are substrings of data, so the file is held once.
func (t *table) parseRows(data string) error {
data = strings.TrimSuffix(strings.TrimPrefix(data, "\xEF\xBB\xBF"), "\n")
header, rest, _ := strings.Cut(data, "\n")
header = strings.TrimSuffix(header, "\r")
if header == "" {
return fmt.Errorf("has no header line naming its columns")
}
t.columns = strings.Split(header, "\t")
t.col = make(map[string]int, len(t.columns))
t.fields = make(map[string]node, len(t.columns))
for i, name := range t.columns {
if err := checkName(name); err != nil {
return fmt.Errorf("column %w", err)
}
if _, dup := t.col[name]; dup {
return fmt.Errorf("column %q is named twice", name)
}
t.col[name] = i
t.fields[name] = &column{t, i}
}
if header == data {
return fmt.Errorf("has no rows below its header; a table is at least two rows")
}
m := len(t.columns)
t.cells = make([]string, 0, m*(strings.Count(rest, "\n")+1))
for line := 2; ; line++ {
row, more, found := strings.Cut(rest, "\n")
row = strings.TrimSuffix(row, "\r")
if err := t.appendRow(row, line); err != nil {
return err
}
if !found {
break
}
rest = more
}
if t.rows() < 2 {
return fmt.Errorf("has one row, which is a template; write it as one")
}
return nil
}
// appendRow splits one line into as many cells as the header has columns.
func (t *table) appendRow(row string, line int) error {
if row == "" {
return fmt.Errorf("line %d is empty; every line below the header is a row", line)
}
m := len(t.columns)
for n := 0; n < m; n++ {
cell, next, tab := strings.Cut(row, "\t")
if !tab && n < m-1 || tab && n == m-1 {
return fmt.Errorf("line %d has %s cells than the %d columns", line, map[bool]string{true: "more", false: "fewer"}[tab], m)
}
t.cells = append(t.cells, cell)
row = next
}
return nil
}
// bindOptions resolves each option to its column and proves what it claims of the
// cells: a key is unique, a weight a positive number.
func (t *table) bindOptions(o tableOptionValues) error {
for _, opt := range []struct {
name, value string
into *int
}{{"key", o.key, &t.key}, {"name", o.name, &t.name}, {"parent", o.parent, &t.parent}, {"weight", o.weight, &t.weight}} {
if opt.value == "" {
continue
}
i, ok := t.col[opt.value]
if !ok {
return fmt.Errorf("%s names no column %q of %s; the columns are %v", opt.name, opt.value, o.rows, t.columns)
}
*opt.into = i
}
if t.name >= 0 && t.key < 0 {
return fmt.Errorf("name selects a row as a key does, and lists the rows it matches by their keys, so it needs a key column; add key")
}
if err := t.indexKeys(); err != nil {
return err
}
return t.sumWeights()
}
// indexKeys proves every key names one row, and keeps the index a link is proved by.
func (t *table) indexKeys() error {
if t.key < 0 {
return nil
}
t.byKey = make(map[string]int, t.rows())
for r := 0; r < t.rows(); r++ {
k := t.cell(r, t.key)
if k == "" {
return fmt.Errorf("%s line %d: the key is empty", t.file, r+2)
}
if first, dup := t.byKey[k]; dup {
return fmt.Errorf("%s line %d: key %q repeats line %d; a key selects one row", t.file, r+2, k, first+2)
}
t.byKey[k] = r
}
for r := 0; r < t.rows() && t.name >= 0; r++ {
if n := t.cell(r, t.name); t.byKey[n] != r {
if other, isKey := t.byKey[n]; isKey {
return fmt.Errorf("%s line %d: name %q is the key of line %d, which a selector reads first, so the name could never select this row", t.file, r+2, n, other+2)
}
}
}
return nil
}
// sumWeights proves every weight is a positive number and builds the cumulative table.
func (t *table) sumWeights() error {
if t.weight < 0 {
return nil
}
t.cum = make([]float64, t.rows())
total := 0.0
for r := range t.cum {
w, err := strconv.ParseFloat(t.cell(r, t.weight), 64)
if err != nil || math.IsInf(w, 0) || math.IsNaN(w) || w <= 0 {
return fmt.Errorf("%s line %d: weight %q is not a positive number", t.file, r+2, t.cell(r, t.weight))
}
total += w
t.cum[r] = total
}
if math.IsInf(total, 0) {
return fmt.Errorf("%s: the weights sum past the largest number", t.file)
}
return nil
}
// checkCells compiles every cell carrying a token, and fences what a selector reads.
func (t *table) checkCells() error {
for i, cell := range t.cells {
row, col := i/len(t.columns), i%len(t.columns)
if (col == t.key || col == t.name) && strings.ContainsAny(cell, inSelector) {
return fmt.Errorf("line %d: %s %q contains %q, which a selector cannot spell", row+2, t.columns[col], cell, cell[strings.IndexAny(cell, inSelector):][:1])
}
if !strings.ContainsAny(cell, "{}") {
continue
}
n, err := compileString(cell)
if err != nil {
return fmt.Errorf("line %d, %s: %w", row+2, t.columns[col], err)
}
if t.tokens == nil {
t.tokens = map[int]*template{}
}
t.tokens[i] = n.(*template)
t.tokens[i].cellOf, t.tokens[i].cellRow = t, row
}
return nil
}
// compileFormat compiles the format over the columns as its fields.
func (t *table) compileFormat(format string) error {
for _, name := range fieldTokens(format) {
a := splitArm(name, nil)
if _, ok := t.col[a.key]; !ok && !isRef(a.key) && a.key != "" {
return fmt.Errorf("format names no column %q of %s; the columns are %v", a.key, t.file, t.columns)
}
}
if err := checkTokens(format, t.fields); err != nil {
return err
}
t.format = &template{format: format, fields: t.fields, repeat: 1, record: true, table: t}
t.whole = &row{t}
return t.format.compileFormat()
}
// linkTables binds every table's parent to the table beside it, and proves the
// links: a parent has a key, every link cell is one, every parent row is linked
// to, no chain of parents closes, and no child is named like a parent's column.
func linkTables(root map[string]node) error {
var walk func(dir string, children map[string]node) error
walk = func(dir string, children map[string]node) error {
for _, name := range sortedNames(children) {
path := join(dir, name)
switch n := children[name].(type) {
case *folder:
if err := walk(path, n.children); err != nil {
return err
}
case *table:
if n.parent < 0 {
continue
}
if err := n.linkParent(path, children); err != nil {
return fmt.Errorf("%s: %w", path, err)
}
}
}
return nil
}
return walk("", root)
}
func (t *table) linkParent(path string, siblings map[string]node) error {
name := t.columns[t.parent]
p, ok := siblings[name].(*table)
switch {
case siblings[name] == nil:
return fmt.Errorf("parent %q names no table beside it", name)
case !ok:
return fmt.Errorf("parent %q is not a table; a link column reads a table's key", name)
case p.key < 0:
return fmt.Errorf("parent %q has no key column to link to", name)
}
var ancestors []*table
for q, seen := p, map[*table]bool{t: true}; q != nil; q, _ = siblings[q.columns[q.parent]].(*table) {
if seen[q] {
return fmt.Errorf("parent cycle: %s reaches itself through its parents", q.category)
}
seen[q] = true
ancestors = append(ancestors, q)
if q.parent < 0 {
break
}
}
for _, q := range ancestors {
if _, clash := q.col[t.category]; clash {
return fmt.Errorf("%q is named like a column of %q, its ancestor, so %s.%s could read either; rename one", t.category, q.category, q.category, t.category)
}
}
linked := make(map[string]bool, p.rows())
for r := 0; r < t.rows(); r++ {
k := t.cell(r, t.parent)
if _, ok := p.byKey[k]; !ok {
return fmt.Errorf("%s line %d: %s %q is no key of %s", t.file, r+2, name, k, p.file)
}
linked[k] = true
}
for r := 0; r < p.rows(); r++ {
if k := p.cell(r, p.key); !linked[k] {
return fmt.Errorf("%s links no row to %s %q; every %s row needs one, or drop line %d of %s", t.file, name, k, name, r+2, p.file)
}
}
t.parentT = p
if p.children == nil {
p.children = map[string]*table{}
}
p.children[t.category] = t
return nil
}
func (t *table) indexed() *tableIndex {
t.once.Do(func() {
if t.name >= 0 {
t.index.byName = make(map[string][]int, t.rows())
for r := 0; r < t.rows(); r++ {
n := t.cell(r, t.name)
t.index.byName[n] = append(t.index.byName[n], r)
}
}
if t.parent >= 0 {
t.index.children = map[string][]int{}
for r := 0; r < t.rows(); r++ {
k := t.cell(r, t.parent)
t.index.children[k] = append(t.index.children[k], r)
}
if t.cum != nil {
t.index.childCum = make(map[string][]float64, len(t.index.children))
for k, rows := range t.index.children {
cum, total := make([]float64, len(rows)), 0.0
for i, r := range rows {
w, _ := strconv.ParseFloat(t.cell(r, t.weight), 64) // sumWeights proved it
total += w
cum[i] = total
}
t.index.childCum[k] = cum
}
}
}
})
return &t.index
}
// draw picks a row over the whole table. The session is concrete rather than the rng
// interface so that a walk holding the draws allocates nothing.
func (t *table) draw(s *session) int {
if t.cum == nil {
return s.IntN(t.rows())
}
return pickCum(s, t.cum)
}
func pickCum(s *session, cum []float64) int {
x := s.Float64() * cum[len(cum)-1]
return min(sort.Search(len(cum), func(i int) bool { return cum[i] > x }), len(cum)-1) // x can round up to the total
}
// drawUnder picks a row among those linked to parent row pr.
func (t *table) drawUnder(s *session, pr int) int {
ix := t.indexed()
k := t.parentT.cell(pr, t.parentT.key)
rows := ix.children[k]
if ix.childCum == nil {
return rows[s.IntN(len(rows))]
}
return rows[pickCum(s, ix.childCum[k])]
}
// descendant is the table named name among those linked to t, at any depth.
func (t *table) descendant(name string) *table {
if c, ok := t.children[name]; ok {
return c
}
for _, c := range t.children {
if d := c.descendant(name); d != nil {
return d
}
}
return nil
}
// parentRow is the row of t's parent that row r links to.
func (t *table) parentRow(r int) int { return t.parentT.byKey[t.cell(r, t.parent)] }
// descends reports whether a is an ancestor of t.
func (t *table) descends(a *table) bool {
for p := t.parentT; p != nil; p = p.parentT {
if p == a {
return true
}
}
return false
}
// under reports whether row r of t sits inside row pr of ancestor a.
func (t *table) under(r int, a *table, pr int) bool {
for c, row := t, r; c.parentT != nil; c, row = c.parentT, c.parentRow(row) {
if c.parentT == a {
return c.parentRow(row) == pr
}
}
return false
}
// find is the row a selector names: by key first, then by name, where a name
// naming several rows resolves inside the ancestors pinned in d.
func (t *table) find(sel string, d *draws) (int, error) {
if t.key < 0 {
return 0, fmt.Errorf("%s has no key column to select a row by", t.category)
}
if r, ok := t.byKey[sel]; ok {
return r, nil
}
rows := t.indexed().byName[sel]
if len(rows) > 1 {
rows = d.inside(t, rows)
}
switch len(rows) {
case 1:
return rows[0], nil
case 0:
return 0, fmt.Errorf("no row of %s has key or name %q", t.category, sel)
}
keys := make([]string, len(rows))
for i, r := range rows {
keys[i] = t.cell(r, t.key)
}
inside := ""
if t.parentT != nil {
inside = fmt.Sprintf(", or select it inside its %s", t.parentT.category)
}
return 0, fmt.Errorf("%q names %d rows of %s; select one by key, one of %v%s", sel, len(rows), t.category, keys, inside)
}
// selectorSpelling is how a path writes a selected row, for messages.
func (t *table) selectorSpelling(r int) string {
return t.category + "[" + t.cell(r, t.key) + "]"
}
+662
View File
@@ -0,0 +1,662 @@
package fejkdata
import (
"os"
"path/filepath"
"reflect"
"regexp"
"slices"
"strings"
"testing"
)
// writeFiles writes a data directory from a map of relative file name, extension
// included, to content.
func writeFiles(t *testing.T, files map[string]string) string {
t.Helper()
dir := t.TempDir()
for name, content := range files {
p := filepath.Join(dir, name)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
return dir
}
// geo is three linked tables: region <- municipality <- locality.
func geo() map[string]string {
return map[string]string{
"region.json": `{"format":"{name}","rows":"region.tsv","key":"code","name":"name","weight":"population"}`,
"region.tsv": "code\tname\tpopulation\ttimezone\n01\tStockholms län\t2400000\tEurope/Stockholm\n12\tSkåne län\t1400000\tEurope/Stockholm\n14\tVästra Götalands län\t1750000\tEurope/Stockholm\n",
"municipality.json": `{"format":"{name}","rows":"municipality.tsv","key":"code","name":"name","parent":"region","weight":"population"}`,
"municipality.tsv": "code\tname\tregion\tpopulation\n0180\tStockholm\t01\t980000\n0184\tSolna\t01\t85000\n1280\tMalmö\t12\t360000\n1281\tLund\t12\t130000\n1480\tGöteborg\t14\t590000\n",
"locality.json": `{"format":"{name}","rows":"locality.tsv","key":"code","name":"name","parent":"municipality"}`,
"locality.tsv": "code\tname\tmunicipality\nL1\tStockholm\t0180\nL2\tSolna\t0184\nL3\tMalmö\t1280\nL4\tLund\t1281\nL5\tGöteborg\t1480\nL6\tHisingen\t1480\nL7\tSandby\t1281\nL8\tSandby\t0184\n",
}
}
var (
municipalityOf = map[string]string{"L1": "0180", "L2": "0184", "L3": "1280", "L4": "1281", "L5": "1480", "L6": "1480", "L7": "1281", "L8": "0184"}
regionOf = map[string]string{"0180": "01", "0184": "01", "1280": "12", "1281": "12", "1480": "14"}
)
func with(files map[string]string, more map[string]string) map[string]string {
out := map[string]string{}
for k, v := range files {
out[k] = v
}
for k, v := range more {
out[k] = v
}
return out
}
func TestTableRendersRowsAndColumns(t *testing.T) {
f := newGenerator(t, writeFiles(t, geo()), WithSeed(1))
names := map[string]bool{"Stockholms län": true, "Skåne län": true, "Västra Götalands län": true}
for i := 0; i < 50; i++ {
if v := fake(t, f, "region"); !names[v] {
t.Fatalf("region = %q, want a row's format", v)
}
if v := fake(t, f, "region.timezone"); v != "Europe/Stockholm" {
t.Fatalf("region.timezone = %q", v)
}
if v := fake(t, f, "region.code"); v != "01" && v != "12" && v != "14" {
t.Fatalf("region.code = %q", v)
}
}
want := []string{
"locality", "locality.code", "locality.municipality", "locality.name",
"municipality", "municipality.code", "municipality.locality", "municipality.locality.code", "municipality.locality.municipality", "municipality.locality.name", "municipality.name", "municipality.population", "municipality.region",
"region", "region.code", "region.municipality", "region.municipality.code", "region.municipality.locality", "region.municipality.locality.code", "region.municipality.locality.municipality", "region.municipality.locality.name", "region.municipality.name", "region.municipality.population", "region.municipality.region", "region.name", "region.population", "region.timezone",
}
if got := f.List(); !reflect.DeepEqual(got, want) {
t.Fatalf("List() = %v\nwant %v", got, want)
}
r, err := f.FakeRecord("region")
if err != nil {
t.Fatal(err)
}
if r.CSVHeader() != "code,name,population,timezone" {
t.Fatalf("CSVHeader = %q", r.CSVHeader())
}
line := strings.Split(r.CSVLine(), ",")
if len(line) != 4 || !names[line[1]] || line[3] != "Europe/Stockholm" {
t.Fatalf("CSVLine = %q, want one row's cells", r.CSVLine())
}
for _, c := range r.Columns() {
if c.DataType != DataTypeString || c.Null {
t.Fatalf("column %q is %s null=%v, want a string", c.Name, c.DataType, c.Null)
}
}
}
func TestTableWeightSkewsTheDraw(t *testing.T) {
f := newGenerator(t, writeFiles(t, geo()), WithSeed(3))
count := map[string]int{}
for i := 0; i < 3000; i++ {
count[fake(t, f, "region.code")]++
}
if count["01"] < 1200 || count["12"] > 900 {
t.Fatalf("region draws %v, want weighted by population (01 ≈ 43%%, 12 ≈ 25%%)", count)
}
count = map[string]int{}
for i := 0; i < 3000; i++ {
count[fake(t, f, "region[01].municipality.code")]++
}
if count["0180"] < 2500 || count["0184"] == 0 || len(count) != 2 {
t.Fatalf("municipality draws inside 01 %v, want Stockholm ≈ 92%% and Solna the rest", count)
}
}
func TestTableCellsAreStringNodes(t *testing.T) {
dir := writeFiles(t, map[string]string{
"w.json": `"x"`,
"place.json": `{"format":"{zip} {name}","rows":"place.tsv","key":"name"}`,
"place.tsv": "name\tzip\ttag\nStockholm\t1{digits(2)} {digits(2)}\t{/w}\nTranås\t573 {digits(2)}\tplain\n",
})
f := newGenerator(t, dir, WithSeed(1))
zip := regexp.MustCompile(`^(1\d\d \d\d Stockholm|573 \d\d Tranås)$`)
for i := 0; i < 50; i++ {
if v := fake(t, f, "place"); !zip.MatchString(v) {
t.Fatalf("place = %q, want the cell's tokens expanded", v)
}
}
if v := fake(t, f, "place[Stockholm].tag"); v != "x" {
t.Fatalf("place[Stockholm].tag = %q, want the reference rendered", v)
}
bad := writeFiles(t, map[string]string{
"place.json": `{"format":"{name}","rows":"place.tsv"}`,
"place.tsv": "name\tzip\nA\t{name}\nB\t2\n",
})
if _, err := New(WithoutShippedData(), WithDataPath(bad)); err == nil || !strings.Contains(err.Error(), "place.tsv") || !strings.Contains(err.Error(), `no field "name"`) {
t.Fatalf("New = %v, want a cell reading a column refused, naming the file", err)
}
}
func TestTableSelectsARowByKeyOrName(t *testing.T) {
files := with(geo(), map[string]string{
"city.json": `{"format":"{name}","rows":"city.tsv","key":"code","name":"name"}`,
"city.tsv": "code\tname\nSTL\tSt. Louis\nSPI\tSpringfield\n",
})
f := newGenerator(t, writeFiles(t, files), WithSeed(1))
for path, want := range map[string]string{
"region[12]": "Skåne län",
"region[Skåne län].code": "12",
"municipality[1281].locality[Sandby]": "Sandby",
"municipality[1281].locality[Sandby].code": "L7",
"municipality[0184].locality[Sandby].code": "L8",
"city[St. Louis].code": "STL",
} {
if got := fake(t, f, path); got != want {
t.Errorf("Fake(%q) = %q, want %q", path, got, want)
}
}
for path, want := range map[string]string{
"region[99]": `"99"`,
"locality[Sandby]": "L7 L8",
"region[12].code[1]": "not a table",
"region[]": "empty",
"region[12": "]",
"region[12].municipality[1480]": "not inside",
} {
if _, err := f.Fake(path); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("Fake(%q) = %v, want an error mentioning %s", path, err, want)
}
}
if v := fake(t, f, "municipality[0180].region"); v != "01" {
t.Fatalf("municipality[0180].region = %q, want the link column's cell", v)
}
noKey := writeFiles(t, map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\nx\ny\n"})
if _, err := newGenerator(t, noKey).Fake("t[x]"); err == nil || !strings.Contains(err.Error(), "no key") {
t.Fatalf("Fake(t[x]) = %v, want no column to select by", err)
}
}
func TestTableDescendsToALinkedTable(t *testing.T) {
f := newGenerator(t, writeFiles(t, geo()), WithSeed(2))
for i := 0; i < 200; i++ {
if m := fake(t, f, "region[12].municipality.code"); regionOf[m] != "12" {
t.Fatalf("region[12].municipality.code = %q, outside 12", m)
}
if l := fake(t, f, "region[12].locality.code"); regionOf[municipalityOf[l]] != "12" {
t.Fatalf("region[12].locality.code = %q, outside 12", l)
}
if l := fake(t, f, "municipality[1480].locality"); l != "Göteborg" && l != "Hisingen" {
t.Fatalf("municipality[1480].locality = %q", l)
}
if l := fake(t, f, "region.municipality.locality.name"); l == "" {
t.Fatal("region.municipality.locality.name rendered empty")
}
}
r, err := f.FakeRecord("region[14].municipality")
if err != nil {
t.Fatal(err)
}
if line := r.CSVLine(); line != "1480,Göteborg,590000,14" {
t.Fatalf("record inside region 14 = %q", line)
}
}
func TestLinkedTablesDrawConsistently(t *testing.T) {
spellings := map[string]string{
"leaf first": `{"format":"{l}|{m}|{r}","l":"{/locality.code}","m":"{/municipality.code}","r":"{/region.code}"}`,
"root first": `{"format":"{r}|{m}|{l}","r":"{/region.code}","m":"{/municipality.code}","l":"{/locality.code}"}`,
"skipping one": `{"format":"{r}|{l}","r":"{/region.code}","l":"{/locality.code}"}`,
"nested": `{"format":"{a}|{l}","a":{"format":"{r}|{m}","r":"{/region.code}","m":"{/municipality.code}"},"l":"{/locality.code}"}`,
"a record": `{"format":"","l":"{/locality.code}","m":"{/municipality.code}","r":"{/region.code}"}`,
}
for name, addr := range spellings {
f := newGenerator(t, writeFiles(t, with(geo(), map[string]string{"addr.json": addr})), WithSeed(5))
seen := map[string]bool{}
for i := 0; i < 300; i++ {
var parts []string
if name == "a record" {
r, err := f.FakeRecord("addr")
if err != nil {
t.Fatal(err)
}
for _, c := range r.Columns() {
parts = append(parts, c.Value)
}
} else {
parts = strings.Split(fake(t, f, "addr"), "|")
}
var l, m, r string
for _, p := range parts {
switch {
case strings.HasPrefix(p, "L"):
l = p
case len(p) == 4:
m = p
default:
r = p
}
}
if m == "" {
m = municipalityOf[l]
}
if municipalityOf[l] != m || regionOf[m] != r {
t.Fatalf("%s: %v is not one consistent draw", name, parts)
}
seen[l] = true
}
if len(seen) < 4 {
t.Fatalf("%s: only %v drawn in 300 renders", name, seen)
}
}
}
func TestLinkedTablesDrawApartAcrossGroupsAndRepeats(t *testing.T) {
files := with(geo(), map[string]string{
"groups.json": `{"format":"{a}|{b}","a":{"format":"{/locality.code}","drawGroup":"g"},"b":"{/locality.code}"}`,
"many.json": `{"format":"{x}","x":{"format":"{/locality.code}","repeat":8,"separator":"|"}}`,
})
f := newGenerator(t, writeFiles(t, files), WithSeed(9))
for _, path := range []string{"groups", "many"} {
differ := false
for i := 0; i < 100 && !differ; i++ {
parts := strings.Split(fake(t, f, path), "|")
for _, p := range parts[1:] {
differ = differ || p != parts[0]
}
}
if !differ {
t.Fatalf("%s: every draw agreed in 100 renders, want groups and repeat iterations drawn apart", path)
}
}
}
func TestTableSelectorInAReference(t *testing.T) {
files := with(geo(), map[string]string{
"x.json": `{"format":"{a} {b} {c}","a":"{/region[12].name}","b":"{/region[12].municipality.code}","c":"{/region[12].locality.code}"}`,
})
f := newGenerator(t, writeFiles(t, files), WithSeed(1))
for i := 0; i < 100; i++ {
parts := strings.Fields(fake(t, f, "x"))
m, l := parts[len(parts)-2], parts[len(parts)-1]
if !strings.HasPrefix(fake(t, f, "x"), "Skåne län") || regionOf[m] != "12" || municipalityOf[l] != m {
t.Fatalf("x = %v, want one draw inside region 12", parts)
}
}
for name, c := range map[string]struct{ json, want string }{
"unknown row": {`"{/region[99].name}"`, `"99"`},
"ambiguous name": {`"{/locality[Sandby].name}"`, "L7 L8"},
"not inside": {`"{/region[12].municipality[0180].name}"`, "not inside"},
"selector on template": {`"{/x[1].a}"`, "not a table"},
} {
_, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(files, map[string]string{"bad.json": c.json}))))
if err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%s: New = %v, want an error mentioning %q", name, err, c.want)
}
}
}
func TestTableSelectionFences(t *testing.T) {
rejected := map[string]struct{ json, want string }{
"bare beside a path": {`"{/region} {/region.name}"`, "reads a path into"},
"bare beside a linked path": {`"{/region} {/locality.name}"`, "drawGroup"},
"selected beside unselected": {`"{/region[12].municipality.name} {/municipality.code}"`, "{/region[12].municipality.code}"},
"two selections": {`"{/region[12].name} {/region[14].name}"`, "drawGroup"},
"selection beside a descendant": {`"{/region[12].name} {/locality.code}"`, "{/region[12].locality.code}"},
}
for name, c := range rejected {
_, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(geo(), map[string]string{"x.json": c.json}))))
if err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%s: New = %v, want it rejected mentioning %q", name, err, c.want)
}
}
accepted := map[string]string{
"a descent beside a path into it": `"{/region.municipality} {/region.municipality.name}"`,
"selections in two groups": `{"format":"{a} {b}","a":{"format":"{/region[12].name}","drawGroup":"a"},"b":"{/region[14].name}"}`,
"one selection twice": `"{/region[12].name} {/region[12].timezone} {/region[12].locality.name}"`,
"a bare table in another group": `{"format":"{a} {b}","a":{"format":"{/region}","drawGroup":"a"},"b":"{/locality.name}"}`,
}
for name, json := range accepted {
f, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(geo(), map[string]string{"x.json": json}))), WithSeed(1))
if err != nil {
t.Errorf("%s: New = %v, want it accepted", name, err)
continue
}
fake(t, f, "x")
}
f := newGenerator(t, writeFiles(t, with(geo(), map[string]string{"x.json": accepted["a descent beside a path into it"]})), WithSeed(1))
for i := 0; i < 100; i++ {
if parts := strings.Fields(fake(t, f, "x")); parts[0] != parts[1] {
t.Fatalf("x = %v, want the descended row and the path into it to agree", parts)
}
}
}
// The family fence replays each read's selectors through the walk the render uses, so
// spellings that pin the same rows by different routes agree, and two rows of one
// table are refused at load rather than found at render.
func TestTableFamilyFenceReplaysPins(t *testing.T) {
accepted := map[string]string{
"skip-level selectors": `"{/region[12].locality[L4].name}|{/region[12].name}"`,
"descendant then ancestor": `"{/locality[L4].name}|{/region[12].name}"`,
"two selected levels": `"{/municipality[1281].name}|{/locality[L4].name}"`,
"nested prefixes": `"{/region[12].municipality[1281].name}|{/municipality[1281].locality[L4].name}"`,
"a draw under the selection": `"{/region[12].name}|{/region[12].municipality.locality[L4].name}"`,
}
for name, json := range accepted {
f, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(geo(), map[string]string{"x.json": json}))), WithSeed(1))
if err != nil {
t.Errorf("%s: New = %v, want it accepted", name, err)
continue
}
for i := 0; i < 50; i++ {
v := fake(t, f, "x")
if strings.Contains(v, "Sandby") || strings.Contains(v, "Malmö") || strings.Contains(v, "1280") || !strings.Contains(v, "Lund") && !strings.Contains(v, "Skåne") {
t.Fatalf("%s: x = %q, want every part inside region 12 and Lund", name, v)
}
}
}
rejected := map[string]struct{ json, want string }{
"two rows of one table": {`"{/region[12].locality[L4].name} {/region[12].locality[L7].name}"`, "drawGroup"},
"a row outside a selected ancestor": {`"{/region[14].name} {/locality[L4].name}"`, "drawGroup"},
}
for name, c := range rejected {
_, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(geo(), map[string]string{"x.json": c.json}))))
if err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%s: New = %v, want it rejected mentioning %q", name, err, c.want)
}
}
}
func TestTableFences(t *testing.T) {
base := map[string]string{
"region.json": `{"format":"{name}","rows":"region.tsv","key":"code","name":"name"}`,
"region.tsv": "code\tname\n01\tA\n12\tB\n",
}
rejected := map[string]struct {
files map[string]string
want string
}{
"rows names a missing file": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`}, "t.tsv"},
"a TSV nothing names": {with(base, map[string]string{"stray.tsv": "a\nx\n"}), "stray.tsv"},
"rows outside its folder": {with(base, map[string]string{"t.json": `{"format":"{code}","rows":"../region.tsv"}`}), "beside"},
"rows not a tsv": {with(base, map[string]string{"t.json": `{"format":"{code}","rows":"region.txt"}`, "region.txt": "code\n1\n"}), ".tsv"},
"key names no column": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"b"}`, "t.tsv": "a\nx\ny\n"}, `"b"`},
"name names no column": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","name":"b"}`, "t.tsv": "a\nx\ny\n"}, `"b"`},
"weight names no column": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","weight":"b"}`, "t.tsv": "a\nx\ny\n"}, `"b"`},
"parent names no column": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","parent":"b"}`, "t.tsv": "a\nx\ny\n"}, `"b"`},
"key equals name": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"a","name":"a"}`, "t.tsv": "a\nx\ny\n"}, "drop"},
"duplicate key": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"a"}`, "t.tsv": "a\nx\nx\n"}, `"x"`},
"empty key": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"a"}`, "t.tsv": "a\tb\n\ty\nx\tz\n"}, "empty"},
"a bracket in a key": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"a"}`, "t.tsv": "a\nx[1]\ny\n"}, `"["`},
"a brace in a name": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"a","name":"n"}`, "t.tsv": "a\tn\nx\tx{1}\ny\ty\n"}, `"{"`},
"name without a key": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","name":"a"}`, "t.tsv": "a\nx\nx\n"}, "key"},
"a name that is another row's key": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"a","name":"n"}`, "t.tsv": "a\tn\nx\ty\ny\tz\n"}, `"y"`},
"a cell reading its family": {with(geo(), map[string]string{"locality.tsv": "code\tname\tmunicipality\tnote\nL1\tStockholm\t0180\t{/municipality.code}\nL2\tSolna\t0184\t-\nL3\tMalmö\t1280\t-\nL4\tLund\t1281\t-\nL5\tGöteborg\t1480\t-\n"}), "family"},
"a format reading its family": {with(geo(), map[string]string{"locality.json": `{"format":"{name} {/region.name}","rows":"locality.tsv","key":"code","name":"name","parent":"municipality"}`}), "family"},
"a descendant named like an ancestor's column": {with(geo(), map[string]string{"region.tsv": "code\tname\tpopulation\tlocality\n01\tStockholms län\t2400000\tx\n12\tSkåne län\t1400000\ty\n14\tVästra Götalands län\t1750000\tz\n"}), `"locality"`},
"weight not a number": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","weight":"w"}`, "t.tsv": "a\tw\nx\tmany\ny\t2\n"}, `"many"`},
"weight zero": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","weight":"w"}`, "t.tsv": "a\tw\nx\t0\ny\t2\n"}, "0"},
"weight negative": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","weight":"w"}`, "t.tsv": "a\tw\nx\t-1\ny\t2\n"}, "-1"},
"reserved column name": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\tb.c\nx\ty\n"}, `"b.c"`},
"duplicate column": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\ta\nx\ty\n"}, `"a"`},
"empty column name": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\t\nx\ty\n"}, "empty"},
"short row": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\tb\nx\ty\nz\n"}, "line 3"},
"no rows": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\n"}, "no rows"},
"a blank line after the rows": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\tb\nx\t1\ny\t2\n\n"}, "line 4"},
"a blank line in one column": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\nx\n\ny\n"}, "line 3"},
"a table reaching its family through a template": {with(geo(), map[string]string{"addr.json": `"{/locality.name}"`, "region.tsv": "code\tname\tpopulation\ttimezone\tnote\n01\tStockholms län\t2400000\tEurope/Stockholm\t{/addr}\n12\tSkåne län\t1400000\tEurope/Stockholm\t-\n14\tVästra Götalands län\t1750000\tEurope/Stockholm\t-\n"}), "family"},
"a table reaching its family through a repeat": {with(geo(), map[string]string{"addr.json": `{"format":"{/locality.name} ","repeat":3}`, "region.tsv": "code\tname\tpopulation\ttimezone\tnote\n01\tStockholms län\t2400000\tEurope/Stockholm\t{/addr}\n12\tSkåne län\t1400000\tEurope/Stockholm\t-\n14\tVästra Götalands län\t1750000\tEurope/Stockholm\t-\n"}), "family"},
"a table reaching its family through a group": {with(geo(), map[string]string{"addr.json": `{"format":"{/locality.name}","drawGroup":"g"}`, "region.tsv": "code\tname\tpopulation\ttimezone\tnote\n01\tStockholms län\t2400000\tEurope/Stockholm\t{/addr}\n12\tSkåne län\t1400000\tEurope/Stockholm\t-\n14\tVästra Götalands län\t1750000\tEurope/Stockholm\t-\n"}), "family"},
"one row": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\nx\n"}, "one row"},
"empty file": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": ""}, "header"},
"parent is not a table": {map[string]string{"p.json": `"x"`, "t.json": `{"format":"{a}","rows":"t.tsv","parent":"p"}`, "t.tsv": "a\tp\nx\tx\ny\tx\n"}, "not a table"},
"parent does not exist": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","parent":"p"}`, "t.tsv": "a\tp\nx\tx\ny\tx\n"}, `"p"`},
"parent in another folder": {map[string]string{"g/p.json": `{"format":"{k}","rows":"p.tsv","key":"k"}`, "g/p.tsv": "k\nx\ny\n", "t.json": `{"format":"{a}","rows":"t.tsv","parent":"p"}`, "t.tsv": "a\tp\nx\tx\ny\ty\n"}, `"p"`},
"parent has no key": {map[string]string{"p.json": `{"format":"{k}","rows":"p.tsv"}`, "p.tsv": "k\nx\ny\n", "t.json": `{"format":"{a}","rows":"t.tsv","parent":"p"}`, "t.tsv": "a\tp\nx\tx\ny\ty\n"}, "key"},
"dangling link": {with(base, map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","parent":"region"}`, "t.tsv": "a\tregion\nx\t01\ny\t99\n"}), `"99"`},
"childless parent row": {with(base, map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","parent":"region"}`, "t.tsv": "a\tregion\nx\t01\ny\t01\n"}), `"12"`},
"parent cycle": {map[string]string{"a.json": `{"format":"{k}","rows":"a.tsv","key":"k","parent":"b"}`, "a.tsv": "k\tb\nx\tx\ny\ty\n", "b.json": `{"format":"{k}","rows":"b.tsv","key":"k","parent":"a"}`, "b.tsv": "k\ta\nx\tx\ny\ty\n"}, "cycle"},
"child named like a column": {with(base, map[string]string{"name.json": `{"format":"{a}","rows":"name.tsv","parent":"region"}`, "name.tsv": "a\tregion\nx\t01\ny\t12\n"}), `"name"`},
"rows nested in a field": {map[string]string{"t.json": `{"format":"{x}","x":{"format":"{a}","rows":"x.tsv"}}`, "x.tsv": "a\nx\ny\n"}, "category"},
"rows in a choice item": {map[string]string{"t.json": `[{"format":"{a}","rows":"t.tsv"},"y"]`, "t.tsv": "a\nx\ny\n"}, "category"},
"unknown table option": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","fields":"a"}`, "t.tsv": "a\nx\ny\n"}, "a table takes"},
"repeat on a table": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","repeat":2}`, "t.tsv": "a\nx\ny\n"}, "a table takes"},
"drawGroup on a table": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","drawGroup":"g"}`, "t.tsv": "a\nx\ny\n"}, "a table takes"},
"option not a string": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":1}`, "t.tsv": "a\nx\ny\n"}, "string"},
"format names no column": {map[string]string{"t.json": `{"format":"{b}","rows":"t.tsv"}`, "t.tsv": "a\nx\ny\n"}, `no column "b"`},
"format reads into a column": {map[string]string{"t.json": `{"format":"{a.x}","rows":"t.tsv"}`, "t.tsv": "a\nx\ny\n"}, `"a"`},
"a reference into a column": {with(base, map[string]string{"t.json": `"{/region.name.x}"`}), "column"},
"a category referencing itself": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\n{/t.a}\ny\n"}, "names the category it sits in"},
}
for name, c := range rejected {
_, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, c.files)))
if err == nil {
t.Errorf("%s: New = nil error, want it rejected at load", name)
continue
}
if !strings.Contains(err.Error(), c.want) {
t.Errorf("%s: New = %v, want it to mention %q", name, err, c.want)
}
}
if _, err := New(WithoutShippedData(), WithDataFS(os.DirFS(writeFiles(t, base)))); err != nil {
t.Fatalf("New(WithDataFS) = %v, want a table loaded from any fs.FS", err)
}
inline := newGenerator(t, writeFiles(t, base))
if _, err := inline.NewTemplate(`{"format":"{a}","rows":"t.tsv"}`); err == nil || !strings.Contains(err.Error(), "inline") {
t.Fatalf("NewTemplate(rows) = %v, want a table refused inline", err)
}
bom := newGenerator(t, writeFiles(t, map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"a"}`, "t.tsv": "\xEF\xBB\xBFa\tb\nx\t1\ny\t2\n"}), WithSeed(1))
if v := fake(t, bom, "t[x].b"); v != "1" {
t.Fatalf("t[x].b under a BOM header = %q, want the mark stripped", v)
}
crlf := newGenerator(t, writeFiles(t, map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"a"}`, "t.tsv": "a\tb\r\nx\t1\r\ny\t2\r\n"}), WithSeed(1))
if v := fake(t, crlf, "t[y].b"); v != "2" {
t.Fatalf("t[y].b in a CRLF file = %q, want the carriage return stripped", v)
}
a, b := newGenerator(t, writeFiles(t, geo()), WithSeed(1)), newGenerator(t, writeFiles(t, geo()), WithSeed(1))
for _, path := range []string{"region.nope", "region.locality.nope", "region.municipality[9999]", "region[12].locality[L1]"} {
if _, err := a.Fake(path); err == nil {
t.Fatalf("Fake(%s) = nil error", path)
}
if _, err := a.FakeRecord(path); err == nil {
t.Fatalf("FakeRecord(%s) = nil error", path)
}
}
if x, y := fake(t, a, "region.locality.name"), fake(t, b, "region.locality.name"); x != y {
t.Fatalf("a failed Fake shifted the seeded stream: %q != %q", x, y)
}
two := newGenerator(t, writeFiles(t, with(geo(), map[string]string{"capital.json": `{"format":"{timezone}","rows":"region.tsv","key":"code"}`})), WithSeed(1))
if v := fake(t, two, "capital[12]"); v != "Europe/Stockholm" {
t.Fatalf("capital[12] over region.tsv = %q, want a second category over one file", v)
}
empty := newGenerator(t, writeFiles(t, map[string]string{"t.json": `{"format":"","rows":"t.tsv","key":"a"}`, "t.tsv": "a\tb\nx\t1\ny\t2\n"}), WithSeed(1))
if v := fake(t, empty, "t"); v != "" {
t.Fatalf("a record-only table renders %q, want \"\"", v)
}
if v := fake(t, empty, "t[y].b"); v != "2" {
t.Fatalf("t[y].b = %q", v)
}
}
func TestSameShapedChoiceIsATable(t *testing.T) {
rows := `[{"format":"{name}","name":"Sweden","alpha2":"SE"},{"format":"{name}","name":"Norway","alpha2":"NO"},{"format":"{name}","name":"Denmark","alpha2":"DK"}]`
_, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"country": rows})))
for _, want := range []string{"country.tsv", `"rows"`, `alpha2\tname`, "3 "} {
if err == nil || !strings.Contains(err.Error(), want) {
t.Fatalf("New(same-shaped choice) = %v, want it refused mentioning %q", err, want)
}
}
weighted := `[{"format":"{name}","name":"Sweden","weight":2},{"format":"{name}","name":"Norway"}]`
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"country": weighted}))); err == nil || !strings.Contains(err.Error(), "weight") {
t.Fatalf("New(weighted same-shaped choice) = %v, want it refused naming a weight column", err)
}
accepted := map[string]string{
"nested": `{"format":"{c}","c":` + rows + `}`,
"a choice field": `[{"format":"{maker} {model}","maker":"BMW","model":["X3","X5"]},{"format":"{maker} {model}","maker":"Ford","model":["Focus","Fiesta"]}]`,
"different formats": `[{"format":"{a}-{b}","a":"1","b":"2"},{"format":"{b}-{a}","a":"3","b":"4"}]`,
"different fields": `[{"format":"{a}","a":"1","b":"2"},{"format":"{a}","a":"3"}]`,
"a string item": `[{"format":"{a}","a":"1"},"x"]`,
"strings": `["a","b","c"]`,
}
for name, json := range accepted {
if _, err := New(WithoutShippedData(), WithDataPath(writeData(t, map[string]string{"x": json}))); err != nil {
t.Errorf("%s: New = %v, want it accepted", name, err)
}
}
f := newGenerator(t, writeData(t, map[string]string{"w": `"x"`}))
if _, err := f.NewTemplate(rows); err != nil {
t.Fatalf("NewTemplate(same-shaped choice) = %v, want an inline template exempt", err)
}
}
func TestTableInAStructTag(t *testing.T) {
f := newGenerator(t, writeFiles(t, geo()), WithSeed(1))
var v struct {
Region string `fake:"region[12].name"`
Locality string `fake:"region[12].locality.code"`
Any string `fake:"{/region[Skåne län].timezone}-x"` // the row region[12] names, by name
}
if err := f.FakeStruct(&v); err != nil {
t.Fatal(err)
}
if v.Region != "Skåne län" || regionOf[municipalityOf[v.Locality]] != "12" || v.Any != "Europe/Stockholm-x" {
t.Fatalf("FakeStruct = %+v, want the selected rows", v)
}
}
func TestTableRowsAreAlternatives(t *testing.T) {
files := map[string]string{
"cur.json": `{"format":"{code}","rows":"cur.tsv","key":"code"}`,
"cur.tsv": "code\tsym\nEUR\t€\nSEK\tkr\nUSD\t$\n",
"y.json": `{"format":"{sym}","rows":"y.tsv","key":"k"}`,
"y.tsv": "k\tsym\n1\t{/cur[SEK].sym}\n2\t{/cur[USD].sym}\n",
"country.json": `{"format":"{name} {money}","rows":"country.tsv","key":"alpha2"}`,
"country.tsv": "alpha2\tname\tmoney\nFI\tFinland\t{/cur[EUR].sym}\nSE\tSweden\t{/cur[SEK].sym}\n",
}
f := newGenerator(t, writeFiles(t, files), WithSeed(1))
if v := fake(t, f, "country[SE]"); v != "Sweden kr" {
t.Fatalf("country[SE] = %q", v)
}
if v := fake(t, f, "country[FI].money"); v != "€" {
t.Fatalf("country[FI].money = %q", v)
}
accepted := map[string]map[string]string{
"a cell reaching a table whose rows are alternatives": {"country.tsv": "alpha2\tname\tmoney\nFI\tFinland\t{/y.sym}\nSE\tSweden\t{/cur[SEK].sym}\n"},
"a cell agreeing with the row it selects": {"country.tsv": "alpha2\tname\tmoney\nFI\tFinland\t{/y[1].sym} {/cur[SEK].sym}\nSE\tSweden\t{/cur[SEK].sym}\n"},
"a selected row's cell beside the row it agrees with": {"z.json": `"{/country[SE].money} {/cur[SEK].sym}"`},
"a selected row whole beside the row it agrees with": {"z.json": `"{/country[SE]} {/cur[SEK].sym}"`},
"a column under a selected ancestor": {
"city.json": `{"format":"{name} {money}","rows":"city.tsv","key":"name","parent":"country"}`,
"city.tsv": "name\tcountry\tmoney\nHelsinki\tFI\t{/cur[EUR].sym}\nMalmö\tSE\t{/cur[SEK].sym}\nLund\tSE\t{/cur[SEK].sym}\n",
"z.json": `"{/country[SE].city.money} {/cur[SEK].sym}"`,
},
}
symbols := regexp.MustCompile(`€|kr|\$`)
for name, more := range accepted {
g, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(files, more))), WithSeed(1))
if err != nil {
t.Errorf("%s: New = %v, want it accepted", name, err)
continue
}
paths := []string{"country", "country[FI]"}
if _, defined := more["z.json"]; defined {
paths = append(paths, "z")
}
for i := 0; i < 50; i++ {
for _, path := range paths {
v := fake(t, g, path)
seen := map[string]bool{}
for _, s := range symbols.FindAllString(v, -1) {
seen[s] = true
}
if len(seen) > 1 {
t.Fatalf("%s: %s = %q, want one currency per render", name, path, v)
}
}
}
}
rejected := map[string]map[string]string{
"one cell selecting two rows": {"country.tsv": "alpha2\tname\tmoney\nFI\tFinland\t{/cur[EUR].sym}{/cur[SEK].sym}\nSE\tSweden\t{/cur[SEK].sym}\n"},
"two tables' cells in one render": {"z.json": `"{/country.money} {/y.sym}"`},
"the format beside a cell": {"country.json": `{"format":"{money} {/cur[SEK].sym}","rows":"country.tsv","key":"alpha2"}`},
"two columns of one row": {"country.json": `{"format":"{a} {b}","rows":"country.tsv","key":"alpha2"}`, "country.tsv": "alpha2\ta\tb\nFI\t{/cur[SEK].sym}\t{/cur[EUR].sym}\nSE\t{/cur[EUR].sym}\t{/cur[SEK].sym}\n"},
"a whole read beside a path": {"country.json": `{"format":"{a} {b}","rows":"country.tsv","key":"alpha2"}`, "country.tsv": "alpha2\ta\tb\nFI\t{/cur}\t{/cur[EUR].sym}\nSE\t{/cur}\t{/cur[SEK].sym}\n"},
"a cell reaching another's cells": {"y.json": `{"format":"{sym}","rows":"y.tsv","key":"k"}`, "y.tsv": "k\tsym\n1\t{/cur[SEK].sym}\n2\t{/cur[USD].sym}\n", "cur.tsv": "code\tsym\nEUR\t€\nSEK\tkr\nUSD\t$\n", "country.tsv": "alpha2\tname\tmoney\nFI\tFinland\t{/y[1].sym} {/cur[EUR].sym}\nSE\tSweden\t{/cur[SEK].sym}\n"},
}
for name, more := range rejected {
_, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(files, more))))
if err == nil || !strings.Contains(err.Error(), "drawGroup") {
t.Errorf("%s: New = %v, want it refused", name, err)
}
}
}
func TestTableOverridesByLayering(t *testing.T) {
mine := writeFiles(t, map[string]string{
"misc/country.json": `{"format":"{name}","rows":"country.tsv","key":"alpha2"}`,
"misc/country.tsv": "alpha2\tname\nXX\tNowhere\nYY\tElsewhere\n",
})
f, err := New(WithDataPath(mine), WithSeed(1))
if err != nil {
t.Fatal(err)
}
if v := fake(t, f, "misc.country[XX]"); v != "Nowhere" {
t.Fatalf("misc.country[XX] = %q, want the layered table", v)
}
if paths := f.List(); slices.Contains(paths, "misc.country.alpha3") {
t.Fatal("List() still offers the shipped table's columns under an overridden category")
}
}
func TestShippedTables(t *testing.T) {
f := newGenerator(t, "data/misc", WithSeed(1))
for path, want := range map[string]string{
"country[SE]": "Sweden",
"country[Sweden].alpha3": "SWE",
"country[SE].numeric": "752",
"country[SE].tld": ".se",
"country[SE].calling-code": "46",
"country[SE].capital": "Stockholm",
"country[SE].currency": "SEK",
"country[SE].flag": "🇸🇪",
"currency[SEK].name": "Swedish Krona",
"currency[SEK].symbol": "kr",
"currency[SEK].numeric": "752",
"currency[SEK].decimals": "2",
"currency[Euro].code": "EUR",
"language[sv]": "Swedish",
"httpstatus[404]": "404 Not Found",
"httpstatus[404].reason": "Not Found",
"mimetype[application/json].ext": ".json",
} {
if got := fake(t, f, path); got != want {
t.Errorf("Fake(%q) = %q, want %q", path, got, want)
}
}
re := map[string]*regexp.Regexp{
"country.numeric": regexp.MustCompile(`^\d{3}$`),
"country.tld": regexp.MustCompile(`^\.[a-z]{2}$`),
"country.calling-code": regexp.MustCompile(`^\d{1,4}(-\d{3})?$`),
"country.capital": regexp.MustCompile(`\p{L}`),
"country.currency": regexp.MustCompile(`^[A-Z]{3}$`),
"country.flag": regexp.MustCompile(`^[\x{1F1E6}-\x{1F1FF}]{2}$`),
"country.languages": regexp.MustCompile(`^[a-z]{2,3}(-[A-Z]{2})?(,[a-z]{2,3}(-[A-Z]{2})?)*$`),
"currency.numeric": regexp.MustCompile(`^\d{3}$`),
"currency.decimals": regexp.MustCompile(`^[0-4]$`),
}
for i := 0; i < 100; i++ {
for p, rx := range re {
if v := fake(t, f, p); !rx.MatchString(v) {
t.Fatalf("misc %s = %q, want %s", p, v, rx)
}
}
}
count := map[string]bool{}
for i := 0; i < 5000; i++ {
count[fake(t, f, "country.alpha2")] = true
}
if len(count) < 200 {
t.Fatalf("country draws %d distinct rows in 5000, want the full register", len(count))
}
}
+17 -17
View File
@@ -81,19 +81,20 @@ type builtin struct {
// for a plain field or alternation body. A '(' without a trailing ')' yields // for a plain field or alternation body. A '(' without a trailing ')' yields
// ok=false; checkFunc reports it as malformed at compile time. // ok=false; checkFunc reports it as malformed at compile time.
func funcCall(body string) (name string, args []string, ok bool) { func funcCall(body string) (name string, args []string, ok bool) {
lp := strings.IndexByte(body, '(') lp := indexOutside(body, '(')
if lp < 0 || !strings.HasSuffix(body, ")") { if lp < 0 || !strings.HasSuffix(body, ")") {
return "", nil, false return "", nil, false
} }
return body[:lp], splitArgs(body[lp+1 : len(body)-1]), true return body[:lp], splitArgs(body[lp+1 : len(body)-1]), true
} }
// splitArgs parses a function arg list: comma-separated, trimmed; empty -> none. // splitArgs parses a function arg list: comma-separated outside a selector,
// trimmed; empty -> none.
func splitArgs(s string) []string { func splitArgs(s string) []string {
if strings.TrimSpace(s) == "" { if strings.TrimSpace(s) == "" {
return nil return nil
} }
args := strings.Split(s, ",") args := splitOutside(s, ',')
for i := range args { for i := range args {
args[i] = strings.TrimSpace(args[i]) args[i] = strings.TrimSpace(args[i])
} }
@@ -132,10 +133,10 @@ func checkTokens(format string, fields map[string]node) error {
if t.kind != 'b' { if t.kind != 'b' {
return nil return nil
} }
if strings.IndexByte(t.body, '(') >= 0 { // a function token, not a field if indexOutside(t.body, '(') >= 0 { // a function token, not a field
return checkFunc(t.body, fields) return checkFunc(t.body, fields)
} }
names := strings.Split(t.body, "|") names := splitOutside(t.body, '|')
for _, name := range names { for _, name := range names {
if isRef(name) { if isRef(name) {
if _, _, err := refShape(name); err != nil { if _, _, err := refShape(name); err != nil {
@@ -187,12 +188,7 @@ func checkArm(name string, fields map[string]node, wholeToken bool) error {
// hintableRef reports whether {/name} is a reference the grammar accepts, so the // hintableRef reports whether {/name} is a reference the grammar accepts, so the
// hint never names a spelling that fails too. // hint never names a spelling that fails too.
func hintableRef(name string) bool { func hintableRef(name string) bool {
for _, seg := range strings.Split(name, ".") { return checkPathNames(name) == nil
if checkName(seg) != nil {
return false
}
}
return true
} }
// tokenOperands lists the fields one {token} body reads as operands, empty for a // tokenOperands lists the fields one {token} body reads as operands, empty for a
@@ -244,8 +240,8 @@ func checkNoRepeatedArm(body string, names []string) error {
func fieldTokens(format string) []string { func fieldTokens(format string) []string {
var names []string var names []string
_ = eachToken(format, func(t ftoken) error { _ = eachToken(format, func(t ftoken) error {
if t.kind == 'b' && strings.IndexByte(t.body, '(') < 0 { if t.kind == 'b' && indexOutside(t.body, '(') < 0 {
names = append(names, strings.Split(t.body, "|")...) names = append(names, splitOutside(t.body, '|')...)
} }
return nil return nil
}) })
@@ -279,11 +275,11 @@ func splitArm(name string, refs map[string]refBinding) arm {
} }
return pathArm(name, b.key, b.tail) return pathArm(name, b.key, b.tail)
} }
head, tail, dotted := strings.Cut(name, ".") segs, err := splitPath(name)
if !dotted { if err != nil || len(segs) == 1 {
return arm{name: name, key: name, path: name} return arm{name: name, key: name, path: name}
} }
return pathArm(name, head, strings.Split(tail, ".")) return pathArm(name, segs[0], segs[1:])
} }
func pathArm(name, key string, segs []string) arm { func pathArm(name, key string, segs []string) arm {
@@ -296,7 +292,7 @@ func pathArm(name, key string, segs []string) arm {
// splitArms splits a token body's '|' alternatives. // splitArms splits a token body's '|' alternatives.
func splitArms(body string, refs map[string]refBinding) []arm { func splitArms(body string, refs map[string]refBinding) []arm {
parts := strings.Split(body, "|") parts := splitOutside(body, '|')
arms := make([]arm, len(parts)) arms := make([]arm, len(parts))
for i, p := range parts { for i, p := range parts {
arms[i] = splitArm(p, refs) arms[i] = splitArm(p, refs)
@@ -352,6 +348,7 @@ type formatOps struct {
bound map[string]string bound map[string]string
held map[string]bool held map[string]bool
holder map[string]string holder map[string]string
heldLocal bool // some held name is kept by the expansion itself rather than the render's draws
} }
func (c *formatOps) hold(a arm, label string) { func (c *formatOps) hold(a arm, label string) {
@@ -360,6 +357,9 @@ func (c *formatOps) hold(a arm, label string) {
c.holder = map[string]string{} c.holder = map[string]string{}
} }
c.held[a.key] = true c.held[a.key] = true
if !isRef(a.key) || len(a.tail) == 0 {
c.heldLocal = true
}
if _, named := c.holder[a.key]; !named { if _, named := c.holder[a.key]; !named {
c.holder[a.key] = label c.holder[a.key] = label
} }
+26 -17
View File
@@ -58,27 +58,36 @@ misc.car.model
misc.coordinate format "{lat}, {lon}" misc.coordinate format "{lat}, {lon}"
misc.coordinate.lat string misc.coordinate.lat string
misc.coordinate.lon string misc.coordinate.lon string
misc.country misc.country format "{name}" key alpha2 name name
misc.country.alpha2 misc.country.alpha2 string
misc.country.alpha3 misc.country.alpha3 string
misc.country.name misc.country.calling-code string
misc.country.capital string
misc.country.currency string
misc.country.flag string
misc.country.languages string
misc.country.name string
misc.country.numeric string
misc.country.tld string
misc.creditcard misc.creditcard
misc.creditcard.d misc.creditcard.d
misc.currency misc.currency format "{code}" key code name name
misc.currency.code misc.currency.code string
misc.currency.name misc.currency.decimals string
misc.currency.symbol misc.currency.name string
misc.currency.numeric string
misc.currency.symbol string
misc.emoji misc.emoji
misc.httpstatus misc.httpstatus format "{code} {reason}" key code name reason
misc.httpstatus.code misc.httpstatus.code string
misc.httpstatus.reason misc.httpstatus.reason string
misc.language misc.language format "{name}" key code name name
misc.language.code misc.language.code string
misc.language.name misc.language.name string
misc.mac format "{hex(2)}:{hex(2)}:{hex(2)}:{hex(2)}:{hex(2)}:{hex(2)}" misc.mac format "{hex(2)}:{hex(2)}:{hex(2)}:{hex(2)}:{hex(2)}:{hex(2)}"
misc.mimetype misc.mimetype format "{type}" key type name ext
misc.mimetype.ext misc.mimetype.ext string
misc.mimetype.type misc.mimetype.type string
misc.objectid format "{hex(24)}" misc.objectid format "{hex(24)}"
misc.timezone misc.timezone
misc.useragent misc.useragent
+1 -1
View File
@@ -27,7 +27,7 @@ func TestTransformReadsTheHeldDraw(t *testing.T) {
func TestTransformOverAReference(t *testing.T) { func TestTransformOverAReference(t *testing.T) {
dir := writeData(t, map[string]string{ dir := writeData(t, map[string]string{
"person": `[{"format":"{first} {last}","first":"Åsa","last":"Öberg"},{"format":"{first} {last}","first":"Bo","last":"Ek"}]`, "person": `[{"format":"{first} {last}","first":"Åsa","last":"Öberg"},{"format":"{first} {last}","first":"Bo","last":"Ek","born":"1990"}]`,
"email": `"{/person.first} {/person.last} <{lowercase(ascii(/person.first))}.{lowercase(ascii(/person.last))}@example.com>"`, "email": `"{/person.first} {/person.last} <{lowercase(ascii(/person.first))}.{lowercase(ascii(/person.last))}@example.com>"`,
}) })
f := newGenerator(t, dir, WithSeed(5)) f := newGenerator(t, dir, WithSeed(5))
+23
View File
@@ -85,6 +85,10 @@ func (p *valueProof) of(n node) proven {
v = p.unite(n.items) v = p.unite(n.items)
case *template: case *template:
v = p.template(n) v = p.template(n)
case *column:
v = p.cells(n)
case *row:
v = unproven(fmt.Sprintf("%q renders a row of %s, which is composed text", n.t.format.format, n.t.category))
default: default:
v = unproven(`it reads a null, which renders "" outside its own column`) v = unproven(`it reads a null, which renders "" outside its own column`)
} }
@@ -92,6 +96,25 @@ func (p *valueProof) of(n node) proven {
return v return v
} }
// cells proves a table column over every cell it may render.
func (p *valueProof) cells(c *column) proven {
var v proven
for r := 0; r < c.t.rows(); r++ {
var w proven
if cell := c.t.cellNode(r, c.i); cell != nil {
w = p.of(cell)
} else {
w = literalValue(c.t.cell(r, c.i))
}
if r == 0 {
v = w
} else {
v = v.or(w)
}
}
return v
}
func (p *valueProof) unite(nodes []node) proven { func (p *valueProof) unite(nodes []node) proven {
v := p.of(nodes[0]) v := p.of(nodes[0])
for _, n := range nodes[1:] { for _, n := range nodes[1:] {