Compare commits
291 Commits
5576a9e05e
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 976d2e5ac1 | |||
| 343c9fa98a | |||
| f206a18027 | |||
| 3dc1cf82f1 | |||
| 0d627d2b11 | |||
| ce8ea550a3 | |||
| 1035f8bb2a | |||
| ce59653ccd | |||
| ea9c1ad436 | |||
| e09e92ff03 | |||
| 81cf04ae8b | |||
| 5715e516aa | |||
| 621e6f4548 | |||
| 839b7bd164 | |||
| d081ec85d0 | |||
| 474f73444b | |||
| edce645f34 | |||
| b581daf0ab | |||
| 815a24c186 | |||
| 878cab95e7 | |||
| e7a81459cd | |||
| 15af49e618 | |||
| b3be1ee137 | |||
| 5083b79730 | |||
| b3ead0ca5e | |||
| 78d7e513f9 | |||
| 4472c8d92b | |||
| caec22001d | |||
| e7a906debf | |||
| df8d15aeb6 | |||
| 93d216616b | |||
| c93281b8cc | |||
| f081fa1433 | |||
| baed259802 | |||
| ec6f0e2edb | |||
| d1fbc96a34 | |||
| 2016a7408c | |||
| 9feadf4d15 | |||
| ad57bf0c54 | |||
| ceb7f81a1f | |||
| daabf8c91f | |||
| af20cec9c3 | |||
| 2dbfb0a622 | |||
| 3f5f754189 | |||
| d366a80629 | |||
| d98571dfb3 | |||
| 29e471ef0f | |||
| 710b6b0261 | |||
| 85d4760cde | |||
| 784a2f0c0c | |||
| 2ad00b3569 | |||
| 8a17fb1548 | |||
| 0910ab7d1a | |||
| 604bcb1878 | |||
| 97c4e17671 | |||
| 66d11ce1c7 | |||
| b0db82f2f1 | |||
| e9d8603611 | |||
| 17d1703806 | |||
| ecc0b475ae | |||
| 69d0db9431 | |||
| 9beb42f1ca | |||
| 8432a7c4ae | |||
| 3e1d3914c2 | |||
| cddf81e6a5 | |||
| 35dc998b03 | |||
| 57b2d182c7 | |||
| fb5a5ff38f | |||
| c326683ada | |||
| 9c719c83a6 | |||
| dd913a17e3 | |||
| c8a4ffa764 | |||
| 9db8d35a49 | |||
| 5fb5e5f4c8 | |||
| c232e65001 | |||
| a851b5f5fd | |||
| 9497824350 | |||
| 1a818ff9b4 | |||
| d8bf86f0c8 | |||
| d5d9c2655e | |||
| 2d08bf4b32 | |||
| 6b0a2a60a8 | |||
| 7eb891dd80 | |||
| b6e1927468 | |||
| 93e17c1770 | |||
| 9d63943237 | |||
| bb43345d63 | |||
| c9c756e2c4 | |||
| 98938e6126 | |||
| 396ae3bcd2 | |||
| 86eb55d564 | |||
| 0c6ab0937a | |||
| 02eb95b94b | |||
| 7d50f43d0b | |||
| 2dcc31fa63 | |||
| 33c441f85a | |||
| 63f4bf7c0a | |||
| 7e25c6de44 | |||
| 7a394ee2f2 | |||
| a136be6059 | |||
| 289d5edc31 | |||
| 7ff817d5ae | |||
| b6219bf76a | |||
| e5711cbdb4 | |||
| 6adc086b7e | |||
| 8c2b51ebed | |||
| 8e30d05a0e | |||
| 8cef7a9c00 | |||
| 92f58edc2d | |||
| 9e37f3dc51 | |||
| db271f82ee | |||
| 1029b789b5 | |||
| 7993e04d95 | |||
| 8c005070ec | |||
| 42c74b9b43 | |||
| 2127b3277c | |||
| f7403f0cfb | |||
| 08d996301b | |||
| c314f9cb57 | |||
| aea7b72e4b | |||
| 1d4a0f673f | |||
| 60b9922c7e | |||
| fb0480e741 | |||
| 66cb5937cc | |||
| d8c348d07b | |||
| 1ab3bbce30 | |||
| 6b06445c32 | |||
| 7abd3e2d95 | |||
| df52566a6a | |||
| 047237f361 | |||
| fb83aa6178 | |||
| 60c102336d | |||
| 658de65490 | |||
| d98e386aea | |||
| e0aa3ac8a4 | |||
| 51b9385aee | |||
| f6b54c8ae6 | |||
| d4060e041a | |||
| 06a93bb96d | |||
| e50d3f7ffc | |||
| 2fa5f3c5ec | |||
| 16f2e96fe8 | |||
| 1cf8ed502a | |||
| ea71b3e479 | |||
| 265bd05dfc | |||
| 7bf3da807c | |||
| f9181a08b5 | |||
| b45a1edae0 | |||
| 0f645d7092 | |||
| 0443ecdf13 | |||
| c60ebb3faa | |||
| 9f4331a1f3 | |||
| a995aba4e9 | |||
| 3e535bcc43 | |||
| 68d2461704 | |||
| 53a87da9b7 | |||
| d3f430bf53 | |||
| f6b8f4fb50 | |||
| 5ed45cc55c | |||
| 246f99a319 | |||
| 4b31482a53 | |||
| 72a6ef9071 | |||
| 1e60d52e48 | |||
| 72fe01e89b | |||
| c991f35f4f | |||
| acad25708a | |||
| f781fa47d1 | |||
| bc4280561e | |||
| 2a750008bf | |||
| b4ebab5abf | |||
| 04a75f7ded | |||
| 2b2010bdd6 | |||
| 802f62a4de | |||
| 3cd3f8f317 | |||
| c212c6bef4 | |||
| e4ebfdb28d | |||
| ef00000436 | |||
| bbe6c5b1d9 | |||
| 7cb6928995 | |||
| e6bd4c5972 | |||
| a548abcc02 | |||
| c0b176a839 | |||
| d4ff3b242e | |||
| 88143153ed | |||
| 08949eb72c | |||
| 3e7127da71 | |||
| 1ad9418a76 | |||
| 8ca887aa7d | |||
| 4283af1968 | |||
| 5615ab6d89 | |||
| 770dca507a | |||
| 94dc562c6c | |||
| 044294dd90 | |||
| 7e789d470f | |||
| 5aec9f19fc | |||
| c6cd816fe7 | |||
| 3e353dece7 | |||
| 943cadfcee | |||
| 0fe7f5bb43 | |||
| 5d981a7304 | |||
| b223fcd717 | |||
| b17106afab | |||
| f75a9be5b4 | |||
| 80524a73b7 | |||
| 2310cb1e1a | |||
| 85a4cd4475 | |||
| b99526aeb8 | |||
| 79827a66d6 | |||
| 78572ccbbc | |||
| c612c9e734 | |||
| be5dfdf977 | |||
| 83e11074ac | |||
| 59159d4f5d | |||
| 7ee7f1788b | |||
| 8cb8cf6334 | |||
| 54e6865b11 | |||
| ee90337834 | |||
| d237c15d77 | |||
| 93ac610192 | |||
| 6a8aa96b20 | |||
| 314ba49b9d | |||
| 7383e8d0ca | |||
| ca40fe0c2c | |||
| 19aae8bbfe | |||
| 2d28220869 | |||
| cc15bdd1d9 | |||
| 466ed5a4d9 | |||
| ce22c83e7d | |||
| 0d2a080b85 | |||
| 296058fb80 | |||
| 44601c90ab | |||
| 8ed8d9bb51 | |||
| 1cd422352b | |||
| 0cdbdb7bf4 | |||
| d8048df169 | |||
| 71cccaf4c9 | |||
| 704d18d109 | |||
| 44c91668f9 | |||
| 6916894ab5 | |||
| 80e8d72bb7 | |||
| 7c7bee5cd9 | |||
| 841513bb27 | |||
| 873b7678ba | |||
| 4d58c93757 | |||
| 305d8f8edd | |||
| d8ece597c8 | |||
| 47ec883bb0 | |||
| 031a3ec7d6 | |||
| e08435afa7 | |||
| 3a9f3a4758 | |||
| 4399b8b89f | |||
| 3bc3e35045 | |||
| 5ba38a8281 | |||
| addba9abf7 | |||
| a9552ed966 | |||
| c5304bb6e7 | |||
| 08c7ef9b7f | |||
| ae0527a26e | |||
| 4f9285ab5c | |||
| 7b28dc36d9 | |||
| 3d3efbb642 | |||
| 5a75df8034 | |||
| fdbbf94d2c | |||
| 512696722d | |||
| 1b8768e431 | |||
| f9647ff6bb | |||
| bcbf044290 | |||
| 7581587a19 | |||
| 1bf5470c6f | |||
| 5d1e567b8d | |||
| 1b1b93b35f | |||
| 34c933b2b3 | |||
| c5c06375d8 | |||
| 5514b42b7b | |||
| 88e71e666d | |||
| 0ad88028c4 | |||
| f6782cf477 | |||
| 8cd603a2b1 | |||
| 00a6160aa4 | |||
| 035e51d3e1 | |||
| 31c2f12098 | |||
| 8fa33694b9 | |||
| f0305f191f | |||
| 054f99c94f | |||
| de8c1d13e9 | |||
| ff4b243148 | |||
| 99d8aa0fb7 | |||
| f210c7a764 | |||
| 91cbae2a44 | |||
| cf9de21495 | |||
| f773738243 |
@@ -16,12 +16,54 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- name: Relevant changes
|
||||
id: changes
|
||||
run: |
|
||||
if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
|
||||
git fetch --no-tags origin "$GITHUB_BASE_REF"
|
||||
base="origin/$GITHUB_BASE_REF"
|
||||
else
|
||||
base='${{ github.event.before }}'
|
||||
fi
|
||||
if git diff --quiet "$base"...HEAD -- '*.go' go.mod go.sum Dockerfile .dockerignore .github data testdata README.md; then
|
||||
echo "code=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "code=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "base=$base" >> "$GITHUB_OUTPUT"
|
||||
- name: CHANGELOG entry for data changes
|
||||
run: |
|
||||
base='${{ steps.changes.outputs.base }}'
|
||||
if ! git diff --quiet "$base"...HEAD -- data testdata/shipped_shape.txt && git diff --quiet "$base"...HEAD -- CHANGELOG.md; then
|
||||
echo "::error::the shipped data changed without a CHANGELOG.md entry (see the README's Versioning)"
|
||||
exit 1
|
||||
fi
|
||||
# `docker build` streams the context to the daemon. A compose bind-mount of
|
||||
# `.` mounts an empty host dir instead, because the job is itself a container.
|
||||
- name: Latest supported Go
|
||||
if: ${{ steps.changes.outputs.code == 'true' }}
|
||||
run: docker build .
|
||||
# Runs even when the step above failed, so a red build says whether the
|
||||
# failure is version-specific.
|
||||
- name: Lowest supported Go
|
||||
if: ${{ !cancelled() }}
|
||||
if: ${{ !cancelled() && steps.changes.outputs.code == 'true' }}
|
||||
run: docker build --build-arg GO_VERSION=1.22.12 .
|
||||
|
||||
release:
|
||||
name: Gitea release from CHANGELOG.md
|
||||
needs: test
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- run: python3 release-tooling/publish_release.py
|
||||
env:
|
||||
GITEA_API_URL: ${{ github.api_url }}
|
||||
GITEA_REPOSITORY: ${{ github.repository }}
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SHA: ${{ github.sha }}
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
.claude
|
||||
*.out
|
||||
todo.md
|
||||
__pycache__/
|
||||
data-import/cache/
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Rules
|
||||
|
||||
- Go runs only through `docker compose run --rm <test|vet|fmt|build|cyclo>`; the merge gate is `docker build .` plus CI's changelog check.
|
||||
- Tests first, in their own commit; the implementation follows in the next. A re-pin of seeded output or of `testdata/shipped_shape.txt` is its own commit.
|
||||
- A change under `data/` or to the shape pin adds its `CHANGELOG.md` entry under `Unreleased` in the same PR, and so does a change to a flag, an exit code, an exported name, a fence, a builtin or the lowest Go; what is major is the README's Versioning table.
|
||||
- One-line commit messages: no ticket prefix, no repo name, no authorship trailers.
|
||||
- Merges are fast-forward only, which `tea pr merge` cannot spell — every style it
|
||||
offers fails with "is it still open?", as does a merge whose required checks are
|
||||
still pending, so read the checks before believing the style is the problem. Use
|
||||
`tea api /repos/{owner}/{repo}/pulls/<n>/merge -f Do=fast-forward-only`.
|
||||
- Gitea queues Actions runs that `/repos/{owner}/{repo}/actions/tasks` does not
|
||||
list, so an empty task list says nothing about whether CI ran; read
|
||||
`/repos/{owner}/{repo}/commits/<sha>/status`.
|
||||
- README goal 2 is gated at 7.0 on the `comprehension-panel` skill, and below it
|
||||
nothing else merges — no feature, no category, no data — bar a security fix and a
|
||||
dependency bump. `todo.md` carries the round: the nine-seat findings run at depth 1,
|
||||
a PR per item it names, then the run again, until the score passes. At or above 7.0
|
||||
every pull request scores with the four-seat run and answers it in one run. No merge
|
||||
lowers the last score.
|
||||
- Hard tabs. No comment by default; delete a restatement, a rationale, history, or a file preamble.
|
||||
- One spelling per result: reject the other at `New`, and let the error name the spelling to use.
|
||||
- A standing choice a reader would relitigate goes under Decisions in the README, not in a comment.
|
||||
- Prose naming a source states what that source states: read the register's own field
|
||||
before paraphrasing it, and name the register you read before claiming none
|
||||
publishes a spelling.
|
||||
- A README example is a `json` block that loads and renders as a category; `readme_test.go` runs every one.
|
||||
- Cyclomatic complexity is gated at 14: the table-shaped dispatches (`linkParent`, `compileTemplate`, `renderEdges`) sit at it and stay whole; a function that would pass it is decomposed.
|
||||
@@ -0,0 +1,92 @@
|
||||
# Changelog
|
||||
|
||||
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). A release's
|
||||
`Breaking` section comes first and names each rejected spelling with its
|
||||
replacement, and each removed path, column or flag.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- 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.territory[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. A `name` without a `key` resolves
|
||||
inside the table's `parent`. Refused at `New`: a `name` without a `key` or a
|
||||
`parent`, a name repeating inside one parent row, 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.territory`, `misc.currency`, `misc.language`, `misc.httpstatus` and
|
||||
`misc.mimetype` are tables. `misc.territory` is every ISO 3166-1 territory that has
|
||||
a capital, a currency and a TLD, with the columns `calling-code`, `capital`,
|
||||
`country`, `currency`, `flag`, `languages`, `numeric` and `tld` added, `country`
|
||||
holding the alpha-2 code of the sovereign state it belongs to and its own where it
|
||||
is one; `misc.currency` the
|
||||
current ISO 4217 currencies with a minor unit, with `decimals` and `numeric`
|
||||
added, and its symbols from CLDR; `misc.language` every ISO 639-2 entry carrying a
|
||||
639-1 code, with a `code3` column holding its 639-2/T code; `misc.httpstatus` each
|
||||
code the IANA registry lists with a plain reason phrase; and `misc.mimetype` each
|
||||
type mime-db records as IANA-registered and gives a filename extension.
|
||||
`DATA-LICENSES.md` lists each source.
|
||||
- `misc.timezone`, `misc.car` and `misc.useragent` are tables too. `misc.timezone`
|
||||
is every zone tzdb `zone.tab` gives a shipped territory, with `offset` holding the
|
||||
zone's standard UTC offset and `territory` linking to `misc.territory`, so
|
||||
`misc.territory[SE].timezone` draws `Europe/Stockholm`; zones of a territory
|
||||
`misc.territory` does not ship, Antarctica's among them, and the constant `UTC`
|
||||
are gone; spell that one as the text `"UTC"`. A `weight` column carries the
|
||||
population GeoNames records in each zone, so a draw favours the zones people live
|
||||
in. `misc.useragent` is the top-user-agents desktop and mobile lists with `browser`,
|
||||
`device` and `os` columns. `misc.car` keeps its makes and models in a `make` and a
|
||||
`model` column.
|
||||
- `misc.httpmethod`, `misc.protocol` and `misc.port` are IANA's registries as tables.
|
||||
`misc.httpmethod` is the eight methods RFC 9110 defines and PATCH, carrying
|
||||
`safe` and `idempotent` as `true` or `false`; `misc.protocol` the keyword, its
|
||||
`number` and its `name`, selectable by either; `misc.port` renders a TCP port number,
|
||||
with the IANA `service` name beside it, keeping the first service the registry
|
||||
describes for a port so a number selects one row.
|
||||
- `misc.tld` is the IANA root zone as a table: every delegated TLD keyed with its
|
||||
leading dot, `.se` and `.xn--p1ai`, the register's `type` beside it, and a `unicode`
|
||||
column holding the form the register displays, so `misc.tld[.рф]` selects the row
|
||||
`.xn--p1ai` renders. A delegation the register marks "Not assigned" does not ship.
|
||||
- `misc.loglevel` is the eight syslog severities: the `keyword` a configuration writes,
|
||||
`err` and `info`, keyed by the numerical `code` a PRI encodes and selectable by
|
||||
either, with the `severity` RFC 5424 spells beside it.
|
||||
- `geo.SE` and `geo.US`: five linked tables per country, `region`, `municipality`,
|
||||
`locality`, `postal-code` and `street`, weighted by population and address counts
|
||||
and built from SCB, GeoNames, Trafikverket NVDB and the US Census Bureau, and an
|
||||
`address` record over one consistent draw of them. `sv_SE.address` and
|
||||
`en_US.address` read those records, so `en_US.address.street` no longer carries
|
||||
`name` and `suffix`, and a locale folder loads only beside `geo`.
|
||||
- `{date(from,to,'layout')}` and `{time('layout')}`: a second between two days, or
|
||||
within one, in a single-quoted Go layout, drawn in UTC; `from` may equal `to`.
|
||||
`sv_SE.date`, `en_US.date`, `sv_SE.time` and `en_US.time` render through them, so
|
||||
`date.year`, `date.month`, `date.day`, `time.hour`, `time.minute`, `time.minute.t`,
|
||||
`time.sec` and `time.ampm` are no longer paths — a part of a date is now its own
|
||||
`{date(…,'2006')}`. `misc.datetime` is an RFC 3339 instant.
|
||||
- `sex`, `first-name` and `last-name` tables in `sv_SE` and `en_US`, weighted by
|
||||
bearers from SCB, the SSA and the Census Bureau; `first-name` links to `sex`, and a
|
||||
name both sexes carry is a row under each. `person` reads them, so its columns are
|
||||
`first`, `last`, `prefix` and `sex`; `person.femalefirst` and `person.malefirst` are
|
||||
no longer paths — draw `sex[f].first-name` and `sex[m].first-name` instead.
|
||||
`en_US.title` links to `sex` as well, so `en_US.person.prefix` draws `Mr` or `Ms`
|
||||
without contradicting the record's `sex`; `sv_SE.title` is an unsexed table of the
|
||||
same shape.
|
||||
- `sv_SE.personnummer` and `sv_SE.samordningsnummer`, Skatteverket's test series
|
||||
from a `sv_SE.birth-number` table under `sex`, in place of `sv_SE.ssn`, whose
|
||||
`ssn.mmdd`, `ssn.mmdd.m` and `ssn.mmdd.d` go with it. `en_US.ssn` now draws the
|
||||
ranges the SSA assigns and carries the columns `area`, `group` and `serial`, so
|
||||
`--format csv en_US.ssn` writes a header where it used to fail; `en_US.itin` is new
|
||||
and carries the same three. `person.prefix` is null where a person has no title,
|
||||
where it used to be an empty string, so `--format sql` writes `NULL` and a `string`
|
||||
struct field reading it becomes `*string`.
|
||||
- `ErrNoColumns` is exported, so a caller can tell the one record fence a path can
|
||||
answer from the rest.
|
||||
- An error names a spelling that runs: a layout is named single-quoted and free of
|
||||
its own quotes, a row of a table with no key is named as the path that selects it,
|
||||
`sv_SE.sex[f].first-name[Kim]`, and a category with no columns names the record
|
||||
that gives it one.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Data licenses
|
||||
|
||||
Every shipped dataset, its source, its licence and the attribution it asks for. A
|
||||
`data-import/` script rebuilds each sourced table, run as the README's
|
||||
[Development](README.md#development) section says; a curated one is hand-written.
|
||||
|
||||
| Table | Source | Licence | Attribution | Rebuild |
|
||||
|-------|--------|---------|-------------|---------|
|
||||
| `geo/SE/locality.tsv`, `postal-code.tsv` | [GeoNames](https://www.geonames.org/) postal codes for SE; populations from SCB tätorter 2023 | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/); CC0 1.0 | "Postal codes from GeoNames, www.geonames.org" | `data-import/geo-se.py` |
|
||||
| `geo/SE/region.tsv`, `municipality.tsv` | [SCB](https://www.scb.se/) län and kommun codes 2026 and population 2024 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | none required | `data-import/geo-se.py` |
|
||||
| `geo/SE/street.tsv` | [Trafikverket NVDB](https://www.trafikverket.se/) Gatunamn, through the open API | CC0 1.0 | none required | `data-import/geo-se.py` |
|
||||
| `geo/US/region.tsv`, `municipality.tsv`, `locality.tsv` | [Census Bureau](https://www.census.gov/) Gazetteer 2026 and population estimates 2025 | [public domain](https://www.usa.gov/government-works) | none required | `data-import/geo-us.py` |
|
||||
| `geo/US/postal-code.tsv`, `street.tsv` | Census Bureau ZCTA to place relationships 2020 and TIGER/Line 2025 address ranges and feature names | public domain | none required | `data-import/geo-us.py` |
|
||||
| `sv_SE/first-name.tsv`, `last-name.tsv` | [SCB](https://www.scb.se/) names with at least two bearers, 31 December 2022 | CC0 1.0 | "Källa: SCB" | `data-import/names-se.py` |
|
||||
| `en_US/first-name.tsv` | [SSA](https://www.ssa.gov/oact/babynames/) baby names, births 1930 to 2020, through [hackerb9/ssa-baby-names](https://github.com/hackerb9/ssa-baby-names) | public domain | none required | `data-import/names-us.py` |
|
||||
| `en_US/last-name.tsv` | Census Bureau surnames occurring 100 or more times, 2010 | public domain | none required | `data-import/names-us.py` |
|
||||
| `sv_SE/sex.tsv`, `sv_SE/birth-number.tsv`, `sv_SE/title.tsv`, `en_US/sex.tsv`, `en_US/title.tsv`, `misc/car.tsv` | curated; Skatteverket's test birth numbers are facts, and `car.tsv` waits for an international source (goal 12) | — | — | — |
|
||||
| `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/httpmethod.tsv` | [IANA HTTP Method Registry](https://www.iana.org/assignments/http-methods/) | [public domain](https://www.iana.org/help/licensing-terms) | none required | `data-import/httpmethod.py` |
|
||||
| `misc/httpstatus.tsv` | [IANA HTTP Status Code Registry](https://www.iana.org/assignments/http-status-codes/) | [public domain](https://www.iana.org/help/licensing-terms) | none required | `data-import/httpstatus.py` |
|
||||
| `misc/language.tsv` | [datasets/language-codes](https://github.com/datasets/language-codes), the [Library of Congress](https://www.loc.gov/standards/iso639-2/) ISO 639-2 register | [PDDL 1.0](https://opendatacommons.org/licenses/pddl/1-0/) | none required | `data-import/language.py` |
|
||||
| `misc/loglevel.tsv` | [RFC 5424](https://www.rfc-editor.org/rfc/rfc5424.txt) Table 2, the syslog message severities; keywords from the [POSIX](https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/syslog.h.html) `<syslog.h>` severity macros | facts | none required | `data-import/loglevel.py` |
|
||||
| `misc/mimetype.tsv` | [mime-db](https://github.com/jshttp/mime-db), the IANA media type registry with filename extensions | [MIT](https://github.com/jshttp/mime-db/blob/master/LICENSE) | "Copyright (c) 2014 Jonathan Ong, Copyright (c) 2015-2022 Douglas Christopher Wilson" | `data-import/mimetype.py` |
|
||||
| `misc/port.tsv` | [IANA Service Name and Transport Protocol Port Number Registry](https://www.iana.org/assignments/service-names-port-numbers/) | [public domain](https://www.iana.org/help/licensing-terms) | none required | `data-import/port.py` |
|
||||
| `misc/protocol.tsv` | [IANA Protocol Numbers](https://www.iana.org/assignments/protocol-numbers/) | [public domain](https://www.iana.org/help/licensing-terms) | none required | `data-import/protocol.py` |
|
||||
| `misc/territory.tsv` | [datasets/country-codes](https://github.com/datasets/country-codes) | [PDDL 1.0](https://opendatacommons.org/licenses/pddl/1-0/) | none required | `data-import/territory.py` |
|
||||
| `misc/timezone.tsv` | [IANA tzdb](https://www.iana.org/time-zones) 2026d `zone.tab` and the standard offset of each zone; weights from [GeoNames](https://www.geonames.org/) `cities15000` populations | [public domain](https://data.iana.org/time-zones/tzdb/LICENSE); [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | "Populations from GeoNames, www.geonames.org" | `data-import/timezone.py` |
|
||||
| `misc/tld.tsv` | [IANA Root Zone Database](https://www.iana.org/domains/root/db), checked against the root zone's [TLD list](https://data.iana.org/TLD/tlds-alpha-by-domain.txt) | [public domain](https://www.iana.org/help/licensing-terms) | none required | `data-import/tld.py` |
|
||||
| `misc/useragent.tsv` | [top-user-agents](https://github.com/microlinkhq/top-user-agents) desktop and mobile lists | [MIT](https://github.com/microlinkhq/top-user-agents/blob/master/LICENSE.md) | "Copyright © 2020 Kiko Beats" | `data-import/useragent.py` |
|
||||
|
||||
Every other category is hand-written under [`data/`](data), MIT like the code.
|
||||
+2
-1
@@ -13,6 +13,7 @@ RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN go vet ./... && \
|
||||
go run github.com/fzipp/gocyclo/cmd/gocyclo@v0.6.0 -over 14 -ignore _test . && \
|
||||
{ unformatted="$(gofmt -l .)"; test -z "$unformatted" || \
|
||||
{ echo "unformatted files:"; echo "$unformatted"; exit 1; }; } && \
|
||||
go test ./...
|
||||
go test -race ./...
|
||||
|
||||
+13
-19
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
func benchGenerator(b *testing.B, dir string) *Generator {
|
||||
b.Helper()
|
||||
f, err := New([]string{dir}, WithSeed(1))
|
||||
f, err := New(WithoutShippedData(), WithDataPath(dir), WithSeed(1))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -26,12 +26,12 @@ func benchPath(b *testing.B, dir, path string) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPerson(b *testing.B) { benchPath(b, "data/sv_SE", "person") }
|
||||
func BenchmarkAddress(b *testing.B) { benchPath(b, "data/sv_SE", "address") }
|
||||
func BenchmarkWord(b *testing.B) { benchPath(b, "data/sv_SE", "word") }
|
||||
func BenchmarkCreditcard(b *testing.B) { benchPath(b, "data/misc", "creditcard") }
|
||||
func BenchmarkSSN(b *testing.B) { benchPath(b, "data/sv_SE", "ssn") }
|
||||
func BenchmarkUUIDv7(b *testing.B) { benchPath(b, "data/misc", "uuid") }
|
||||
func BenchmarkPerson(b *testing.B) { benchPath(b, "data", "sv_SE.person") }
|
||||
func BenchmarkAddress(b *testing.B) { benchPath(b, "data", "sv_SE.address") }
|
||||
func BenchmarkWord(b *testing.B) { benchPath(b, "data", "sv_SE.word") }
|
||||
func BenchmarkCreditcard(b *testing.B) { benchPath(b, "data", "misc.creditcard") }
|
||||
func BenchmarkPersonnummer(b *testing.B) { benchPath(b, "data", "sv_SE.personnummer") }
|
||||
func BenchmarkUUIDv7(b *testing.B) { benchPath(b, "data", "misc.uuid") }
|
||||
|
||||
func tmpData(b *testing.B, name, body string) string {
|
||||
b.Helper()
|
||||
@@ -43,12 +43,12 @@ func tmpData(b *testing.B, name, body string) string {
|
||||
}
|
||||
|
||||
func BenchmarkCalc(b *testing.B) {
|
||||
dir := tmpData(b, "inv", `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":["19.99"],"qty":["3"]}`)
|
||||
dir := tmpData(b, "inv", `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":"19.99","qty":"3"}`)
|
||||
benchPath(b, dir, "inv")
|
||||
}
|
||||
|
||||
func BenchmarkLongLiteral(b *testing.B) {
|
||||
dir := tmpData(b, "sql", `{"format":"INSERT INTO customers (id, name, city) V#ALUES (#1#2#3, '{word}', '{word}');","word":["alpha","beta","gamma","delta"]}`)
|
||||
dir := tmpData(b, "sql", `{"format":"INSERT INTO customers (id, name, city) VALUES (123, '{word}', '{word}');","word":["alpha","beta","gamma","delta"]}`)
|
||||
benchPath(b, dir, "sql")
|
||||
}
|
||||
|
||||
@@ -61,24 +61,19 @@ func BenchmarkRepeat(b *testing.B) {
|
||||
// what a correlated pair costs against BenchmarkUnbound, the same output drawn from
|
||||
// two independent fields.
|
||||
func BenchmarkBound(b *testing.B) {
|
||||
dir := tmpData(b, "addr", `{"format":"{place.postal-code} {place.locality}","place":[
|
||||
{"format":"{locality}","locality":"Stockholm","postal-code":{"format":"#100 00"}},
|
||||
{"format":"{locality}","locality":"Tranås","postal-code":{"format":"#5#7#3 00"}}]}`)
|
||||
dir := tmpData(b, "addr", `{"format":"{place.postal-code} {place.locality}","place":[{"format":"{locality}","locality":"Stockholm","postal-code":"1{digits(2)} {digits(2)}"},{"format":"{locality}","locality":"Tranås","postal-code":"573 {digits(2)}"}]}`)
|
||||
benchPath(b, dir, "addr")
|
||||
}
|
||||
|
||||
func BenchmarkUnbound(b *testing.B) {
|
||||
dir := tmpData(b, "addr", `{"format":"{postal-code} {locality}",
|
||||
"postal-code":[{"format":"#100 00"},{"format":"#5#7#3 00"}],
|
||||
"locality":["Stockholm","Tranås"]}`)
|
||||
dir := tmpData(b, "addr", `{"format":"{postal-code} {locality}","postal-code":["1{digits(2)} {digits(2)}","573 {digits(2)}"],"locality":["Stockholm","Tranås"]}`)
|
||||
benchPath(b, dir, "addr")
|
||||
}
|
||||
|
||||
// BenchmarkBoundDeep reads two paths through an intermediate level, the shape the
|
||||
// depth question is about.
|
||||
func BenchmarkBoundDeep(b *testing.B) {
|
||||
dir := tmpData(b, "addr", `{"format":"{p.addr.city} {p.addr.zip}","p":[
|
||||
{"format":"{addr}","addr":{"format":"{city}","city":["Stockholm","Tranås"],"zip":{"format":"#100 00"}}}]}`)
|
||||
dir := tmpData(b, "addr", `{"format":"{p.addr.city} {p.addr.zip}","p":{"format":"{addr}","addr":{"format":"{city}","city":["Stockholm","Tranås"],"zip":"1{digits(2)} {digits(2)}"}}}`)
|
||||
benchPath(b, dir, "addr")
|
||||
}
|
||||
|
||||
@@ -91,11 +86,10 @@ func BenchmarkBoundWide(b *testing.B) {
|
||||
benchPath(b, dir, "row")
|
||||
}
|
||||
|
||||
// BenchmarkNew measures load+compile+validate of the whole shipped tree.
|
||||
func BenchmarkNew(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := New([]string{"data"}, WithSeed(1)); err != nil {
|
||||
if _, err := New(WithSeed(1)); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
+99
-150
@@ -2,41 +2,39 @@ package fejkdata
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// maxLen caps sample output lengths (hex, nanoid, base64) and maxDecimals caps
|
||||
// float/calc decimal places, so a fat-fingered or overflowing argument fails at
|
||||
// New instead of trying to allocate gigabytes — or panicking — at render.
|
||||
// maxLen caps sample output lengths (hex, nanoid, base64, digits, upper, lower)
|
||||
// and maxDecimals float/calc decimal places, so a fat-fingered or overflowing
|
||||
// argument fails at New instead of trying to allocate gigabytes — or panicking —
|
||||
// at render.
|
||||
const (
|
||||
maxLen = 1 << 20 // 1,048,576 chars/bytes
|
||||
maxLen = 1 << 20
|
||||
maxDecimals = 1024
|
||||
)
|
||||
|
||||
// builtins is the registry of {name(args)} functions. Derivations read the
|
||||
// digits emitted so far in the current expansion (luhn, mod11, ean — place them
|
||||
// after their payload); samples read only the rng (uuid, ulid, ...). All
|
||||
// must stay pure over (rng, emitted, args) so seeded output is reproducible — a
|
||||
// time-based id (uuid v7, ulid) draws its timestamp from the rng, not the wall
|
||||
// clock. Add a builtin only for what data can't express: a random v4 UUID and a
|
||||
// 24-hex ObjectID both ship as data, so the uuid builtin is v7.
|
||||
// builtins is the registry of {name(args)} functions. Derivations read the digits
|
||||
// emitted so far in the current expansion (place them after their payload);
|
||||
// samples read only the rng. A time-based id (uuid v7, ulid) draws its timestamp
|
||||
// from the rng, not the wall clock, so seeded output stays reproducible.
|
||||
var builtins = map[string]builtin{
|
||||
"luhn": {arity: 0, prep: derive(func(e string) string { return string(rune('0' + luhnCheck(e))) })},
|
||||
"mod11": {arity: 0, prep: derive(mod11Check)},
|
||||
"ean": {arity: 0, prep: derive(eanCheck)},
|
||||
"uuid": {arity: 0, prep: sample(uuidV7)},
|
||||
"ulid": {arity: 0, prep: sample(ulid)},
|
||||
"nanoid": {arity: 1, check: posIntArg, prep: func(a []string) callFn {
|
||||
n := atoi(a[0])
|
||||
return func(s *session, _ string, _ []string) string { return nanoid(s, n) }
|
||||
}},
|
||||
"hex": {arity: 1, check: posIntArg, prep: func(a []string) callFn {
|
||||
n := atoi(a[0])
|
||||
return func(s *session, _ string, _ []string) string { return randHex(s, n) }
|
||||
"nanoid": {arity: 1, check: posIntArg, prep: chars(nanoidAlphabet)},
|
||||
"hex": {arity: 1, check: posIntArg, prep: chars(hexDigits)},
|
||||
"digits": {arity: 1, check: posIntArg, prep: chars("0123456789"), number: func(token string, a []string) proven {
|
||||
return printing(token, DataTypeString, bounded(0, math.Pow(10, float64(atoi(a[0])))-1, true))
|
||||
}},
|
||||
"upper": {arity: 1, check: posIntArg, prep: chars("ABCDEFGHIJKLMNOPQRSTUVWXYZ")},
|
||||
"lower": {arity: 1, check: posIntArg, prep: chars("abcdefghijklmnopqrstuvwxyz")},
|
||||
"base64": {arity: 1, check: posIntArg, prep: func(a []string) callFn {
|
||||
n := atoi(a[0])
|
||||
return func(s *session, _ string, _ []string) string {
|
||||
@@ -46,20 +44,27 @@ var builtins = map[string]builtin{
|
||||
"int": {arity: 2, check: intRangeArgs, prep: func(a []string) callFn {
|
||||
lo, span := atoi(a[0]), atoi(a[1])-atoi(a[0])+1
|
||||
return func(s *session, _ string, _ []string) string { return strconv.Itoa(lo + s.IntN(span)) }
|
||||
}, number: func(token string, a []string) proven {
|
||||
return printing(token, DataTypeInteger, bounded(float64(atoi(a[0])), float64(atoi(a[1])), true))
|
||||
}},
|
||||
"float": {arity: 3, check: floatArgs, prep: func(a []string) callFn {
|
||||
lo, hi, dp := atof(a[0]), atof(a[1]), atoi(a[2])
|
||||
return func(s *session, _ string, _ []string) string {
|
||||
return strconv.FormatFloat(lo+s.Float64()*(hi-lo), 'f', dp, 64)
|
||||
return formatFloat(lo+s.Float64()*(hi-lo), dp)
|
||||
}
|
||||
}, number: func(token string, a []string) proven {
|
||||
return printedNumber(token, bounded(atof(a[0]), atof(a[1]), false), atoi(a[2]))
|
||||
}},
|
||||
"iban": {arity: 1, check: ibanArg, prep: func(a []string) callFn {
|
||||
cc := a[0]
|
||||
return func(s *session, _ string, _ []string) string { return iban(s, cc) }
|
||||
}},
|
||||
// calc is the one builtin that names operands (expand reads them for it);
|
||||
// every other ignores them. 1 or 2 args: the expression and an optional decimals.
|
||||
"calc": {arity: -1, check: checkCalc, prep: calcPrep},
|
||||
"date": {arity: -1, check: dateArgs, prep: datePrep},
|
||||
"time": {arity: -1, check: timeArg, prep: timePrep},
|
||||
"calc": {arity: -1, check: checkCalc, prep: calcPrep, operands: calcOperands},
|
||||
"lowercase": {arity: 1, check: transformArg, prep: transformPrep(strings.ToLower), operands: transformOperand},
|
||||
"uppercase": {arity: 1, check: transformArg, prep: transformPrep(strings.ToUpper), operands: transformOperand},
|
||||
"ascii": {arity: 1, check: transformArg, prep: transformPrep(asciiFold), operands: transformOperand},
|
||||
// seq is the one stateful builtin: a per-session counter from 1, advancing on
|
||||
// each call. An optional name selects an independent counter; no name uses the
|
||||
// default one. Deterministic by construction, so seeded output stays stable.
|
||||
@@ -71,26 +76,42 @@ var builtins = map[string]builtin{
|
||||
return func(s *session, _ string, _ []string) string {
|
||||
return strconv.FormatUint(s.next(key), 10)
|
||||
}
|
||||
}, number: func(token string, _ []string) proven {
|
||||
return printing(token, DataTypeInteger, bounded(1, math.MaxInt64, true))
|
||||
}},
|
||||
}
|
||||
|
||||
// derive and sample are the two argument-free builtin shapes the README names: a
|
||||
// derivation reads the output emitted so far, a sample reads only the rng. Each
|
||||
// lifts that one function into the prep every registry entry supplies.
|
||||
// derive and sample are the two argument-free builtin shapes: a derivation reads
|
||||
// the output emitted so far, a sample reads only the rng. chars is the shape of a
|
||||
// sample of n characters drawn from an alphabet.
|
||||
func derive(f func(emitted string) string) func([]string) callFn {
|
||||
return func([]string) callFn {
|
||||
return func(_ *session, emitted string, _ []string) string { return f(emitted) }
|
||||
}
|
||||
}
|
||||
|
||||
func sample(f func(rng) string) func([]string) callFn {
|
||||
return func([]string) callFn {
|
||||
return func(s *session, _ string, _ []string) string { return f(s) }
|
||||
}
|
||||
}
|
||||
func chars(alphabet string) func([]string) callFn {
|
||||
return func(a []string) callFn {
|
||||
n := atoi(a[0])
|
||||
return func(s *session, _ string, _ []string) string { return randChars(s, n, alphabet) }
|
||||
}
|
||||
}
|
||||
|
||||
const hexDigits = "0123456789abcdef"
|
||||
|
||||
// formatFloat prints v to dp decimals, -1 for the shortest form, and a zero unsigned.
|
||||
func formatFloat(v float64, dp int) string {
|
||||
s := strconv.FormatFloat(v, 'f', dp, 64)
|
||||
if strings.HasPrefix(s, "-") && strings.Trim(s, "-0.") == "" {
|
||||
return s[1:]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// atoi parses an arg a builtin's check already validated. It panics rather than
|
||||
// returning zero, so a check that stops covering its own args is a stack trace and
|
||||
// not a silently wrong length, range or decimal count.
|
||||
@@ -110,7 +131,6 @@ func atof(s string) float64 {
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func randBytes(r rng, n int) []byte {
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
@@ -118,63 +138,94 @@ func randBytes(r rng, n int) []byte {
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func randHex(r rng, n int) string {
|
||||
func randChars(r rng, n int, alphabet string) string {
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = hexDigits[r.IntN(16)]
|
||||
b[i] = alphabet[r.IntN(len(alphabet))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// plainInt parses an integer arg written the one way: no sign, no leading zero.
|
||||
func plainInt(s string) (int, error) {
|
||||
n, err := strconv.Atoi(s)
|
||||
if errors.Is(err, strconv.ErrRange) {
|
||||
return 0, fmt.Errorf("%q is past the integer range: %w", s, err)
|
||||
}
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%q is not an integer", s)
|
||||
}
|
||||
if strconv.Itoa(n) != s {
|
||||
return 0, fmt.Errorf("%q is not a plain integer; write %d", s, n)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
func posIntArg(_ map[string]node, a []string) error {
|
||||
n, err := strconv.Atoi(a[0])
|
||||
if err != nil || n < 1 {
|
||||
return fmt.Errorf("count %q must be a positive integer", a[0])
|
||||
n, err := plainInt(a[0])
|
||||
if errors.Is(err, strconv.ErrRange) {
|
||||
return fmt.Errorf("count %q exceeds the maximum %d", a[0], maxLen)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("count %w", err)
|
||||
}
|
||||
if n < 1 {
|
||||
return fmt.Errorf("count %q must be positive", a[0])
|
||||
}
|
||||
if n > maxLen {
|
||||
return fmt.Errorf("count %d exceeds the maximum %d", n, maxLen)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func intRangeArgs(_ map[string]node, a []string) error {
|
||||
lo, e1 := strconv.Atoi(a[0])
|
||||
hi, e2 := strconv.Atoi(a[1])
|
||||
if e1 != nil || e2 != nil {
|
||||
return fmt.Errorf("int(min,max) needs integer args, got %q,%q", a[0], a[1])
|
||||
lo, err := plainInt(a[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("int(min,max): min %w", err)
|
||||
}
|
||||
hi, err := plainInt(a[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("int(min,max): max %w", err)
|
||||
}
|
||||
if lo > hi {
|
||||
return fmt.Errorf("int(min,max): min %d > max %d", lo, hi)
|
||||
}
|
||||
if lo == hi {
|
||||
return fmt.Errorf("int(%d,%d) is the constant %d; write it as text", lo, hi, lo)
|
||||
}
|
||||
if uint64(hi)-uint64(lo) >= uint64(math.MaxInt64) { // span hi-lo+1 would overflow int -> IntN panic
|
||||
return fmt.Errorf("int(min,max): range %d..%d is too wide", lo, hi)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func floatArgs(_ map[string]node, a []string) error {
|
||||
lo, e1 := strconv.ParseFloat(a[0], 64)
|
||||
hi, e2 := strconv.ParseFloat(a[1], 64)
|
||||
dp, e3 := strconv.Atoi(a[2])
|
||||
if e1 != nil || e2 != nil || e3 != nil {
|
||||
return fmt.Errorf("float(min,max,dp) needs numeric args, got %q,%q,%q", a[0], a[1], a[2])
|
||||
if e1 != nil || e2 != nil {
|
||||
return fmt.Errorf("float(min,max,dp) needs numeric bounds, got %q,%q", a[0], a[1])
|
||||
}
|
||||
dp, err := plainInt(a[2])
|
||||
if err != nil {
|
||||
return fmt.Errorf("float(min,max,dp): decimals %w", err)
|
||||
}
|
||||
if math.IsNaN(lo) || math.IsNaN(hi) || math.IsInf(lo, 0) || math.IsInf(hi, 0) {
|
||||
return fmt.Errorf("float(min,max,dp) needs finite bounds, got %q,%q", a[0], a[1])
|
||||
}
|
||||
if lo > hi {
|
||||
return fmt.Errorf("float(min,max,dp): min %v > max %v", lo, hi)
|
||||
}
|
||||
if math.IsInf(hi-lo, 0) { // an overflowing span would render as "+Inf"
|
||||
return fmt.Errorf("float(min,max,dp): range %v..%v is too wide", lo, hi)
|
||||
}
|
||||
if dp < 0 || dp > maxDecimals {
|
||||
return fmt.Errorf("float(min,max,dp): decimals %d out of range 0..%d", dp, maxDecimals)
|
||||
}
|
||||
if lo == hi {
|
||||
return fmt.Errorf("float(%s,%s,%d) is the constant %q; write it as text", a[0], a[1], dp, strconv.FormatFloat(lo, 'f', dp, 64))
|
||||
}
|
||||
if math.IsInf(hi-lo, 0) { // an overflowing span would render as "+Inf"
|
||||
return fmt.Errorf("float(min,max,dp): range %v..%v is too wide", lo, hi)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func seqArg(_ map[string]node, a []string) error {
|
||||
if len(a) > 1 {
|
||||
return fmt.Errorf("seq takes at most one name, got %d args", len(a))
|
||||
return fmt.Errorf("seq takes at most one name, got %d", len(a))
|
||||
}
|
||||
if len(a) == 1 && a[0] == "" {
|
||||
return fmt.Errorf("seq name must not be empty")
|
||||
@@ -182,18 +233,9 @@ func seqArg(_ map[string]node, a []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// nanoidAlphabet is the 64-char URL-safe set Nano IDs use (order is irrelevant
|
||||
// to the uniform pick).
|
||||
// nanoidAlphabet is the 64-char URL-safe set Nano IDs use.
|
||||
const nanoidAlphabet = "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
func nanoid(r rng, n int) string {
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = nanoidAlphabet[r.IntN(len(nanoidAlphabet))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// uuidV7 builds an RFC 9562 v7 UUID. The 48-bit timestamp field is drawn from
|
||||
// the rng (not the clock) to stay reproducible, then the version (7) and variant
|
||||
// (10) bits are forced; the rest is random.
|
||||
@@ -234,96 +276,3 @@ func ulid(r rng) string {
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// luhnCheck returns the Luhn check digit (0-9) over the digits of s; non-digit
|
||||
// runes are skipped. Doubling runs from the rightmost digit, so the result is
|
||||
// correct whatever the payload length.
|
||||
func luhnCheck(s string) int {
|
||||
sum, double := 0, true
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
c := s[i]
|
||||
if c < '0' || c > '9' {
|
||||
continue
|
||||
}
|
||||
d := int(c - '0')
|
||||
if double {
|
||||
if d *= 2; d > 9 {
|
||||
d -= 9
|
||||
}
|
||||
}
|
||||
double = !double
|
||||
sum += d
|
||||
}
|
||||
return (10 - sum%10) % 10
|
||||
}
|
||||
|
||||
// mod11Check returns the weighted mod-11 check character over the digits of s
|
||||
// (weights 2..7 cycling from the right). A would-be value of 10 emits 'X', as in
|
||||
// ISBN-10 / ISO 7064; non-digits are skipped.
|
||||
func mod11Check(s string) string {
|
||||
sum, w := 0, 2
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
c := s[i]
|
||||
if c < '0' || c > '9' {
|
||||
continue
|
||||
}
|
||||
sum += int(c-'0') * w
|
||||
if w++; w > 7 {
|
||||
w = 2
|
||||
}
|
||||
}
|
||||
if chk := (11 - sum%11) % 11; chk != 10 {
|
||||
return string(rune('0' + chk))
|
||||
}
|
||||
return "X"
|
||||
}
|
||||
|
||||
// eanCheck returns the EAN-13 / UPC-A / ISBN-13 / GTIN check digit over the
|
||||
// digits of s: weights 3 and 1 alternating from the rightmost digit, mod 10.
|
||||
func eanCheck(s string) string {
|
||||
sum, w := 0, 3
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
c := s[i]
|
||||
if c < '0' || c > '9' {
|
||||
continue
|
||||
}
|
||||
sum += int(c-'0') * w
|
||||
w = 4 - w // 3 <-> 1
|
||||
}
|
||||
return string(rune('0' + (10-sum%10)%10))
|
||||
}
|
||||
|
||||
// ibanLen maps a supported country code to the full IBAN length. The check digits
|
||||
// sit between the country code and the BBAN, so — unlike luhn/ean — iban can't be
|
||||
// a left-to-right derivation; it generates the whole value instead.
|
||||
var ibanLen = map[string]int{"BE": 16, "DE": 22, "DK": 18, "ES": 24, "FI": 18, "NO": 15, "SE": 24}
|
||||
|
||||
func ibanArg(_ map[string]node, a []string) error {
|
||||
if _, ok := ibanLen[a[0]]; !ok {
|
||||
return fmt.Errorf("iban(%q): unsupported country code", a[0])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// iban generates a structurally valid IBAN for cc: a numeric BBAN of the right
|
||||
// length, then mod-97 check digits. Real bank/branch structure isn't modelled —
|
||||
// the result passes length and checksum validation, which is what fake data needs.
|
||||
func iban(r rng, cc string) string {
|
||||
bban := make([]byte, ibanLen[cc]-4)
|
||||
for i := range bban {
|
||||
bban[i] = byte('0' + r.IntN(10))
|
||||
}
|
||||
rem := 0
|
||||
feed := func(d int) { rem = (rem*10 + d) % 97 }
|
||||
for _, c := range bban {
|
||||
feed(int(c - '0'))
|
||||
}
|
||||
for i := 0; i < len(cc); i++ { // letters A-Z -> 10..35, fed as two digits
|
||||
v := int(cc[i]-'A') + 10
|
||||
feed(v / 10)
|
||||
feed(v % 10)
|
||||
}
|
||||
feed(0)
|
||||
feed(0)
|
||||
return fmt.Sprintf("%s%02d%s", cc, 98-rem, bban)
|
||||
}
|
||||
|
||||
+178
-27
@@ -4,7 +4,9 @@ import (
|
||||
"encoding/base64"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBuiltinIDGenerators(t *testing.T) {
|
||||
@@ -13,10 +15,10 @@ func TestBuiltinIDGenerators(t *testing.T) {
|
||||
tmpl string
|
||||
re *regexp.Regexp
|
||||
}{
|
||||
{"uuid v7", `{"format":"{uuid()}"}`, regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)},
|
||||
{"ulid", `{"format":"{ulid()}"}`, regexp.MustCompile(`^[0-7][0-9A-HJKMNP-TV-Z]{25}$`)},
|
||||
{"nanoid", `{"format":"{nanoid(21)}"}`, regexp.MustCompile(`^[A-Za-z0-9_-]{21}$`)},
|
||||
{"hex", `{"format":"{hex(16)}"}`, regexp.MustCompile(`^[0-9a-f]{16}$`)},
|
||||
{"uuid v7", `"{uuid()}"`, regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)},
|
||||
{"ulid", `"{ulid()}"`, regexp.MustCompile(`^[0-7][0-9A-HJKMNP-TV-Z]{25}$`)},
|
||||
{"nanoid", `"{nanoid(21)}"`, regexp.MustCompile(`^[A-Za-z0-9_-]{21}$`)},
|
||||
{"hex", `"{hex(16)}"`, regexp.MustCompile(`^[0-9a-f]{16}$`)},
|
||||
}
|
||||
f := engine(1)
|
||||
for _, c := range cases {
|
||||
@@ -38,9 +40,10 @@ func TestBuiltinIDGenerators(t *testing.T) {
|
||||
// sample draws only from the seeded rng, so same seed -> same value.
|
||||
func TestBuiltinSamplesReproducible(t *testing.T) {
|
||||
for _, tmpl := range []string{
|
||||
`{"format":"{uuid()}"}`, `{"format":"{ulid()}"}`,
|
||||
`{"format":"{nanoid(12)}"}`, `{"format":"{int(1,1000000)}"}`,
|
||||
`{"format":"{float(0,1,6)}"}`, `{"format":"{base64(12)}"}`, `{"format":"{iban(SE)}"}`,
|
||||
`"{uuid()}"`, `"{ulid()}"`,
|
||||
`"{nanoid(12)}"`, `"{int(1,1000000)}"`,
|
||||
`"{float(0,1,6)}"`, `"{base64(12)}"`, `"{iban(SE)}"`,
|
||||
`"{date(2000-01-01,2020-12-31,'2006-01-02 15:04:05')}"`, `"{time('15:04:05')}"`,
|
||||
} {
|
||||
if a, b := mustRender(t, engine(7), tmpl), mustRender(t, engine(7), tmpl); a != b {
|
||||
t.Fatalf("%s not reproducible: %q != %q", tmpl, a, b)
|
||||
@@ -52,7 +55,7 @@ func TestBuiltinIntInclusiveRange(t *testing.T) {
|
||||
f := engine(1)
|
||||
lo, hi := false, false
|
||||
for i := 0; i < 1000; i++ {
|
||||
n, err := strconv.Atoi(mustRender(t, f, `{"format":"{int(3,7)}"}`))
|
||||
n, err := strconv.Atoi(mustRender(t, f, `"{int(3,7)}"`))
|
||||
if err != nil || n < 3 || n > 7 {
|
||||
t.Fatalf("int(3,7) = %d (err %v), out of range", n, err)
|
||||
}
|
||||
@@ -66,14 +69,17 @@ func TestBuiltinIntInclusiveRange(t *testing.T) {
|
||||
func TestBuiltinFloat(t *testing.T) {
|
||||
f, re := engine(1), regexp.MustCompile(`^[12]\.\d{3}$`)
|
||||
for i := 0; i < 200; i++ {
|
||||
if got := mustRender(t, f, `{"format":"{float(1,2,3)}"}`); !re.MatchString(got) {
|
||||
if got := mustRender(t, f, `"{float(1,2,3)}"`); !re.MatchString(got) {
|
||||
t.Fatalf("float(1,2,3) = %q, want d.ddd in [1,2]", got)
|
||||
}
|
||||
if got := mustRender(t, f, `"{float(-1,1,0)}"`); got == "-0" {
|
||||
t.Fatalf("float(-1,1,0) = %q, want a zero printed unsigned", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuiltinBase64(t *testing.T) {
|
||||
got := mustRender(t, engine(1), `{"format":"{base64(9)}"}`)
|
||||
got := mustRender(t, engine(1), `"{base64(9)}"`)
|
||||
if b, err := base64.StdEncoding.DecodeString(got); err != nil || len(b) != 9 {
|
||||
t.Fatalf("base64(9) = %q decodes to %d bytes (err %v), want 9", got, len(b), err)
|
||||
}
|
||||
@@ -83,9 +89,9 @@ func TestBuiltinBase64(t *testing.T) {
|
||||
// hand-computed check, like the luhn test. mod-11 emits X when it would be 10.
|
||||
func TestBuiltinChecksums(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
`{"format":"#1#2#3#4#5#6#7#8{mod11()}"}`: "123456785", // weights 2..7 from the right
|
||||
`{"format":"#6{mod11()}"}`: "6X", // remainder 10 -> X
|
||||
`{"format":"#4#0#0#6#3#8#1#3#3#3#9#3{ean()}"}`: "4006381333931", // EAN-13 (= ISBN-13) check digit
|
||||
`"12345678{mod11()}"`: "123456785", // weights 2..7 from the right
|
||||
`"6{mod11()}"`: "6X", // remainder 10 -> X
|
||||
`"400638133393{ean()}"`: "4006381333931", // EAN-13 (= ISBN-13) check digit
|
||||
}
|
||||
f := engine(1)
|
||||
for tmpl, want := range cases {
|
||||
@@ -101,14 +107,14 @@ func TestBuiltinChecksums(t *testing.T) {
|
||||
func TestBuiltinSeqPerSession(t *testing.T) {
|
||||
f := engine(1)
|
||||
for i := 1; i <= 5; i++ {
|
||||
if got := mustRender(t, f, `{"format":"{seq()}"}`); got != strconv.Itoa(i) {
|
||||
if got := mustRender(t, f, `"{seq()}"`); got != strconv.Itoa(i) {
|
||||
t.Fatalf("seq call %d = %q, want %d", i, got, i)
|
||||
}
|
||||
}
|
||||
if got := mustRender(t, f, `{"format":"{seq(orders)}"}`); got != "1" {
|
||||
if got := mustRender(t, f, `"{seq(orders)}"`); got != "1" {
|
||||
t.Fatalf("named seq(orders) = %q, want its own count from 1", got)
|
||||
}
|
||||
if got := mustRender(t, f, `{"format":"{seq()}"}`); got != "6" {
|
||||
if got := mustRender(t, f, `"{seq()}"`); got != "6" {
|
||||
t.Fatalf("default seq after the named one = %q, want 6 (counters are independent)", got)
|
||||
}
|
||||
// repeat drives one counter forward across its renders...
|
||||
@@ -116,7 +122,7 @@ func TestBuiltinSeqPerSession(t *testing.T) {
|
||||
t.Fatalf("repeat seq = %q, want 1,2,3", got)
|
||||
}
|
||||
// ...and a fresh session restarts from 1.
|
||||
if got := mustRender(t, engine(2), `{"format":"{seq()}"}`); got != "1" {
|
||||
if got := mustRender(t, engine(2), `"{seq()}"`); got != "1" {
|
||||
t.Fatalf("new session seq = %q, want 1", got)
|
||||
}
|
||||
}
|
||||
@@ -126,14 +132,14 @@ func TestBuiltinSeqPerSession(t *testing.T) {
|
||||
// at compile, not detonate when the template is drawn.
|
||||
func TestBuiltinArgLimits(t *testing.T) {
|
||||
bad := []string{
|
||||
`{"format":"{int(-9223372036854775808,9223372036854775807)}"}`, // span overflows int -> IntN panic
|
||||
`{"format":"{hex(2000000000)}"}`, // ~2 GB string
|
||||
`{"format":"{nanoid(2000000000)}"}`,
|
||||
`{"format":"{base64(2000000000)}"}`,
|
||||
`{"format":"{float(-1e308,1e308,2)}"}`, // span is +Inf
|
||||
`{"format":"{float(0,1,100000)}"}`, // decimals -> huge string
|
||||
`{"format":"{calc(1+1,100000)}"}`, // calc decimals -> huge string
|
||||
`{"format":"{x}","x":["1"],"repeat":2000000000}`, // repeat -> multi-GB join
|
||||
`"{int(-9223372036854775808,9223372036854775807)}"`, // span overflows int -> IntN panic
|
||||
`"{hex(2000000000)}"`, // ~2 GB string
|
||||
`"{nanoid(2000000000)}"`,
|
||||
`"{base64(2000000000)}"`,
|
||||
`"{float(-1e308,1e308,2)}"`, // span is +Inf
|
||||
`"{float(0,1,100000)}"`, // decimals -> huge string
|
||||
`"{calc(1+1,100000)}"`, // calc decimals -> huge string
|
||||
`{"format":"{x}","x":"1","repeat":2000000000}`, // repeat -> multi-GB join
|
||||
}
|
||||
for _, tmpl := range bad {
|
||||
if _, err := compile(parse(t, tmpl)); err == nil {
|
||||
@@ -141,7 +147,7 @@ func TestBuiltinArgLimits(t *testing.T) {
|
||||
}
|
||||
}
|
||||
// Ordinary values still compile.
|
||||
if _, err := compile(parse(t, `{"format":"{hex(16)} {int(-5,5)} {float(0,1,3)} {calc(1+1,2)}"}`)); err != nil {
|
||||
if _, err := compile(parse(t, `"{hex(16)} {int(-5,5)} {float(0,1,3)} {calc(1+1,2)}"`)); err != nil {
|
||||
t.Errorf("compile of normal args failed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -150,7 +156,7 @@ func TestBuiltinIBAN(t *testing.T) {
|
||||
wantLen := map[string]int{"SE": 24, "DE": 22, "NO": 15}
|
||||
f := engine(1)
|
||||
for cc, n := range wantLen {
|
||||
tmpl := `{"format":"{iban(` + cc + `)}"}`
|
||||
tmpl := `"{iban(` + cc + `)}"`
|
||||
for i := 0; i < 100; i++ {
|
||||
got := mustRender(t, f, tmpl)
|
||||
if got[:2] != cc || len(got) != n {
|
||||
@@ -217,3 +223,148 @@ func mustPanic(t *testing.T, name string, call func()) {
|
||||
}()
|
||||
call()
|
||||
}
|
||||
|
||||
func TestClassBuiltinArgs(t *testing.T) {
|
||||
for _, bad := range []string{`"{digits(0)}"`, `"{digits(2000000000)}"`, `"{upper(-1)}"`, `"{lower(x)}"`, `"{digits()}"`, `"{upper(1,2)}"`} {
|
||||
if _, err := compile(parse(t, bad)); err == nil {
|
||||
t.Errorf("compile(%s) = nil error, want the arg rejected", bad)
|
||||
}
|
||||
}
|
||||
for _, ok := range []string{`"{digits(1048576)}"`, `"{upper(1)}"`, `"{lower(26)}"`} {
|
||||
if _, err := compile(parse(t, ok)); err != nil {
|
||||
t.Errorf("compile(%s) = %v", ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuiltinDateAndTime pins the two clock-free samples: date draws a second in
|
||||
// [from 00:00:00, to 23:59:59], both days reachable, and renders it in the quoted Go
|
||||
// layout, commas and English names included; time draws a second within one day.
|
||||
func TestBuiltinDateAndTime(t *testing.T) {
|
||||
f := engine(1)
|
||||
seen := map[string]bool{}
|
||||
for i := 0; i < 500; i++ {
|
||||
got := mustRender(t, f, `"{date(1990-01-01,1990-12-31,'2006-01-02')}"`)
|
||||
if d, err := time.Parse("2006-01-02", got); err != nil || d.Year() != 1990 {
|
||||
t.Fatalf("date = %q, want a 1990 calendar date (err %v)", got, err)
|
||||
}
|
||||
seen[got] = true
|
||||
}
|
||||
if len(seen) < 200 {
|
||||
t.Fatalf("date drew %d distinct days of 365 in 500, want a uniform spread", len(seen))
|
||||
}
|
||||
lo, hi := false, false
|
||||
for i := 0; i < 200; i++ {
|
||||
got := mustRender(t, f, `"{date(2020-02-28,2020-02-29,'2006-01-02')}"`)
|
||||
if got != "2020-02-28" && got != "2020-02-29" {
|
||||
t.Fatalf("date(2020-02-28,2020-02-29) = %q, out of range", got)
|
||||
}
|
||||
lo, hi = lo || got == "2020-02-28", hi || got == "2020-02-29"
|
||||
}
|
||||
if !lo || !hi {
|
||||
t.Fatalf("date never hit a bound: lo=%v hi=%v (bounds must be inclusive)", lo, hi)
|
||||
}
|
||||
if got := mustRender(t, f, `"{date(2020-07-04,2020-07-05,'January 2, 2006')}"`); got != "July 4, 2020" && got != "July 5, 2020" {
|
||||
t.Fatalf("date with a comma in its layout = %q", got)
|
||||
}
|
||||
rfc := regexp.MustCompile(`^2021-\d\d-\d\dT\d\d:\d\d:\d\dZ$`)
|
||||
clock := regexp.MustCompile(`^([01]\d|2[0-3]):[0-5]\d$`)
|
||||
ampm := regexp.MustCompile(`^(1[0-2]|[1-9]):[0-5]\d (AM|PM)$`)
|
||||
seconds := map[string]bool{}
|
||||
for i := 0; i < 300; i++ {
|
||||
got := mustRender(t, f, `"{date(2021-01-01,2021-12-31,'2006-01-02T15:04:05Z07:00')}"`)
|
||||
if !rfc.MatchString(got) {
|
||||
t.Fatalf("date in an RFC 3339 layout = %q, want %s", got, rfc)
|
||||
}
|
||||
seconds[got[17:19]] = true
|
||||
if got := mustRender(t, f, `"{time('15:04')}"`); !clock.MatchString(got) {
|
||||
t.Fatalf("time('15:04') = %q, want %s", got, clock)
|
||||
}
|
||||
if got := mustRender(t, f, `"{time('3:04 PM')}"`); !ampm.MatchString(got) {
|
||||
t.Fatalf("time('3:04 PM') = %q, want %s", got, ampm)
|
||||
}
|
||||
got = mustRender(t, f, `"{date(1950-01-01,2000-12-31,'060102')}-238{luhn()}"`)
|
||||
if d := digitsOnly(got); len(d) != 10 || !luhnValid(d) {
|
||||
t.Fatalf("a personnummer over date() = %q, want ten Luhn-valid digits", got)
|
||||
}
|
||||
}
|
||||
if len(seconds) < 30 {
|
||||
t.Fatalf("date drew %d distinct seconds in 300, want the whole day, not midnight", len(seconds))
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuiltinDateArgs pins the New-time checks: bounds are calendar dates in order,
|
||||
// the layout is quoted, names a field, and for time names no date field.
|
||||
func TestBuiltinDateArgs(t *testing.T) {
|
||||
for tmpl, want := range map[string]string{
|
||||
`"{date(1990-13-01,1990-12-31,'2006-01-02')}"`: "1990-13-01",
|
||||
`"{date(1990-12-31,1990-01-01,'2006-01-02')}"`: "is after",
|
||||
`"{date(1990-01-01,1990-01-01,'2006-01-02')}"`: "write it as text",
|
||||
`"{date(1990-01-01,1990-12-31,2006-01-02)}"`: "'2006-01-02'",
|
||||
`"{date(1990-01-01,1990-12-31,'January 2, 2006)}"`: "'",
|
||||
`"{date(1990-01-01,1990-12-31,\"January 2, 2006\")}"`: "quoted: 'January 2, 2006'",
|
||||
`"{date(1990-01-01,1990-12-31,'x')}"`: "text",
|
||||
`"{date(1990-01-01,1990-12-31,'')}"`: "text",
|
||||
`"{date(1990-01-01,1990-12-31)}"`: "3 arguments",
|
||||
`"{date(1990-01-01,1990-12-31,January 2, 2006)}"`: "'January 2, 2006'",
|
||||
`"{time(3:04 PM, Mon)}"`: "'3:04 PM, Mon'",
|
||||
`"{time(15:04)}"`: "'15:04'",
|
||||
`"{time('2006-01-02 15:04')}"`: "date(",
|
||||
`"{time('x')}"`: "text",
|
||||
`"{date(1990-01-01,1990-12-31,'15:04')}"`: "time('15:04')",
|
||||
} {
|
||||
_, err := compile(parse(t, tmpl))
|
||||
if err == nil || !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("compile(%s) = %v, want an error mentioning %q", tmpl, err, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuiltinLayoutErrorsNameARunnableSpelling pins the two layout errors a user
|
||||
// can follow: a double-quoted layout is named single-quoted, without its own
|
||||
// quotes carried into the suggestion, whether it split on a comma or not.
|
||||
func TestBuiltinLayoutErrorsNameARunnableSpelling(t *testing.T) {
|
||||
_, err := compile(parse(t, `"{date(1990-01-01,1990-12-31,\"2006-01-02\")}"`))
|
||||
if err == nil || !strings.Contains(err.Error(), "write '2006-01-02'") {
|
||||
t.Fatalf("a double-quoted layout = %v, want it named single-quoted", err)
|
||||
}
|
||||
if strings.Contains(err.Error(), `'"`) {
|
||||
t.Errorf("%v names a layout that renders its own quotes", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "is double-quoted") {
|
||||
t.Errorf("%v does not say which quotes were wrong", err)
|
||||
}
|
||||
_, err = compile(parse(t, `"{time(15:04)}"`))
|
||||
if err == nil || !strings.Contains(err.Error(), "write '15:04'") {
|
||||
t.Fatalf("a bare layout = %v, want it named quoted", err)
|
||||
}
|
||||
// The comma hint belongs to a layout that split, not to a call given extra args.
|
||||
_, err = compile(parse(t, `"{time(0,12,'15:04')}"`))
|
||||
if err == nil || !strings.Contains(err.Error(), "takes 1 argument, got 3") {
|
||||
t.Fatalf("time with three args = %v, want the count named in the singular", err)
|
||||
}
|
||||
if strings.Contains(err.Error(), "holding a comma") {
|
||||
t.Errorf("%v offers the comma hint though the layout is already quoted", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuiltinDateSpansOneDay pins that from == to is a day: with a clock layout it
|
||||
// draws every second of it, and without one it could only emit one value.
|
||||
func TestBuiltinDateSpansOneDay(t *testing.T) {
|
||||
f := engine(1)
|
||||
seen := map[string]bool{}
|
||||
for i := 0; i < 500; i++ {
|
||||
got := mustRender(t, f, `"{date(2026-01-01,2026-01-01,'2006-01-02 15:04:05')}"`)
|
||||
if !strings.HasPrefix(got, "2026-01-01 ") {
|
||||
t.Fatalf("date over one day = %q, out of range", got)
|
||||
}
|
||||
seen[got] = true
|
||||
}
|
||||
if len(seen) < 400 {
|
||||
t.Fatalf("date over one day drew %d distinct seconds in 500, want the whole day", len(seen))
|
||||
}
|
||||
_, err := compile(parse(t, `"{date(2026-01-01,2026-01-01,'2006-01-02')}"`))
|
||||
if err == nil || !strings.Contains(err.Error(), "write it as text") {
|
||||
t.Fatalf("one day in a date-only layout = %v, want it named a constant", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,16 +8,6 @@ import (
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// calc is the {calc(expr[, dp])} token: an arithmetic expression over number
|
||||
// literals and sibling-field names, with + - * /, unary minus and parentheses. It is
|
||||
// a registry builtin like any other {name(args)} function — the one that names
|
||||
// operands, which expand reads for it (once per expansion, so the value computed is
|
||||
// the value the format showed) and hands over as strings. The value prints in
|
||||
// minimal decimal form, or rounded to dp when given. An operand that isn't a number
|
||||
// becomes NaN, which propagates and prints as "NaN" — visible, never a render error.
|
||||
// The expression is parsed once at New into an AST every render shares and none
|
||||
// mutates, so render cannot fail and must not carry per-render state.
|
||||
|
||||
// calcNode is a parsed expression node. It evaluates over the operand values expand
|
||||
// read, so the evaluator touches neither the rng nor the node tree.
|
||||
type calcNode interface {
|
||||
@@ -80,18 +70,116 @@ func checkCalc(fields map[string]node, args []string) error {
|
||||
return fmt.Errorf("calc(%q): %w", args[0], err)
|
||||
}
|
||||
for _, name := range calcVars(expr) {
|
||||
if _, ok := fields[name]; !ok {
|
||||
operand, ok := fields[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("calc(%q): no field %q", args[0], name)
|
||||
}
|
||||
if text, never := neverNumeric(operand); never {
|
||||
return fmt.Errorf("calc(%q): operand %q is never a number: it renders %q", args[0], name, text)
|
||||
}
|
||||
}
|
||||
if divisor, zero := constantZeroDivisor(expr, fields); zero {
|
||||
return fmt.Errorf("calc(%q) divides by %s, which is always zero", args[0], divisor)
|
||||
}
|
||||
if len(args) == 2 {
|
||||
if dp, err := strconv.Atoi(args[1]); err != nil || dp < 0 || dp > maxDecimals {
|
||||
return fmt.Errorf("calc decimals %q must be an integer in 0..%d", args[1], maxDecimals)
|
||||
dp, err := plainInt(args[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("calc decimals %w", err)
|
||||
}
|
||||
if dp < 0 || dp > maxDecimals {
|
||||
return fmt.Errorf("calc decimals %d must be in 0..%d", dp, maxDecimals)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// constantZeroDivisor finds a division whose right side is a constant zero: number
|
||||
// literals and fixed operands folded, anything that varies left unknown.
|
||||
func constantZeroDivisor(n calcNode, fields map[string]node) (string, bool) {
|
||||
switch n := n.(type) {
|
||||
case calcNeg:
|
||||
return constantZeroDivisor(n.x, fields)
|
||||
case calcBin:
|
||||
if n.op == '/' {
|
||||
if v, known := constantValue(n.r, fields); known && v == 0 {
|
||||
return calcText(n.r), true
|
||||
}
|
||||
}
|
||||
if d, zero := constantZeroDivisor(n.l, fields); zero {
|
||||
return d, true
|
||||
}
|
||||
return constantZeroDivisor(n.r, fields)
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// constantValue evaluates an expression whose every operand is fixed.
|
||||
func constantValue(n calcNode, fields map[string]node) (float64, bool) {
|
||||
switch n := n.(type) {
|
||||
case calcNum:
|
||||
return float64(n), true
|
||||
case calcVar:
|
||||
t, ok := fields[string(n)].(*template)
|
||||
if !ok || !t.fixed || t.repeat > 1 {
|
||||
return 0, false
|
||||
}
|
||||
v, err := strconv.ParseFloat(strings.TrimSpace(t.lit), 64)
|
||||
return v, err == nil
|
||||
case calcNeg:
|
||||
v, ok := constantValue(n.x, fields)
|
||||
return -v, ok
|
||||
case calcBin:
|
||||
l, lok := constantValue(n.l, fields)
|
||||
r, rok := constantValue(n.r, fields)
|
||||
if !lok || !rok {
|
||||
return 0, false
|
||||
}
|
||||
return calcBin{n.op, calcNum(l), calcNum(r)}.eval(nil), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// calcText spells an expression node the way an author would read it.
|
||||
func calcText(n calcNode) string {
|
||||
switch n := n.(type) {
|
||||
case calcNum:
|
||||
return strconv.FormatFloat(float64(n), 'f', -1, 64)
|
||||
case calcVar:
|
||||
return string(n)
|
||||
case calcNeg:
|
||||
return "-" + calcText(n.x)
|
||||
case calcBin:
|
||||
return "(" + calcText(n.l) + " " + string(n.op) + " " + calcText(n.r) + ")"
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
|
||||
// neverNumeric reports a node no render of which is a number: a null, fixed text that
|
||||
// does not parse, or a choice of only such items. text is one such render.
|
||||
func neverNumeric(n node) (text string, never bool) {
|
||||
switch n := n.(type) {
|
||||
case *null:
|
||||
return "", true
|
||||
case *template:
|
||||
if !n.fixed || n.repeat > 1 {
|
||||
return "", false
|
||||
}
|
||||
if _, err := strconv.ParseFloat(strings.TrimSpace(n.lit), 64); err != nil {
|
||||
return n.lit, true
|
||||
}
|
||||
case *choice:
|
||||
for _, it := range n.items {
|
||||
t, itemNever := neverNumeric(it)
|
||||
if !itemNever {
|
||||
return "", false
|
||||
}
|
||||
text = t
|
||||
}
|
||||
return text, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// calcPrep parses the expression and decimals once, at compile time, and places each
|
||||
// operand name at the position expand will read it into. checkCalc proved both args
|
||||
// valid, so no step here can fail; dp -1 prints the minimal form.
|
||||
@@ -105,15 +193,20 @@ func calcPrep(args []string) callFn {
|
||||
at[name] = i
|
||||
}
|
||||
placed := indexVars(expr, at)
|
||||
dp := -1
|
||||
if len(args) == 2 {
|
||||
dp = atoi(args[1])
|
||||
}
|
||||
dp := calcDecimals(args)
|
||||
return func(_ *session, _ string, operands []string) string {
|
||||
return strconv.FormatFloat(placed.eval(operands), 'f', dp, 64)
|
||||
return formatFloat(placed.eval(operands), dp)
|
||||
}
|
||||
}
|
||||
|
||||
// calcDecimals is a calc's decimals count, or -1 for the shortest form.
|
||||
func calcDecimals(args []string) int {
|
||||
if len(args) == 2 {
|
||||
return atoi(args[1])
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// indexVars replaces each operand name with its position in the values expand reads.
|
||||
// Both sides take that order from calcVars, so they cannot drift.
|
||||
func indexVars(n calcNode, at map[string]int) calcNode {
|
||||
@@ -132,26 +225,10 @@ func indexVars(n calcNode, at map[string]int) calcNode {
|
||||
return n
|
||||
}
|
||||
|
||||
// calcOperands lists the sibling-field names every {calc(...)} token in a format
|
||||
// reads, so cycle detection sees the field edges calc renders through (a function
|
||||
// token otherwise carries no field edge).
|
||||
func calcOperands(format string) []string {
|
||||
var names []string
|
||||
_ = eachToken(format, func(t ftoken) error {
|
||||
if t.kind == 'b' {
|
||||
names = append(names, calcTokenOperands(t.body)...)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return names
|
||||
}
|
||||
|
||||
// calcTokenOperands lists the sibling-field names one {token} body reads, empty
|
||||
// for anything that is not a {calc(...)}. checkCalc reports an expression that does
|
||||
// not parse, so one that does not simply names nothing here.
|
||||
func calcTokenOperands(body string) []string {
|
||||
name, args, ok := funcCall(body)
|
||||
if !ok || name != "calc" || len(args) == 0 {
|
||||
// calcOperands lists the sibling-field names a calc's args read. checkCalc reports
|
||||
// an expression that does not parse, so one that does not simply names nothing.
|
||||
func calcOperands(args []string) []string {
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
expr, err := parseCalc(args[0])
|
||||
@@ -240,6 +317,7 @@ func (p *calcParser) binary(next func() (calcNode, error), ops ...byte) (calcNod
|
||||
}
|
||||
}
|
||||
|
||||
// factor is a table-shaped scanner, one case per token kind, kept whole on purpose.
|
||||
func (p *calcParser) factor() (calcNode, error) {
|
||||
p.space()
|
||||
if p.pos >= len(p.rs) {
|
||||
|
||||
+85
-31
@@ -12,14 +12,14 @@ import (
|
||||
func TestCalcArithmetic(t *testing.T) {
|
||||
f := engine(1)
|
||||
cases := map[string]string{
|
||||
`{"format":"{calc(2 + 3)}"}`: "5",
|
||||
`{"format":"{calc(2 * 3 + 4)}"}`: "10", // * binds tighter than +
|
||||
`{"format":"{calc(2 + 3 * 4)}"}`: "14",
|
||||
`{"format":"{calc((2 + 3) * 4)}"}`: "20", // parentheses override
|
||||
`{"format":"{calc(10 / 4)}"}`: "2.5",
|
||||
`{"format":"{calc(-2 + 5)}"}`: "3", // unary minus
|
||||
`{"format":"{calc(2 - -3)}"}`: "5",
|
||||
`{"format":"{calc(1.5 * 2)}"}`: "3", // whole result drops the decimals
|
||||
`"{calc(2 + 3)}"`: "5",
|
||||
`"{calc(2 * 3 + 4)}"`: "10", // * binds tighter than +
|
||||
`"{calc(2 + 3 * 4)}"`: "14",
|
||||
`"{calc((2 + 3) * 4)}"`: "20", // parentheses override
|
||||
`"{calc(10 / 4)}"`: "2.5",
|
||||
`"{calc(-2 + 5)}"`: "3", // unary minus
|
||||
`"{calc(2 - -3)}"`: "5",
|
||||
`"{calc(1.5 * 2)}"`: "3", // whole result drops the decimals
|
||||
}
|
||||
for tmpl, want := range cases {
|
||||
if got := mustRender(t, f, tmpl); got != want {
|
||||
@@ -33,9 +33,10 @@ func TestCalcArithmetic(t *testing.T) {
|
||||
func TestCalcAuto(t *testing.T) {
|
||||
f := engine(1)
|
||||
cases := map[string]string{
|
||||
`{"format":"{calc(10 / 3)}"}`: "3.3333333333333335",
|
||||
`{"format":"{calc(6 / 2)}"}`: "3",
|
||||
`{"format":"{calc(1 / 4)}"}`: "0.25",
|
||||
`"{calc(10 / 3)}"`: "3.3333333333333335",
|
||||
`"{calc(6 / 2)}"`: "3",
|
||||
`"{calc(1 / 4)}"`: "0.25",
|
||||
`"{calc(0 * -1)}"`: "0",
|
||||
}
|
||||
for tmpl, want := range cases {
|
||||
if got := mustRender(t, f, tmpl); got != want {
|
||||
@@ -49,9 +50,10 @@ func TestCalcAuto(t *testing.T) {
|
||||
func TestCalcDecimals(t *testing.T) {
|
||||
f := engine(1)
|
||||
cases := map[string]string{
|
||||
`{"format":"{calc(10 / 3, 2)}"}`: "3.33",
|
||||
`{"format":"{calc(10 / 3, 0)}"}`: "3",
|
||||
`{"format":"{calc(2 * 3, 2)}"}`: "6.00",
|
||||
`"{calc(10 / 3, 2)}"`: "3.33",
|
||||
`"{calc(10 / 3, 0)}"`: "3",
|
||||
`"{calc(2 * 3, 2)}"`: "6.00",
|
||||
`"{calc(-0.001, 2)}"`: "0.00",
|
||||
}
|
||||
for tmpl, want := range cases {
|
||||
if got := mustRender(t, f, tmpl); got != want {
|
||||
@@ -63,43 +65,47 @@ func TestCalcDecimals(t *testing.T) {
|
||||
// TestCalcFields pins that bare names resolve to sibling fields, rendered then
|
||||
// parsed as numbers.
|
||||
func TestCalcFields(t *testing.T) {
|
||||
if got := mustRender(t, engine(1), `{"format":"{calc(price * qty, 2)}","price":["19.99"],"qty":["3"]}`); got != "59.97" {
|
||||
if got := mustRender(t, engine(1), `{"format":"{calc(price * qty, 2)}","price":"19.99","qty":"3"}`); got != "59.97" {
|
||||
t.Fatalf("calc over fields = %q, want 59.97", got)
|
||||
}
|
||||
// A field that is itself a template renders before parsing.
|
||||
if got := mustRender(t, engine(1), `{"format":"{calc(a - b)}","a":[{"format":"{n}","n":["10"]}],"b":["3"]}`); got != "7" {
|
||||
if got := mustRender(t, engine(1), `{"format":"{calc(a - b)}","a":{"format":"{n}","n":"10"},"b":"3"}`); got != "7" {
|
||||
t.Fatalf("calc(a - b) = %q, want 7", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalcNonNumericIsNaN pins the never-fail rule: a field that doesn't render
|
||||
// to a number becomes NaN, which propagates and prints visibly.
|
||||
// TestCalcNonNumericIsNaN pins the never-fail rule: a field that sometimes does
|
||||
// not render to a number becomes NaN then, which propagates and prints visibly.
|
||||
func TestCalcNonNumericIsNaN(t *testing.T) {
|
||||
if got := mustRender(t, engine(1), `{"format":"{calc(x * 2)}","x":["abc"]}`); got != "NaN" {
|
||||
t.Fatalf("calc over non-numeric field = %q, want NaN", got)
|
||||
f := engine(1)
|
||||
for i := 0; i < 50; i++ {
|
||||
if got := mustRender(t, f, `{"format":"{calc(x * 2)}","x":["abc","1"]}`); got == "NaN" {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("calc over a sometimes non-numeric field never printed NaN in 50 draws")
|
||||
}
|
||||
|
||||
// TestCalcReproducible pins that a calc over a random operand stays seed-stable.
|
||||
func TestCalcReproducible(t *testing.T) {
|
||||
tmpl := `{"format":"{calc(q * 2 + 1)}","q":[{"format":"{int(1,1000000)}"}]}`
|
||||
tmpl := `{"format":"{calc(q * 2 + 1)}","q":"{int(1,1000000)}"}`
|
||||
if a, b := mustRender(t, engine(7), tmpl), mustRender(t, engine(7), tmpl); a != b {
|
||||
t.Fatalf("calc not reproducible: %q != %q", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalcTokenOperandsReadsOnlyACalc pins what the helper answers for a body that
|
||||
// TestTokenOperandsReadsOnlyAnOperandBuiltin pins what the helper answers for a body that
|
||||
// is not a calc, and for one whose expression does not parse: nothing, either way.
|
||||
// checkCalc is what reports a bad expression, so the callers that run after it
|
||||
// never meet one — but they must not have to depend on that order to be safe.
|
||||
func TestCalcTokenOperandsReadsOnlyACalc(t *testing.T) {
|
||||
func TestTokenOperandsReadsOnlyAnOperandBuiltin(t *testing.T) {
|
||||
for _, body := range []string{"plain", "luhn()", "calc()", "calc(1 +)", "calc(()"} {
|
||||
if got := calcTokenOperands(body); got != nil {
|
||||
t.Errorf("calcTokenOperands(%q) = %v, want none", body, got)
|
||||
if got := tokenOperands(body); got != nil {
|
||||
t.Errorf("tokenOperands(%q) = %v, want none", body, got)
|
||||
}
|
||||
}
|
||||
if got := calcTokenOperands("calc(net * qty)"); len(got) != 2 {
|
||||
t.Errorf("calcTokenOperands(calc(net * qty)) = %v, want both operands", got)
|
||||
if got := tokenOperands("calc(net * qty)"); len(got) != 2 {
|
||||
t.Errorf("tokenOperands(calc(net * qty)) = %v, want both operands", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,16 +146,16 @@ func TestCalcOperandReadsTheExpansionsDraw(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestCalcOperandSharesOneDraw pins the reach of that hold: the draw belongs to the
|
||||
// expansion, not to the calc, so a bare token rendering the same name reads it too.
|
||||
// expansion, not to the calc, so the bare token rendering the same name reads it too.
|
||||
func TestCalcOperandSharesOneDraw(t *testing.T) {
|
||||
dir := writeData(t, map[string]string{
|
||||
"same": `{"format":"{w} {w} {calc(w)}","w":["1","2","3","4","5"]}`,
|
||||
"same": `{"format":"{w} {calc(w)}","w":["1","2","3","4","5"]}`,
|
||||
})
|
||||
f := newGenerator(t, dir, WithSeed(5))
|
||||
for i := 0; i < 200; i++ {
|
||||
got := fake(t, f, "same")
|
||||
if p := strings.Fields(got); len(p) != 3 || p[0] != p[1] || p[0] != p[2] {
|
||||
t.Fatalf("same = %q, want one value three times", got)
|
||||
if p := strings.Fields(got); len(p) != 2 || p[0] != p[1] {
|
||||
t.Fatalf("same = %q, want one value twice", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,3 +224,51 @@ func TestCalcHoldIsPerTemplate(t *testing.T) {
|
||||
t.Fatal("the nested template never differed from its parent, want its own draw")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcOverANeverNumericOperandIsRejected(t *testing.T) {
|
||||
for src, want := range map[string]string{
|
||||
`{"format":"{calc(x * 2)}","x":"abc"}`: `"abc"`,
|
||||
`{"format":"{calc(x * 2)}","x":["a","b"]}`: `"x"`,
|
||||
`{"format":"{calc(x + y)}","x":"1","y":"{{2}}"}`: `"{2}"`,
|
||||
} {
|
||||
_, err := compile(parse(t, src))
|
||||
if err == nil || !strings.Contains(err.Error(), want) || !strings.Contains(err.Error(), "never a number") {
|
||||
t.Errorf("compile(%s) = %v, want the operand rejected naming %s", src, err, want)
|
||||
}
|
||||
}
|
||||
for _, ok := range []string{
|
||||
`{"format":"{calc(x * 2)}","x":"3"}`,
|
||||
`{"format":"{calc(x * 2)}","x":" 2.5 "}`,
|
||||
`{"format":"{calc(x * 2)}","x":["1","abc"]}`,
|
||||
`{"format":"{calc(x * 2)}","x":"{digits(2)}"}`,
|
||||
} {
|
||||
if _, err := compile(parse(t, ok)); err != nil {
|
||||
t.Errorf("compile(%s) = %v, want it accepted", ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcConstantZeroDivisorIsRejected(t *testing.T) {
|
||||
for src, want := range map[string]string{
|
||||
`"{calc(1/0)}"`: "divides by 0",
|
||||
`"{calc(2/(1-1))}"`: "divides by (1 - 1)",
|
||||
`{"format":"{calc(x/y)}","x":"1","y":"0"}`: "divides by y",
|
||||
`{"format":"{calc(x/(y*2))}","x":"1","y":" 0 "}`: "divides by (y * 2)",
|
||||
`{"format":"{calc((a/0)+b)}","a":"1","b":"2"}`: "divides by 0",
|
||||
`{"format":"{calc(a/-0)}","a":"1"}`: "divides by -0",
|
||||
} {
|
||||
if _, err := compile(parse(t, src)); err == nil || !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("compile(%s) = %v, want the constant zero divisor rejected naming %q", src, err, want)
|
||||
}
|
||||
}
|
||||
if _, err := compile(parse(t, `{"format":"{calc(a/(b*c))}","a":"1","b":"0","c":["1","2"]}`)); err != nil {
|
||||
t.Errorf("compile(a/(b*c)) = %v, want a divisor that varies accepted", err)
|
||||
}
|
||||
f := engine(1)
|
||||
for i := 0; i < 50; i++ {
|
||||
if got := mustRender(t, f, `{"format":"{calc(x/y)}","x":"1","y":["0","1"]}`); got == "+Inf" {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("a sometimes-zero divisor never printed +Inf in 50 draws")
|
||||
}
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package fejkdata
|
||||
|
||||
import "fmt"
|
||||
|
||||
// luhnCheck returns the Luhn check digit (0-9) over the digits of s; non-digit
|
||||
// runes are skipped. Doubling runs from the rightmost digit, so the result is
|
||||
// correct whatever the payload length.
|
||||
func luhnCheck(s string) int {
|
||||
sum, double := 0, true
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
c := s[i]
|
||||
if c < '0' || c > '9' {
|
||||
continue
|
||||
}
|
||||
d := int(c - '0')
|
||||
if double {
|
||||
if d *= 2; d > 9 {
|
||||
d -= 9
|
||||
}
|
||||
}
|
||||
double = !double
|
||||
sum += d
|
||||
}
|
||||
return (10 - sum%10) % 10
|
||||
}
|
||||
|
||||
// mod11Check returns the weighted mod-11 check character over the digits of s
|
||||
// (weights 2..7 cycling from the right). A would-be value of 10 emits 'X', as in
|
||||
// ISBN-10 / ISO 7064; non-digits are skipped.
|
||||
func mod11Check(s string) string {
|
||||
sum, w := 0, 2
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
c := s[i]
|
||||
if c < '0' || c > '9' {
|
||||
continue
|
||||
}
|
||||
sum += int(c-'0') * w
|
||||
if w++; w > 7 {
|
||||
w = 2
|
||||
}
|
||||
}
|
||||
if chk := (11 - sum%11) % 11; chk != 10 {
|
||||
return string(rune('0' + chk))
|
||||
}
|
||||
return "X"
|
||||
}
|
||||
|
||||
// eanCheck returns the EAN-13 / UPC-A / ISBN-13 / GTIN check digit over the
|
||||
// digits of s: weights 3 and 1 alternating from the rightmost digit, mod 10.
|
||||
func eanCheck(s string) string {
|
||||
sum, w := 0, 3
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
c := s[i]
|
||||
if c < '0' || c > '9' {
|
||||
continue
|
||||
}
|
||||
sum += int(c-'0') * w
|
||||
w = 4 - w // 3 <-> 1
|
||||
}
|
||||
return string(rune('0' + (10-sum%10)%10))
|
||||
}
|
||||
|
||||
// ibanLen maps a supported country code to the full IBAN length. The check digits
|
||||
// sit between the country code and the BBAN, so — unlike luhn/ean — iban can't be
|
||||
// a left-to-right derivation; it generates the whole value instead.
|
||||
var ibanLen = map[string]int{"BE": 16, "DE": 22, "DK": 18, "ES": 24, "FI": 18, "NO": 15, "SE": 24}
|
||||
|
||||
func ibanArg(_ map[string]node, a []string) error {
|
||||
if _, ok := ibanLen[a[0]]; !ok {
|
||||
return fmt.Errorf("iban(%q): unsupported country code", a[0])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// iban generates a structurally valid IBAN for cc: a numeric BBAN of the right
|
||||
// length, then mod-97 check digits. Real bank/branch structure isn't modelled —
|
||||
// the result passes length and checksum validation, which is what fake data needs.
|
||||
func iban(r rng, cc string) string {
|
||||
bban := make([]byte, ibanLen[cc]-4)
|
||||
for i := range bban {
|
||||
bban[i] = byte('0' + r.IntN(10))
|
||||
}
|
||||
rem := 0
|
||||
feed := func(d int) { rem = (rem*10 + d) % 97 }
|
||||
for _, c := range bban {
|
||||
feed(int(c - '0'))
|
||||
}
|
||||
for i := 0; i < len(cc); i++ { // letters A-Z -> 10..35, fed as two digits
|
||||
v := int(cc[i]-'A') + 10
|
||||
feed(v / 10)
|
||||
feed(v % 10)
|
||||
}
|
||||
feed(0)
|
||||
feed(0)
|
||||
return fmt.Sprintf("%s%02d%s", cc, 98-rem, bban)
|
||||
}
|
||||
+460
-73
@@ -1,122 +1,509 @@
|
||||
// Command fejkdata prints one fake value from one or more data directories.
|
||||
// Command fejkdata prints fake values from the shipped data and any directories
|
||||
// layered over it.
|
||||
//
|
||||
// fejkdata -data-path ./data/sv_SE person # a full person
|
||||
// fejkdata -data-path ./data/sv_SE person.last # just the surname (dotted path)
|
||||
// fejkdata -data-path ./data sv_SE.person # point at the tree, address by folder
|
||||
// fejkdata -data-path ./data/sv_SE -data-path ./mydata person # layer custom data; last dir wins
|
||||
// fejkdata -seed 42 -data-path ./data/sv_SE address
|
||||
//
|
||||
// It is a thin CLI over the fejkdata library: New(dirs) then Fake(path).
|
||||
// fejkdata sv_SE.person # a full person
|
||||
// fejkdata sv_SE.person.last # just the surname
|
||||
// fejkdata --data-path ./mydata sv_SE.person # layer custom data; the last dir wins
|
||||
// fejkdata --seed 42 sv_SE.address
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.larvit.se/larvit/fejkdata"
|
||||
)
|
||||
|
||||
const usage = `Usage: fejkdata -data-path D [-data-path D]... [-seed N] [-repeat N] [-separator S] <path>
|
||||
const usage = `Usage: fejkdata [flags] <path|template>
|
||||
|
||||
-data-path D a data directory, e.g. ./data/sv_SE (repeatable; last wins on clash)
|
||||
<path> a category, or a dotted path into one (person, person.last)
|
||||
-seed N seed for reproducible output
|
||||
-repeat N render the path N times (default 1)
|
||||
-separator S string between repeated values (default newline)
|
||||
-list list the paths the data offers, then exit
|
||||
-version print the version, then exit
|
||||
<path> a category, or a dotted path into one (person, person.last);
|
||||
a table's row by key or name: 'misc.territory[SE]', 'misc.territory[Sweden].tld'
|
||||
<template> a format string or JSON value to render inline, e.g.
|
||||
'name: {/sv_SE.person.last}' or '{"format":"{x}","x":["bosse","lina"]}'
|
||||
|
||||
Flags must come before <path>.`
|
||||
A layout inside a template is single-quoted, so quote the whole argument with " to
|
||||
keep it: "{date(1990-01-01,2010-12-31,'2006-01-02')}".
|
||||
|
||||
// stringList collects a repeatable string flag, preserving order.
|
||||
type stringList []string
|
||||
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 or a quote,
|
||||
and a bracket only as a [selector] after a table's name). Templates reach the
|
||||
data by reference from the root — {/sv_SE.person.last} — whether the data is
|
||||
shipped or layered with --data-path. An argument carrying a closing brace or a
|
||||
quote but no valid JSON names neither.
|
||||
|
||||
func (s *stringList) String() string { return strings.Join(*s, ",") }
|
||||
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
|
||||
array, one JSON object per line, one CSV row (after a header), or one INSERT.
|
||||
|
||||
func (s *stringList) Set(v string) error { *s = append(*s, v); return nil }
|
||||
-d, --data-path D a data directory to layer over the shipped data (repeatable; last wins on a clash)
|
||||
--format F output form: text (default), json, ndjson, csv or sql
|
||||
-h, --help print this help, then exit
|
||||
--list list the paths the data offers, then exit
|
||||
--no-shipped-data load only the --data-path directories
|
||||
-n, --repeat N render the value N times, 1..1048576 (default 1)
|
||||
-s, --seed N seed for reproducible output
|
||||
--separator S string between repeated values (default newline)
|
||||
--table T the INSERT target for --format sql (default: the path's last segment, or records for an inline template)
|
||||
--version print the version, then exit
|
||||
|
||||
Flags may come before or after <path|template>; -- ends the flags. A short flag's
|
||||
value attaches or follows (-n3, -n 3); short flags bundle (-hn 3).
|
||||
`
|
||||
|
||||
type invocation struct {
|
||||
dirs []string
|
||||
format string
|
||||
formatSet bool
|
||||
help bool
|
||||
list bool
|
||||
noShipped bool
|
||||
paths []string
|
||||
repeat int
|
||||
repeatSet bool
|
||||
seed uint64
|
||||
seeded bool
|
||||
separator string
|
||||
separatorSet bool
|
||||
table string
|
||||
tableSet bool
|
||||
version bool
|
||||
}
|
||||
|
||||
type flagDef struct {
|
||||
long string
|
||||
short string
|
||||
value bool
|
||||
set func(*invocation, string) error
|
||||
}
|
||||
|
||||
var flagDefs = []flagDef{
|
||||
{"data-path", "d", true, func(in *invocation, v string) error { in.dirs = append(in.dirs, v); return nil }},
|
||||
{"format", "", true, func(in *invocation, v string) error { in.format, in.formatSet = v, true; return nil }},
|
||||
{"help", "h", false, func(in *invocation, _ string) error { in.help = true; return nil }},
|
||||
{"list", "", false, func(in *invocation, _ string) error { in.list = true; return nil }},
|
||||
{"no-shipped-data", "", false, func(in *invocation, _ string) error { in.noShipped = true; return nil }},
|
||||
{"repeat", "n", true, func(in *invocation, v string) error {
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil || n < 1 || n > fejkdata.MaxRepeat {
|
||||
return fmt.Errorf("--repeat needs an integer in 1..%d, got %q", fejkdata.MaxRepeat, v)
|
||||
}
|
||||
in.repeat, in.repeatSet = n, true
|
||||
return nil
|
||||
}},
|
||||
{"seed", "s", true, func(in *invocation, v string) error {
|
||||
n, err := strconv.ParseUint(v, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("--seed needs an unsigned integer, got %q", v)
|
||||
}
|
||||
in.seed, in.seeded = n, true
|
||||
return nil
|
||||
}},
|
||||
{"separator", "", true, func(in *invocation, v string) error { in.separator, in.separatorSet = v, true; return nil }},
|
||||
{"table", "", true, func(in *invocation, v string) error { in.table, in.tableSet = v, true; return nil }},
|
||||
{"version", "", false, func(in *invocation, _ string) error { in.version = true; return nil }},
|
||||
}
|
||||
|
||||
func flagByLong(name string) *flagDef {
|
||||
for i := range flagDefs {
|
||||
if flagDefs[i].long == name {
|
||||
return &flagDefs[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func flagByShort(name string) *flagDef {
|
||||
for i := range flagDefs {
|
||||
if flagDefs[i].short == name {
|
||||
return &flagDefs[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// flagArg is one flag as written: its definition and the value attached to it,
|
||||
// if any.
|
||||
type flagArg struct {
|
||||
def *flagDef
|
||||
value string
|
||||
inline bool
|
||||
}
|
||||
|
||||
// splitFlags reads one argv element as flags: --name or --name=value, or -abc
|
||||
// where each letter is a short flag and the first that takes a value takes the
|
||||
// rest of the element.
|
||||
func splitFlags(arg string) ([]flagArg, error) {
|
||||
if strings.HasPrefix(arg, "--") {
|
||||
name, value, inline := strings.Cut(arg[2:], "=")
|
||||
def := flagByLong(name)
|
||||
if def == nil {
|
||||
return nil, fmt.Errorf("unknown flag --%s", name)
|
||||
}
|
||||
return []flagArg{{def, value, inline}}, nil
|
||||
}
|
||||
if name, _, _ := strings.Cut(arg[1:], "="); len(name) > 1 && flagByLong(name) != nil {
|
||||
return nil, fmt.Errorf("unknown flag %s; use --%s", arg, name)
|
||||
}
|
||||
var flags []flagArg
|
||||
letters := arg[1:]
|
||||
for i, r := range letters {
|
||||
letter := string(r)
|
||||
def := flagByShort(letter)
|
||||
if def == nil {
|
||||
return nil, fmt.Errorf("unknown flag -%s", letter)
|
||||
}
|
||||
if !def.value {
|
||||
flags = append(flags, flagArg{def: def})
|
||||
continue
|
||||
}
|
||||
rest := letters[i+len(letter):]
|
||||
if strings.HasPrefix(rest, "=") {
|
||||
return nil, fmt.Errorf("-%s takes its value attached (-%s%s) or next (-%s %s); = belongs to --%s=%s",
|
||||
letter, letter, rest[1:], letter, rest[1:], def.long, rest[1:])
|
||||
}
|
||||
return append(flags, flagArg{def, rest, rest != ""}), nil
|
||||
}
|
||||
return flags, nil
|
||||
}
|
||||
|
||||
func parseArgs(argv []string) (invocation, error) {
|
||||
in := invocation{repeat: 1, separator: "\n", format: "text"}
|
||||
for i := 0; i < len(argv); i++ {
|
||||
arg := argv[i]
|
||||
if arg == "--" {
|
||||
in.paths = append(in.paths, argv[i+1:]...)
|
||||
return in, nil
|
||||
}
|
||||
if len(arg) < 2 || arg[0] != '-' {
|
||||
in.paths = append(in.paths, arg)
|
||||
continue
|
||||
}
|
||||
flags, err := splitFlags(arg)
|
||||
if err != nil {
|
||||
return in, err
|
||||
}
|
||||
for _, fl := range flags {
|
||||
if !fl.def.value {
|
||||
if fl.inline {
|
||||
return in, fmt.Errorf("--%s takes no value", fl.def.long)
|
||||
}
|
||||
_ = fl.def.set(&in, "")
|
||||
continue
|
||||
}
|
||||
value := fl.value
|
||||
if !fl.inline {
|
||||
if i++; i >= len(argv) {
|
||||
return in, fmt.Errorf("--%s needs a value", fl.def.long)
|
||||
}
|
||||
value = argv[i]
|
||||
}
|
||||
if err := fl.def.set(&in, value); err != nil {
|
||||
return in, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return in, nil
|
||||
}
|
||||
|
||||
// recordFormat is one way to write a record out: the line each record renders,
|
||||
// the header that precedes the first one, and the open/close frame plus the
|
||||
// between-record separator a document form needs.
|
||||
type recordFormat struct {
|
||||
header func(*fejkdata.Record) string
|
||||
line func(r *fejkdata.Record, table string) string
|
||||
open string
|
||||
close string
|
||||
sep string
|
||||
}
|
||||
|
||||
// recordFormats is every --format that writes records. json frames the records
|
||||
// as one array document; ndjson is the same column, one object per line.
|
||||
var recordFormats = map[string]recordFormat{
|
||||
"csv": {header: (*fejkdata.Record).CSVHeader, line: func(r *fejkdata.Record, _ string) string { return r.CSVLine() }, sep: "\n"},
|
||||
"json": {line: jsonLine, open: "[", close: "]", sep: ",\n"},
|
||||
"ndjson": {line: jsonLine, sep: "\n"},
|
||||
"sql": {line: func(r *fejkdata.Record, table string) string { return r.SQLInsert(table) }, sep: "\n"},
|
||||
}
|
||||
|
||||
func jsonLine(r *fejkdata.Record, _ string) string { return r.JSON() }
|
||||
|
||||
// writesRecords reports whether the format writes records rather than plain text.
|
||||
func (in invocation) writesRecords() bool {
|
||||
return in.format != "text"
|
||||
}
|
||||
|
||||
// formatNames lists the --format values, text included, for the misuse error.
|
||||
func formatNames() string {
|
||||
names := []string{"text"}
|
||||
for name := range recordFormats {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return strings.Join(names[:len(names)-1], ", ") + " or " + names[len(names)-1]
|
||||
}
|
||||
|
||||
// checkFlags rejects a flag value or combination that cannot run.
|
||||
func (in invocation) checkFlags() error {
|
||||
if _, ok := recordFormats[in.format]; !ok && in.format != "text" {
|
||||
return fmt.Errorf("--format takes %s, got %q", formatNames(), in.format)
|
||||
}
|
||||
if in.tableSet && in.table == "" {
|
||||
return errors.New("--table names the INSERT target, so it cannot be empty")
|
||||
}
|
||||
if in.tableSet && in.format != "sql" {
|
||||
return errors.New("--table names the INSERT target, so it needs --format sql")
|
||||
}
|
||||
if in.writesRecords() && in.separatorSet {
|
||||
return errors.New("--separator joins text values, so it has no effect with --format " + in.format)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// check rejects a flag combination or an argument that cannot run, and reports
|
||||
// what the argument names, so its shape is settled before any data is read.
|
||||
func (in invocation) check() (argKind, error) {
|
||||
if err := in.checkFlags(); err != nil {
|
||||
return argPath, err
|
||||
}
|
||||
if in.list && len(in.paths) > 0 {
|
||||
return argPath, errors.New("--list takes no path")
|
||||
}
|
||||
if in.list && (in.repeatSet || in.separatorSet || in.formatSet || in.tableSet) {
|
||||
return argPath, errors.New("--list takes no --repeat, --separator, --format or --table")
|
||||
}
|
||||
if in.list {
|
||||
return argPath, nil
|
||||
}
|
||||
if len(in.paths) != 1 {
|
||||
return argPath, fmt.Errorf("expected one path or template, got %d%s", len(in.paths), templateSplit(in.paths))
|
||||
}
|
||||
return classify(in.paths[0])
|
||||
}
|
||||
|
||||
func (in invocation) options() []fejkdata.Option {
|
||||
var opts []fejkdata.Option
|
||||
if in.noShipped {
|
||||
opts = append(opts, fejkdata.WithoutShippedData())
|
||||
}
|
||||
for _, dir := range in.dirs {
|
||||
opts = append(opts, fejkdata.WithDataPath(dir))
|
||||
}
|
||||
if in.seeded {
|
||||
opts = append(opts, fejkdata.WithSeed(in.seed))
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
// write streams the argument's renders to w, repeat of them joined by the
|
||||
// separator and ended by a newline. A value that renders once renders every time,
|
||||
// so a render failure comes before anything is written; a write failure surfaces
|
||||
// from Flush, bufio keeping the first one.
|
||||
func (in invocation) write(f *fejkdata.Generator, kind argKind, w io.Writer) error {
|
||||
if in.writesRecords() {
|
||||
return in.writeRecords(f, kind, w)
|
||||
}
|
||||
draw, err := in.textDraw(f, kind, in.paths[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out := bufio.NewWriter(w)
|
||||
for i := 0; i < in.repeat; i++ {
|
||||
v, err := draw()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if i > 0 {
|
||||
out.WriteString(in.separator)
|
||||
}
|
||||
out.WriteString(v)
|
||||
}
|
||||
out.WriteString("\n")
|
||||
return out.Flush()
|
||||
}
|
||||
|
||||
// textDraw builds what one text render yields: the value, from a path or an
|
||||
// inline template.
|
||||
func (in invocation) textDraw(f *fejkdata.Generator, kind argKind, arg string) (func() (string, error), error) {
|
||||
if kind != argTemplate {
|
||||
return func() (string, error) { return f.Fake(arg) }, nil
|
||||
}
|
||||
t, err := f.NewTemplate(arg)
|
||||
if err != nil {
|
||||
return nil, templateError{err}
|
||||
}
|
||||
return func() (string, error) { return t.Fake(), nil }, nil
|
||||
}
|
||||
|
||||
// recordStream builds the record drawer for the argument, plus the INSERT table
|
||||
// a sql format names.
|
||||
func (in invocation) recordStream(f *fejkdata.Generator, kind argKind, arg string) (func() (*fejkdata.Record, error), string, error) {
|
||||
record := func() (*fejkdata.Record, error) { return f.FakeRecord(arg) }
|
||||
if kind == argTemplate {
|
||||
t, err := f.NewRecordTemplate(arg)
|
||||
if err != nil {
|
||||
return nil, "", templateError{err}
|
||||
}
|
||||
record = func() (*fejkdata.Record, error) { return t.Fake(), nil }
|
||||
}
|
||||
table := in.table
|
||||
if table == "" {
|
||||
table = defaultTable(arg, kind)
|
||||
}
|
||||
return record, table, nil
|
||||
}
|
||||
|
||||
// writeRecords streams a record per line in the chosen format, framing a document
|
||||
// form with its open/close brackets and a header preceding the first record.
|
||||
func (in invocation) writeRecords(f *fejkdata.Generator, kind argKind, w io.Writer) error {
|
||||
record, table, err := in.recordStream(f, kind, in.paths[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
format := recordFormats[in.format]
|
||||
out := bufio.NewWriter(w)
|
||||
if format.open != "" {
|
||||
out.WriteString(format.open)
|
||||
out.WriteByte('\n')
|
||||
}
|
||||
for i := 0; i < in.repeat; i++ {
|
||||
r, err := record()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if i == 0 && format.header != nil {
|
||||
out.WriteString(format.header(r))
|
||||
out.WriteByte('\n')
|
||||
}
|
||||
if i > 0 {
|
||||
out.WriteString(format.sep)
|
||||
}
|
||||
out.WriteString(format.line(r, table))
|
||||
}
|
||||
if format.close != "" {
|
||||
out.WriteByte('\n')
|
||||
out.WriteString(format.close)
|
||||
}
|
||||
out.WriteByte('\n')
|
||||
return out.Flush()
|
||||
}
|
||||
|
||||
// defaultTable names the INSERT target when --table is absent: the path's last
|
||||
// segment, or "records" for an inline template that sits in no folder.
|
||||
func defaultTable(arg string, kind argKind) string {
|
||||
if kind == argTemplate {
|
||||
return "records"
|
||||
}
|
||||
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]
|
||||
}
|
||||
|
||||
// templateError marks a render failure that is the argument's own fault — an
|
||||
// inline template that does not compile. run reports it as misuse (exit 2, with a
|
||||
// pointer to --help), unlike an unknown path, which is a runtime error (exit 1).
|
||||
type templateError struct{ error }
|
||||
|
||||
func (e templateError) Unwrap() error { return e.error }
|
||||
|
||||
type argKind int
|
||||
|
||||
const (
|
||||
argPath argKind = iota
|
||||
argTemplate
|
||||
)
|
||||
|
||||
// classify reads what a positional argument names by its shape (see fejkdata.IsTemplate).
|
||||
func classify(arg string) (argKind, error) {
|
||||
inline, err := fejkdata.IsTemplate(arg)
|
||||
if inline {
|
||||
return argTemplate, nil
|
||||
}
|
||||
return argPath, err
|
||||
}
|
||||
|
||||
func main() { os.Exit(run(os.Args[1:], os.Stdout, os.Stderr)) }
|
||||
|
||||
// run is main's testable core: it returns the process exit code (0 ok, 1
|
||||
// runtime error, 2 misuse) and writes only to the given streams.
|
||||
// run returns the exit code: 0 ok, 1 runtime error, 2 misuse.
|
||||
func run(args []string, stdout, stderr io.Writer) int {
|
||||
fs := flag.NewFlagSet("fejkdata", flag.ContinueOnError)
|
||||
fs.SetOutput(stderr)
|
||||
fs.Usage = func() { fmt.Fprintln(stderr, usage) }
|
||||
seed := fs.Uint64("seed", 0, "seed for reproducible output")
|
||||
repeat := fs.Int("repeat", 1, "render the path this many times")
|
||||
sep := fs.String("separator", "\n", "string between repeated values")
|
||||
list := fs.Bool("list", false, "list the paths the data offers, then exit")
|
||||
showVersion := fs.Bool("version", false, "print the version, then exit")
|
||||
var dirs stringList
|
||||
fs.Var(&dirs, "data-path", "a data directory to load (repeatable)")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) { // -h/-help already printed usage
|
||||
in, err := parseArgs(args)
|
||||
if err != nil {
|
||||
return misuse(stderr, err)
|
||||
}
|
||||
if in.help {
|
||||
fmt.Fprint(stdout, usage)
|
||||
return 0
|
||||
}
|
||||
return 2
|
||||
}
|
||||
if *showVersion {
|
||||
if in.version {
|
||||
fmt.Fprintln(stdout, "fejkdata "+buildVersion())
|
||||
return 0
|
||||
}
|
||||
if len(dirs) == 0 {
|
||||
fs.Usage()
|
||||
return 2
|
||||
kind, err := in.check()
|
||||
if err != nil {
|
||||
return misuse(stderr, err)
|
||||
}
|
||||
|
||||
var opts []fejkdata.Option
|
||||
fs.Visit(func(fl *flag.Flag) {
|
||||
if fl.Name == "seed" {
|
||||
opts = append(opts, fejkdata.WithSeed(*seed))
|
||||
f, err := fejkdata.New(in.options()...)
|
||||
if errors.Is(err, fejkdata.ErrNoData) {
|
||||
return misuse(stderr, errors.New("--no-shipped-data needs at least one --data-path"))
|
||||
}
|
||||
})
|
||||
|
||||
if *list {
|
||||
f, err := fejkdata.New(dirs, opts...)
|
||||
if err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
return 1
|
||||
}
|
||||
if in.list {
|
||||
for _, p := range f.List() {
|
||||
fmt.Fprintln(stdout, p)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
if fs.NArg() != 1 {
|
||||
fs.Usage()
|
||||
return 2
|
||||
if err := in.write(f, kind, stdout); err != nil {
|
||||
var te templateError
|
||||
if errors.As(err, &te) {
|
||||
return misuse(stderr, te.error)
|
||||
}
|
||||
if *repeat < 1 {
|
||||
fmt.Fprintln(stderr, "repeat must be a positive integer")
|
||||
return 2
|
||||
}
|
||||
|
||||
path := fs.Arg(0)
|
||||
f, err := fejkdata.New(dirs, opts...)
|
||||
if err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
if errors.Is(err, fejkdata.ErrNoColumns) {
|
||||
fmt.Fprintln(stderr, "wrap that JSON in single quotes, which keep a shell from expanding its braces")
|
||||
}
|
||||
return 1
|
||||
}
|
||||
vals := make([]string, *repeat)
|
||||
for i := range vals {
|
||||
if vals[i], err = f.Fake(path); err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(stdout, strings.Join(vals, *sep))
|
||||
return 0
|
||||
}
|
||||
|
||||
// buildVersion reports the module version stamped into the binary by `go install`
|
||||
// (or "devel" for a local build), read from the build info — no version constant
|
||||
// to bump, no extra dependency.
|
||||
// templateSplit names the cure for a template a shell split on its spaces, which is
|
||||
// what several arguments carrying a token mean.
|
||||
func templateSplit(paths []string) string {
|
||||
for _, p := range paths {
|
||||
if strings.Contains(p, "{") {
|
||||
return `; a template's spaces split the argument, so wrap the whole argument in "…", or in '…' where it carries double quotes`
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func misuse(stderr io.Writer, err error) int {
|
||||
// A library error already names the program, so the prefix is not doubled.
|
||||
fmt.Fprintf(stderr, "fejkdata: %s\ntry 'fejkdata --help'\n", strings.TrimPrefix(err.Error(), "fejkdata: "))
|
||||
return 2
|
||||
}
|
||||
|
||||
// buildVersion is the module version go install stamps into the binary, or "devel".
|
||||
func buildVersion() string {
|
||||
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "" {
|
||||
return info.Main.Version
|
||||
|
||||
+498
-50
@@ -2,6 +2,11 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -9,9 +14,9 @@ import (
|
||||
const (
|
||||
svSE = "../../data/sv_SE"
|
||||
enUS = "../../data/en_US"
|
||||
misc = "../../data/misc"
|
||||
)
|
||||
|
||||
// runOut runs the CLI and returns exit code, stdout, stderr.
|
||||
func runOut(args ...string) (int, string, string) {
|
||||
var out, errb bytes.Buffer
|
||||
code := run(args, &out, &errb)
|
||||
@@ -19,7 +24,7 @@ func runOut(args ...string) (int, string, string) {
|
||||
}
|
||||
|
||||
func TestRunOutputsValue(t *testing.T) {
|
||||
code, out, errb := runOut("-data-path", svSE, "person")
|
||||
code, out, errb := runOut("--data-path", svSE, "person")
|
||||
if code != 0 {
|
||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||
}
|
||||
@@ -32,12 +37,11 @@ func TestRunOutputsValue(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunDotPath(t *testing.T) {
|
||||
// person.last descends to just a surname — never the full "First Last".
|
||||
code, full, _ := runOut("-seed", "7", "-data-path", svSE, "person")
|
||||
code, full, _ := runOut("--seed", "7", "--data-path", svSE, "person")
|
||||
if code != 0 {
|
||||
t.Fatalf("person run = %d", code)
|
||||
}
|
||||
code, last, errb := runOut("-seed", "7", "-data-path", svSE, "person.last")
|
||||
code, last, errb := runOut("--seed", "7", "--data-path", svSE, "person.last")
|
||||
if code != 0 {
|
||||
t.Fatalf("person.last run = %d, stderr=%q", code, errb)
|
||||
}
|
||||
@@ -49,17 +53,123 @@ func TestRunDotPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunSeedDeterministic(t *testing.T) {
|
||||
_, a, _ := runOut("-seed", "42", "-data-path", svSE, "address")
|
||||
_, b, _ := runOut("-seed", "42", "-data-path", svSE, "address")
|
||||
if a != b {
|
||||
t.Errorf("same seed diverged: %q != %q", a, b)
|
||||
func TestRunSeedSpellings(t *testing.T) {
|
||||
_, want, _ := runOut("--seed", "42", "--data-path", svSE, "address")
|
||||
for _, args := range [][]string{
|
||||
{"--seed=42", "--data-path", svSE, "address"},
|
||||
{"-s", "42", "-d", svSE, "address"},
|
||||
{"--data-path", svSE, "address", "--seed", "42"},
|
||||
} {
|
||||
code, got, errb := runOut(args...)
|
||||
if code != 0 {
|
||||
t.Fatalf("run(%v) = %d, stderr=%q", args, code, errb)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("run(%v) = %q, want %q", args, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunShortFlagValues(t *testing.T) {
|
||||
_, want, _ := runOut("--seed", "42", "--data-path", svSE, "address")
|
||||
for _, args := range [][]string{
|
||||
{"-s42", "-d", svSE, "address"},
|
||||
{"-s", "42", "-d" + svSE, "address"},
|
||||
{"-d", svSE, "address", "-s42"},
|
||||
} {
|
||||
code, got, errb := runOut(args...)
|
||||
if code != 0 {
|
||||
t.Fatalf("run(%v) = %d, stderr=%q", args, code, errb)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("run(%v) = %q, want %q", args, got, want)
|
||||
}
|
||||
}
|
||||
_, three, _ := runOut("-s", "1", "-n3", "-d", svSE, "word")
|
||||
if lines := strings.Split(strings.TrimRight(three, "\n"), "\n"); len(lines) != 3 {
|
||||
t.Errorf("-n3 gave %d lines: %q", len(lines), three)
|
||||
}
|
||||
for _, c := range []struct {
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{[]string{"-s=42", "-d", svSE, "address"}, "-s42"},
|
||||
{[]string{"-s=42", "-d", svSE, "address"}, "--seed=42"},
|
||||
{[]string{"-d=" + svSE, "address"}, "--data-path="},
|
||||
{[]string{"-nd", "3", "-d", svSE, "word"}, `--repeat needs an integer in 1..1048576, got "d"`},
|
||||
{[]string{"--seed=", "-d", svSE, "word"}, `--seed needs an unsigned integer, got ""`},
|
||||
} {
|
||||
code, out, errb := runOut(c.args...)
|
||||
if code != 2 || out != "" {
|
||||
t.Errorf("run(%v) = %d, stdout %q, want misuse", c.args, code, out)
|
||||
}
|
||||
if !strings.Contains(errb, c.want) {
|
||||
t.Errorf("run(%v) stderr = %q, want %q", c.args, errb, c.want)
|
||||
}
|
||||
}
|
||||
code, out, _ := runOut("-hd", svSE)
|
||||
if code != 0 || !strings.Contains(out, "Usage") {
|
||||
t.Errorf("-hd (bundled help) = %d, %q, want usage", code, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunDoubleDashEndsFlags(t *testing.T) {
|
||||
code, out, errb := runOut("--data-path", svSE, "--", "person")
|
||||
if code != 0 || strings.TrimSpace(out) == "" {
|
||||
t.Fatalf("run = %d, out=%q, stderr=%q", code, out, errb)
|
||||
}
|
||||
code, _, errb = runOut("--data-path", svSE, "--", "--list")
|
||||
if code != 1 || !strings.Contains(errb, "--list") {
|
||||
t.Errorf("after --, --list should be a path: code %d, stderr %q", code, errb)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunSingleDashLongIsRejected(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{[]string{"-seed", "42", "-d", svSE, "person"}, "use --seed"},
|
||||
{[]string{"-seed=42", "-d", svSE, "person"}, "use --seed"},
|
||||
{[]string{"-data-path", svSE, "person"}, "use --data-path"},
|
||||
{[]string{"-list", "-d", svSE}, "use --list"},
|
||||
{[]string{"--nope", "-d", svSE, "person"}, "unknown flag --nope"},
|
||||
{[]string{"-x", "-d", svSE, "person"}, "unknown flag -x"},
|
||||
} {
|
||||
code, _, errb := runOut(c.args...)
|
||||
if code != 2 {
|
||||
t.Errorf("run(%v) = %d, want 2", c.args, code)
|
||||
}
|
||||
if !strings.Contains(errb, c.want) {
|
||||
t.Errorf("run(%v) stderr = %q, want %q", c.args, errb, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunFlagValues(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{[]string{"--seed", "-d", svSE, "person"}, `--seed needs an unsigned integer, got "-d"`},
|
||||
{[]string{"-d", svSE, "person", "--seed"}, "--seed needs a value"},
|
||||
{[]string{"--seed", "x", "-d", svSE, "person"}, "--seed"},
|
||||
{[]string{"--list=1", "-d", svSE}, "--list takes no value"},
|
||||
{[]string{"--repeat", "0", "-d", svSE, "person"}, "--repeat"},
|
||||
{[]string{"-n", "-1", "-d", svSE, "person"}, "--repeat"},
|
||||
} {
|
||||
code, _, errb := runOut(c.args...)
|
||||
if code != 2 {
|
||||
t.Errorf("run(%v) = %d, want 2", c.args, code)
|
||||
}
|
||||
if !strings.Contains(errb, c.want) {
|
||||
t.Errorf("run(%v) stderr = %q, want %q", c.args, errb, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRepeat(t *testing.T) {
|
||||
// -repeat N prints N values, one per line by default.
|
||||
code, out, errb := runOut("-seed", "1", "-repeat", "3", "-data-path", svSE, "word")
|
||||
code, out, errb := runOut("--seed", "1", "--repeat", "3", "--data-path", svSE, "word")
|
||||
if code != 0 {
|
||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||
}
|
||||
@@ -67,11 +177,14 @@ func TestRunRepeat(t *testing.T) {
|
||||
if len(lines) != 3 {
|
||||
t.Errorf("repeat=3 gave %d lines: %q", len(lines), out)
|
||||
}
|
||||
_, short, _ := runOut("--seed", "1", "-n", "3", "-d", svSE, "word")
|
||||
if short != out {
|
||||
t.Errorf("-n 3 = %q, want the same as --repeat 3 %q", short, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunSeparator(t *testing.T) {
|
||||
// -separator joins the repeated values instead of newlines.
|
||||
code, out, errb := runOut("-repeat", "3", "-separator", ",", "-data-path", svSE, "word")
|
||||
code, out, errb := runOut("--repeat", "3", "--separator", ",", "--data-path", svSE, "word")
|
||||
if code != 0 {
|
||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||
}
|
||||
@@ -84,8 +197,7 @@ func TestRunSeparator(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunRepeatAdvancesRNG(t *testing.T) {
|
||||
// Each repeat is a fresh draw, not the same value N times.
|
||||
code, out, errb := runOut("-seed", "1", "-repeat", "5", "-data-path", svSE, "person")
|
||||
code, out, errb := runOut("--seed", "1", "--repeat", "5", "--data-path", svSE, "person")
|
||||
if code != 0 {
|
||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||
}
|
||||
@@ -98,35 +210,31 @@ func TestRunRepeatAdvancesRNG(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRepeatInvalid(t *testing.T) {
|
||||
// A non-positive repeat is misuse.
|
||||
for _, r := range []string{"0", "-1"} {
|
||||
code, _, errb := runOut("-repeat", r, "-data-path", svSE, "word")
|
||||
if code != 2 {
|
||||
t.Errorf("repeat=%s = %d, want 2", r, code)
|
||||
}
|
||||
if errb == "" {
|
||||
t.Errorf("repeat=%s: want an error message", r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunUsageOnMissingArgs(t *testing.T) {
|
||||
// Need at least one -data-path and exactly one positional path; else misuse.
|
||||
for _, args := range [][]string{{}, {"-data-path", svSE}, {"person"}} {
|
||||
code, _, errb := runOut(args...)
|
||||
func TestRunMisuse(t *testing.T) {
|
||||
for _, args := range [][]string{{}, {"--data-path", svSE}, {"-d", svSE, "person", "word"}, {"-d", svSE, "--list", "person"}, {"--no-shipped-data", "sv_SE.person"}} {
|
||||
code, out, errb := runOut(args...)
|
||||
if code != 2 {
|
||||
t.Errorf("run(%v) = %d, want 2", args, code)
|
||||
}
|
||||
if !strings.Contains(errb, "Usage") {
|
||||
t.Errorf("run(%v) stderr = %q, want usage", args, errb)
|
||||
if !strings.Contains(errb, "try 'fejkdata --help'") {
|
||||
t.Errorf("run(%v) stderr = %q, want a pointer to --help", args, errb)
|
||||
}
|
||||
if out != "" {
|
||||
t.Errorf("run(%v) stdout = %q, want nothing", args, out)
|
||||
}
|
||||
}
|
||||
// A template a shell split names both quotings, since which one cures it turns
|
||||
// on whether the argument carries a double-quoted JSON.
|
||||
_, _, errb := runOut("{date(1990-01-01,2010-12-31,", "January", "2,", "2006)}")
|
||||
for _, want := range []string{`wrap the whole argument in "…"`, `or in '…'`} {
|
||||
if !strings.Contains(errb, want) {
|
||||
t.Errorf("a split template stderr = %q, want it to name %s", errb, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunMultipleDataPaths(t *testing.T) {
|
||||
// -data-path repeats: dirs merge, last wins; the path stays positional.
|
||||
code, out, errb := runOut("-data-path", enUS, "-data-path", svSE, "person")
|
||||
code, out, errb := runOut("-d", enUS, "--data-path", svSE, "person")
|
||||
if code != 0 {
|
||||
t.Fatalf("run(multi-dir) = %d, stderr=%q", code, errb)
|
||||
}
|
||||
@@ -136,7 +244,7 @@ func TestRunMultipleDataPaths(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunUnknownCategoryFails(t *testing.T) {
|
||||
code, _, errb := runOut("-data-path", svSE, "nope")
|
||||
code, _, errb := runOut("--data-path", svSE, "nope")
|
||||
if code != 1 {
|
||||
t.Fatalf("run = %d, want 1", code)
|
||||
}
|
||||
@@ -146,8 +254,7 @@ func TestRunUnknownCategoryFails(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunList(t *testing.T) {
|
||||
// -list prints the discoverable paths and exits 0, no positional path needed.
|
||||
code, out, errb := runOut("-data-path", svSE, "-list")
|
||||
code, out, errb := runOut("--data-path", svSE, "--list")
|
||||
if code != 0 {
|
||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||
}
|
||||
@@ -158,32 +265,31 @@ func TestRunList(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunHelpExitsZero(t *testing.T) {
|
||||
// -h/-help is success (0), not misuse (2), so scripts under `set -e` survive.
|
||||
for _, h := range []string{"-h", "-help"} {
|
||||
code, _, errb := runOut(h)
|
||||
func TestRunHelpOnStdout(t *testing.T) {
|
||||
for _, h := range []string{"-h", "--help"} {
|
||||
code, out, errb := runOut(h)
|
||||
if code != 0 {
|
||||
t.Errorf("%s = %d, want 0", h, code)
|
||||
}
|
||||
if !strings.Contains(errb, "Usage") {
|
||||
t.Errorf("%s: stderr = %q, want usage", h, errb)
|
||||
if !strings.Contains(out, "Usage") || errb != "" {
|
||||
t.Errorf("%s: stdout = %q, stderr = %q, want usage on stdout only", h, out, errb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunVersion(t *testing.T) {
|
||||
code, out, errb := runOut("-version")
|
||||
code, out, errb := runOut("--version")
|
||||
if code != 0 {
|
||||
t.Fatalf("-version = %d, stderr=%q", code, errb)
|
||||
t.Fatalf("--version = %d, stderr=%q", code, errb)
|
||||
}
|
||||
version, ok := strings.CutPrefix(out, "fejkdata ")
|
||||
if !ok || strings.TrimSpace(version) == "" {
|
||||
t.Errorf("-version = %q, want the command name and a version on stdout", out)
|
||||
t.Errorf("--version = %q, want the command name and a version on stdout", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunMissingDirFails(t *testing.T) {
|
||||
code, _, errb := runOut("-data-path", "../../data/nope", "person")
|
||||
code, _, errb := runOut("--data-path", "../../data/nope", "person")
|
||||
if code != 1 {
|
||||
t.Fatalf("run = %d, want 1", code)
|
||||
}
|
||||
@@ -191,3 +297,345 @@ func TestRunMissingDirFails(t *testing.T) {
|
||||
t.Error("want an error message on stderr")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunShippedDataByDefault(t *testing.T) {
|
||||
code, out, errb := runOut("--seed", "1", "sv_SE.person")
|
||||
if code != 0 || strings.TrimSpace(out) == "" {
|
||||
t.Fatalf("run = %d, out=%q, stderr=%q", code, out, errb)
|
||||
}
|
||||
code, list, _ := runOut("--list")
|
||||
if code != 0 || !strings.Contains(list, "en_US.person\n") || !strings.Contains(list, "misc.uuid\n") {
|
||||
t.Errorf("--list without --data-path = %d, %q", code, list)
|
||||
}
|
||||
code, _, errb = runOut("person")
|
||||
if code != 1 || !strings.Contains(errb, "person") {
|
||||
t.Errorf("a category outside the shipped tree: code %d, stderr %q", code, errb)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassify(t *testing.T) {
|
||||
for arg, want := range map[string]argKind{"sv_SE.person": argPath, "name: {x}": argTemplate} {
|
||||
if got, err := classify(arg); err != nil || got != want {
|
||||
t.Errorf("classify(%q) = %v, %v; want %v", arg, got, err, want)
|
||||
}
|
||||
}
|
||||
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 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.territory[SE]"); code != 0 || !strings.HasPrefix(out, "alpha2,") || !strings.Contains(out, "\nSE,SWE,") {
|
||||
t.Fatalf("--format csv misc.territory[SE] = %d, %q", code, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsageReferencesResolve(t *testing.T) {
|
||||
for _, token := range regexp.MustCompile(`\{/[^}]+\}`).FindAllString(usage, -1) {
|
||||
path := token[2 : len(token)-1]
|
||||
code, out, errb := runOut("--seed", "1", path)
|
||||
if code != 0 || strings.TrimSpace(out) == "" {
|
||||
t.Errorf("usage advertises %s: run %s = %d, %q, stderr %q", token, path, code, out, errb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunShapeMisuseBeforeLoad(t *testing.T) {
|
||||
code, _, errb := runOut("--no-shipped-data", "[abc]")
|
||||
if code != 2 || !strings.Contains(errb, "[abc]") || strings.Contains(errb, "--data-path") {
|
||||
t.Fatalf("shape misuse with no data = %d, %q; want the shape error before any load", code, errb)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunInlineTemplate(t *testing.T) {
|
||||
code, out, errb := runOut("--seed", "1", "name: {/sv_SE.person.last}")
|
||||
if code != 0 || !strings.HasPrefix(out, "name: ") || strings.Contains(out, "{") {
|
||||
t.Fatalf("inline format string = %d, %q, stderr %q", code, out, errb)
|
||||
}
|
||||
code, out, errb = runOut("--seed", "1", `{"format":"name: {x}","x":["bosse","lina"]}`)
|
||||
if code != 0 || (out != "name: bosse\n" && out != "name: lina\n") {
|
||||
t.Fatalf("inline JSON template = %d, %q, want one name, stderr %q", code, out, errb)
|
||||
}
|
||||
code, out, errb = runOut("--seed", "1", `"name: {/sv_SE.person.last}"`)
|
||||
if code != 0 || !strings.HasPrefix(out, "name: ") || strings.Contains(out, "{") {
|
||||
t.Fatalf("inline JSON string = %d, %q, stderr %q", code, out, errb)
|
||||
}
|
||||
code, out, errb = runOut("--seed", "1", "-n", "2", `{digits(1)}`)
|
||||
if code != 0 || len(strings.Split(strings.TrimRight(out, "\n"), "\n")) != 2 {
|
||||
t.Fatalf("inline template with --repeat = %d, %q, stderr %q", code, out, errb)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunTemplateMisuse(t *testing.T) {
|
||||
for arg, want := range map[string]string{
|
||||
"{bad": "unterminated",
|
||||
"[red,green]": `starts with "["`,
|
||||
`{"format":"x"}`: "is a string",
|
||||
"name: {/no.such.path}": "no entry",
|
||||
"{/sv_SE.person}": "write sv_SE.person",
|
||||
"x[1]y": `"]"`,
|
||||
` ["a","b"] `: "may not be padded",
|
||||
} {
|
||||
code, out, errb := runOut("--seed", "1", arg)
|
||||
if code != 2 || out != "" || !strings.Contains(errb, "try 'fejkdata --help'") || !strings.Contains(errb, want) {
|
||||
t.Errorf("run(%q) = %d, %q, %q; want misuse naming %q and --help", arg, code, out, errb, want)
|
||||
}
|
||||
if strings.Contains(errb, "fejkdata: fejkdata:") {
|
||||
t.Errorf("run(%q) doubled the program prefix: %q", arg, errb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunNoShippedData(t *testing.T) {
|
||||
code, list, errb := runOut("--no-shipped-data", "-d", misc, "--list")
|
||||
if code != 0 {
|
||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||
}
|
||||
if strings.Contains(list, "sv_SE") || !strings.Contains(list, "uuid\n") {
|
||||
t.Errorf("--no-shipped-data --list = %q, want only the given dir", list)
|
||||
}
|
||||
code, out, _ := runOut("--no-shipped-data", "-d", misc, "-s", "3", "uuid")
|
||||
if code != 0 || strings.TrimSpace(out) == "" {
|
||||
t.Errorf("run = %d, out=%q", code, out)
|
||||
}
|
||||
code, _, errb = runOut("--no-shipped-data", "sv_SE.person")
|
||||
if code != 2 || !strings.Contains(errb, "--no-shipped-data needs at least one --data-path") {
|
||||
t.Errorf("--no-shipped-data alone = %d, %q, want misuse naming --data-path", code, errb)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRepeatIsBounded(t *testing.T) {
|
||||
for _, args := range [][]string{
|
||||
{"--repeat", "9223372036854775807", "sv_SE.word"},
|
||||
{"--repeat", "1048577", "sv_SE.word"},
|
||||
{"-n", "99999999999", "sv_SE.word"},
|
||||
} {
|
||||
code, out, errb := runOut(args...)
|
||||
if code != 2 || out != "" || !strings.Contains(errb, "1..1048576") {
|
||||
t.Errorf("run(%v) = %d, %q, %q, want misuse naming the range", args, code, out, errb)
|
||||
}
|
||||
}
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "x.json"), []byte(`"x"`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
code, out, errb := runOut("--no-shipped-data", "-d", dir, "--repeat", "1048576", "--separator", "", "x")
|
||||
if code != 0 || len(out) != 1048576+1 {
|
||||
t.Errorf("repeat at the cap = %d, %d bytes, stderr %q; want every render streamed", code, len(out), errb)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunListTakesNoRepeatOrSeparator(t *testing.T) {
|
||||
for _, args := range [][]string{{"--list", "-n", "3"}, {"--list", "--separator", ","}} {
|
||||
code, _, errb := runOut(args...)
|
||||
if code != 2 || !strings.Contains(errb, "--list takes no") {
|
||||
t.Errorf("run(%v) = %d, %q, want misuse", args, code, errb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunUnknownFlagIsNamedByRune(t *testing.T) {
|
||||
code, _, errb := runOut("-ä", "sv_SE.word")
|
||||
if code != 2 || !strings.Contains(errb, "unknown flag -ä") {
|
||||
t.Errorf("run(-ä) = %d, %q, want the flag named whole", code, errb)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunEmptyDataPathIsNamed(t *testing.T) {
|
||||
code, _, errb := runOut("--data-path=", "sv_SE.word")
|
||||
if code != 1 || !strings.Contains(errb, "empty") {
|
||||
t.Errorf("run(--data-path=) = %d, %q, want the empty path named", code, errb)
|
||||
}
|
||||
}
|
||||
|
||||
func recordDir(t *testing.T) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
content := `{"format":"{first} {last}","first":["Ada","Bo"],"last":["Lovelace","Ek"]}`
|
||||
if err := os.WriteFile(filepath.Join(dir, "users.json"), []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func TestRunRecordJSON(t *testing.T) {
|
||||
code, out, errb := runOut("--seed", "1", "--format", "json", "--repeat", "2", "--data-path", recordDir(t), "users")
|
||||
if code != 0 {
|
||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||
}
|
||||
var arr []map[string]string
|
||||
if err := json.Unmarshal([]byte(out), &arr); err != nil {
|
||||
t.Fatalf("json output is not one JSON array: %v\n%q", err, out)
|
||||
}
|
||||
if len(arr) != 2 || len(arr[0]) != 2 || arr[0]["first"] == "" || arr[0]["last"] == "" {
|
||||
t.Fatalf("json output = %q, want an array of two records with first and last columns", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRecordNDJSON(t *testing.T) {
|
||||
code, out, errb := runOut("--seed", "1", "--format", "ndjson", "--repeat", "2", "--data-path", recordDir(t), "users")
|
||||
if code != 0 {
|
||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||
}
|
||||
lines := strings.Split(strings.TrimRight(out, "\n"), "\n")
|
||||
if len(lines) != 2 {
|
||||
t.Fatalf("ndjson output = %d lines %q, want one object per line", len(lines), out)
|
||||
}
|
||||
for _, line := range lines {
|
||||
var m map[string]string
|
||||
if err := json.Unmarshal([]byte(line), &m); err != nil || len(m) != 2 {
|
||||
t.Fatalf("ndjson line %q is not one object: %v", line, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRecordCSVRoundTrips(t *testing.T) {
|
||||
code, out, errb := runOut("--seed", "1", "--format", "csv", "--repeat", "3", "--data-path", recordDir(t), "users")
|
||||
if code != 0 {
|
||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||
}
|
||||
lines := strings.Split(strings.TrimRight(out, "\n"), "\n")
|
||||
if len(lines) != 4 {
|
||||
t.Fatalf("csv output = %d lines %q, want a header and 3 rows", len(lines), out)
|
||||
}
|
||||
if lines[0] != "first,last" {
|
||||
t.Fatalf("csv header = %q, want first,last", lines[0])
|
||||
}
|
||||
r, err := csv.NewReader(strings.NewReader(out)).ReadAll()
|
||||
if err != nil {
|
||||
t.Fatalf("csv output is not valid CSV: %v", err)
|
||||
}
|
||||
if len(r) != 4 || len(r[0]) != 2 {
|
||||
t.Fatalf("csv parsed to %v, want 4 records of 2 fields", r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRecordSQL(t *testing.T) {
|
||||
code, out, errb := runOut("--seed", "1", "--format", "sql", "--repeat", "2", "--table", "people", "--data-path", recordDir(t), "users")
|
||||
if code != 0 {
|
||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||
}
|
||||
lines := strings.Split(strings.TrimRight(out, "\n"), "\n")
|
||||
if len(lines) != 2 || !strings.HasPrefix(lines[0], `INSERT INTO "people" ("first", "last") VALUES (`) {
|
||||
t.Fatalf("sql output = %q, want two INSERT INTO people statements", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRecordDefaultTable(t *testing.T) {
|
||||
code, out, errb := runOut("--seed", "1", "--format", "sql", "--data-path", recordDir(t), "users")
|
||||
if code != 0 || !strings.HasPrefix(out, `INSERT INTO "users" (`) {
|
||||
t.Fatalf("default table = %d, %q, want the path's last segment; stderr %q", code, out, errb)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRecordInlineTemplate(t *testing.T) {
|
||||
code, out, errb := runOut("--seed", "1", "--format", "json", `{"format":"{x}","x":["a","b"]}`)
|
||||
if code != 0 {
|
||||
t.Fatalf("inline record = %d, stderr=%q", code, errb)
|
||||
}
|
||||
var arr []map[string]string
|
||||
if err := json.Unmarshal([]byte(out), &arr); err != nil || len(arr) != 1 || (arr[0]["x"] != "a" && arr[0]["x"] != "b") {
|
||||
t.Fatalf("inline record json = %q, want one record with a column x (err %v)", out, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRecordMisuse(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{[]string{"--format", "yaml", "users"}, "--format takes csv, json, ndjson, sql or text"},
|
||||
{[]string{"--format", "json", "--separator", ",", "users"}, "--separator joins text values"},
|
||||
{[]string{"--table", "t", "users"}, "--table names the INSERT target"},
|
||||
{[]string{"--format", "json", "--table", "t", "users"}, "--table names the INSERT target"},
|
||||
{[]string{"--format", "sql", "--table", "", "users"}, "--table names the INSERT target"},
|
||||
} {
|
||||
code, out, errb := runOut(c.args...)
|
||||
if code != 2 || out != "" || !strings.Contains(errb, c.want) {
|
||||
t.Errorf("run(%v) = %d, %q, %q; want misuse naming %q", c.args, code, out, errb, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRecordOnABareValueIsRuntimeError(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "word.json"), []byte(`["a","b"]`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
code, _, errb := runOut("--format", "json", "--data-path", dir, "word")
|
||||
if code != 1 {
|
||||
t.Fatalf("record on a choice = %d, want exit 1 (runtime error), stderr %q", code, errb)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRecordRejectsATopLevelRepeat(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
content := `{"format":"{a}-","repeat":3,"separator":"|","a":["x","y"]}`
|
||||
if err := os.WriteFile(filepath.Join(dir, "rep.json"), []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
code, _, errb := runOut("--format", "json", "--data-path", dir, "rep")
|
||||
if code != 1 || !strings.Contains(errb, "carries repeat 3") {
|
||||
t.Errorf("record on a repeating path = %d, %q; want exit 1 naming the repeat", code, errb)
|
||||
}
|
||||
code, _, errb = runOut("--format", "json", content)
|
||||
if code != 2 || !strings.Contains(errb, "carries repeat 3") {
|
||||
t.Errorf("record on a repeating inline template = %d, %q; want exit 2 naming the repeat", code, errb)
|
||||
}
|
||||
if code, out, _ := runOut("--seed", "1", "--data-path", dir, "rep"); code != 0 || strings.TrimSpace(out) != "x-|x-|x-" {
|
||||
t.Errorf("text view = %d, %q; want the repeat still composed", code, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunNumbersFollowTheShell(t *testing.T) {
|
||||
_, want, _ := runOut("--seed", "7", "--repeat", "3", "sv_SE.word")
|
||||
code, got, errb := runOut("--seed", "007", "--repeat", "+3", "sv_SE.word")
|
||||
if code != 0 || got != want {
|
||||
t.Errorf("run(--seed 007 --repeat +3) = %d, %q, stderr %q; want the same as --seed 7 --repeat 3 %q", code, got, errb, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunRecordOfAFieldlessPath pins the way out of asking a category of one value
|
||||
// for columns: the record to write, and the quoting a shell needs to take it. The
|
||||
// argument is well-formed and only the data cannot serve it, so the code is 1.
|
||||
func TestRunRecordOfAFieldlessPath(t *testing.T) {
|
||||
code, out, errb := runOut("--format", "csv", "sv_SE.personnummer")
|
||||
if code != 1 {
|
||||
t.Fatalf("run(--format csv sv_SE.personnummer) = %d, want 1", code)
|
||||
}
|
||||
for _, want := range []string{`{"format":"","personnummer":"{/sv_SE.personnummer}"}`, "single quotes"} {
|
||||
if !strings.Contains(errb, want) {
|
||||
t.Errorf("stderr = %q, want it to name %s", errb, want)
|
||||
}
|
||||
}
|
||||
if out != "" {
|
||||
t.Errorf("stdout = %q, want nothing", out)
|
||||
}
|
||||
}
|
||||
|
||||
+13
-1
@@ -24,11 +24,12 @@ x-go: &go
|
||||
HOME: /cache
|
||||
GOCACHE: /cache/build
|
||||
GOMODCACHE: /cache/mod
|
||||
REPIN: ${REPIN:-}
|
||||
|
||||
services:
|
||||
test:
|
||||
<<: *go
|
||||
command: go test ./...
|
||||
command: go test -race ./...
|
||||
|
||||
cover:
|
||||
<<: *go
|
||||
@@ -42,6 +43,10 @@ services:
|
||||
<<: *go
|
||||
command: go vet ./...
|
||||
|
||||
cyclo:
|
||||
<<: *go
|
||||
command: go run github.com/fzipp/gocyclo/cmd/gocyclo@v0.6.0 -over 14 -ignore _test .
|
||||
|
||||
build:
|
||||
<<: *go
|
||||
command: go build ./...
|
||||
@@ -59,5 +64,12 @@ services:
|
||||
<<: *go
|
||||
command: sh
|
||||
|
||||
data-import:
|
||||
image: python:3.14.7-slim
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- .:/app
|
||||
entrypoint: python3
|
||||
|
||||
volumes:
|
||||
gocache:
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/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 ...] [--cache DIR] [--out FILE]
|
||||
|
||||
The symbols come from the first locale file that has one, narrow symbols before wide.
|
||||
"""
|
||||
import argparse
|
||||
import csv
|
||||
import io
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
import source
|
||||
import tsv
|
||||
|
||||
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"
|
||||
CACHE = Path(__file__).resolve().parent / "cache"
|
||||
COLUMNS = ["code", "decimals", "name", "numeric", "symbol"]
|
||||
|
||||
|
||||
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 main():
|
||||
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
p.add_argument("--cache", default=str(CACHE))
|
||||
p.add_argument("--source", default=SOURCE)
|
||||
p.add_argument("--symbols", nargs="+", default=SYMBOLS)
|
||||
p.add_argument("--out", default=str(OUT))
|
||||
a = p.parse_args()
|
||||
symbol = symbols(source.fetch(s, a.cache, Path(s).name).decode("utf-8") for s in a.symbols)
|
||||
table = rows(source.fetch(a.source, a.cache, "codes-all.csv").decode("utf-8"), symbol)
|
||||
tsv.write(a.out, COLUMNS, sorted(table, key=lambda r: r["code"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,234 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild data/geo/SE/*.tsv from SCB (CC0), GeoNames (CC BY 4.0) and Trafikverket NVDB (CC0).
|
||||
|
||||
TRAFIKVERKET_API_KEY=… data-import/geo-se.py [--key-file FILE] [--cache DIR] [--streets-per-locality N] [--out DIR]
|
||||
"""
|
||||
import argparse
|
||||
import collections
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import source
|
||||
import tsv
|
||||
import xlsx
|
||||
|
||||
CODES = "https://www.scb.se/contentassets/7a89e48960f741e08918e489ea36354a/kommunlankod-2026.xlsx"
|
||||
POPULATION = "https://api.scb.se/OV0104/v1/doris/sv/ssd/START/BE/BE0101/BE0101A/BefolkningNy"
|
||||
POPULATION_QUERY = {
|
||||
"query": [
|
||||
{"code": "Region", "selection": {"filter": "all", "values": ["*"]}},
|
||||
{"code": "ContentsCode", "selection": {"filter": "item", "values": ["BE0101N1"]}},
|
||||
{"code": "Tid", "selection": {"filter": "top", "values": ["1"]}},
|
||||
],
|
||||
"response": {"format": "json"},
|
||||
}
|
||||
TATORTER = "https://geodata.scb.se/geoserver/stat/wfs?service=WFS&version=2.0.0&request=GetFeature&typeNames=stat:Tatorter_2023&outputFormat=csv&propertyName=tatort,kommun,bef"
|
||||
POSTAL_CODES = "https://download.geonames.org/export/zip/SE.zip"
|
||||
NVDB = "https://api.trafikinfo.trafikverket.se/v2/data.json"
|
||||
NVDB_PAGE = 50000
|
||||
OUT = Path(__file__).resolve().parent.parent / "data" / "geo" / "SE"
|
||||
CACHE = Path(__file__).resolve().parent / "cache"
|
||||
TIMEZONE = "Europe/Stockholm"
|
||||
ONE_POSITION = {"Stockholm", "Göteborg", "Malmö"}
|
||||
UNMATCHED_POPULATION = 200
|
||||
|
||||
|
||||
def scb_codes(cache):
|
||||
regions, municipalities = {}, {}
|
||||
for cells in xlsx.rows(source.fetch(CODES, cache, "kommunlankod.xlsx", magic=b"PK")):
|
||||
if len(cells) < 2 or not re.fullmatch(r"\d{2}|\d{4}", cells[0]):
|
||||
continue
|
||||
(regions if len(cells[0]) == 2 else municipalities)[cells[0]] = cells[1].strip()
|
||||
return regions, municipalities
|
||||
|
||||
|
||||
def scb_population(cache):
|
||||
body = json.dumps(POPULATION_QUERY).encode()
|
||||
data = source.fetch(POPULATION, cache, "befolkning.json", data=body, headers={"Content-Type": "application/json"})
|
||||
return {row["key"][0]: row["values"][0] for row in json.loads(data.decode("utf-8-sig"))["data"]}
|
||||
|
||||
|
||||
def scb_tatorter(cache):
|
||||
text = source.fetch(TATORTER, cache, "tatorter.csv").decode("utf-8")
|
||||
by_name = collections.defaultdict(list)
|
||||
for r in csv.DictReader(io.StringIO(text)):
|
||||
by_name[r["tatort"]].append((r["kommun"], int(r["bef"])))
|
||||
return by_name
|
||||
|
||||
|
||||
def geonames(cache):
|
||||
z = zipfile.ZipFile(io.BytesIO(source.fetch(POSTAL_CODES, cache, "SE.zip", magic=b"PK")))
|
||||
rows = []
|
||||
for line in z.read("SE.txt").decode("utf-8").splitlines():
|
||||
f = line.split("\t")
|
||||
lat, lon = (float(f[9]), float(f[10])) if f[9] and f[10] else (None, None)
|
||||
rows.append({"code": f[1], "locality": f[2], "municipality": f[6], "lat": lat, "lon": lon})
|
||||
return rows
|
||||
|
||||
|
||||
def nvdb_segments(cache, key):
|
||||
path = cache / "nvdb-gatunamn.tsv"
|
||||
if not path.exists():
|
||||
with open(path.with_suffix(".part"), "w", encoding="utf-8") as out:
|
||||
change = "0"
|
||||
while True:
|
||||
query = (
|
||||
f'<REQUEST><LOGIN authenticationkey="{key}"/>'
|
||||
f'<QUERY objecttype="Gatunamn" namespace="vägdata.nvdb_dk_o" schemaversion="1.0" limit="{NVDB_PAGE}" changeid="{change}">'
|
||||
"<FILTER><EQ name=\"Deleted\" value=\"false\"/></FILTER>"
|
||||
"<INCLUDE>Namn</INCLUDE><INCLUDE>Geometry.WKT-WGS84-3D</INCLUDE></QUERY></REQUEST>"
|
||||
)
|
||||
req = urllib.request.Request(NVDB, data=query.encode(), headers={"Content-Type": "text/xml"})
|
||||
with urllib.request.urlopen(req, timeout=600) as r:
|
||||
result = json.load(r)["RESPONSE"]["RESULT"][0]
|
||||
rows = result.get("Gatunamn", [])
|
||||
for row in rows:
|
||||
m = re.match(r"LINESTRING Z \(([-\d.]+) ([-\d.]+) ", row.get("Geometry", {}).get("WKT-WGS84-3D", ""))
|
||||
name = " ".join(row.get("Namn", "").split())
|
||||
if m and name:
|
||||
out.write(f"{name}\t{m.group(1)}\t{m.group(2)}\n")
|
||||
change = result["INFO"]["LASTCHANGEID"]
|
||||
if len(rows) < NVDB_PAGE:
|
||||
break
|
||||
path.with_suffix(".part").rename(path)
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
name, lon, lat = line.split("\t")
|
||||
yield name, float(lat), float(lon)
|
||||
|
||||
|
||||
class Nearest:
|
||||
"""Nearest point by an equirectangular distance, over a degree grid."""
|
||||
|
||||
def __init__(self, points, cell=0.05):
|
||||
self.cell = cell
|
||||
self.grid = collections.defaultdict(list)
|
||||
for lat, lon, value in points:
|
||||
self.grid[(int(lat // cell), int(lon // cell))].append((lat, lon, value))
|
||||
|
||||
def find(self, lat, lon):
|
||||
ci, cj = int(lat // self.cell), int(lon // self.cell)
|
||||
best, best_d = None, math.inf
|
||||
ring = 0
|
||||
while ring < 400:
|
||||
for i in range(ci - ring, ci + ring + 1):
|
||||
for j in range(cj - ring, cj + ring + 1):
|
||||
if max(abs(i - ci), abs(j - cj)) != ring:
|
||||
continue
|
||||
for plat, plon, value in self.grid.get((i, j), ()):
|
||||
d = (plat - lat) ** 2 + ((plon - lon) * math.cos(math.radians(lat))) ** 2
|
||||
if d < best_d:
|
||||
best, best_d = value, d
|
||||
if best is not None and math.sqrt(best_d) < ring * self.cell * math.cos(math.radians(lat)):
|
||||
return best
|
||||
ring += 1
|
||||
return best
|
||||
|
||||
|
||||
def street_delivery(name, codes):
|
||||
"""The codes delivered to a street: the digit after the postort's own prefix says box, company or reply."""
|
||||
if name in ONE_POSITION:
|
||||
return [c for c in codes if c[1] != "0"]
|
||||
largest = collections.Counter(c[:3] for c in codes).most_common(1)[0][1]
|
||||
length = 3 if largest * 2 >= len(codes) else 2
|
||||
return [c for c in codes if c[length] not in "018"]
|
||||
|
||||
|
||||
def municipality_of(name, rows, tatorter, municipalities):
|
||||
named = [code for code, n in municipalities.items() if n == name]
|
||||
if named:
|
||||
return named[0], "kommun"
|
||||
voted = collections.Counter(r["municipality"] for r in rows if r["municipality"] in municipalities)
|
||||
matches = tatorter.get(name, [])
|
||||
if matches:
|
||||
in_vote = [m for m in matches if voted and m[0] == voted.most_common(1)[0][0]]
|
||||
return max(in_vote or matches, key=lambda m: m[1])[0], "tatort"
|
||||
if voted:
|
||||
return voted.most_common(1)[0][0], "codes"
|
||||
return None, "unplaced"
|
||||
|
||||
|
||||
def population_of(name, municipality, tatorter, municipalities, population):
|
||||
matches = [m for m in tatorter.get(name, []) if m[0] == municipality]
|
||||
if matches:
|
||||
return str(max(m[1] for m in matches))
|
||||
return population[municipality] if municipalities[municipality] == name else str(UNMATCHED_POPULATION)
|
||||
|
||||
|
||||
def well_cased(name):
|
||||
return all(part[:1].isupper() and (len(part) == 1 or not part.isupper()) for part in re.split(r"[ -]", name))
|
||||
|
||||
|
||||
def localities(codes, tatorter, municipalities, population):
|
||||
"""Each postort with its municipality, weight, centroid and street-delivery codes."""
|
||||
by_locality = collections.defaultdict(list)
|
||||
for r in codes:
|
||||
by_locality[r["locality"]].append(r)
|
||||
out, how = {}, collections.Counter()
|
||||
for name, rows in by_locality.items():
|
||||
municipality, method = municipality_of(name, rows, tatorter, municipalities)
|
||||
how[method] += 1
|
||||
kept = street_delivery(name, [r["code"].replace(" ", "") for r in rows])
|
||||
with_point = [r for r in rows if r["lat"] is not None]
|
||||
if municipality is None or not kept or not with_point or not well_cased(name):
|
||||
continue
|
||||
lat = sum(r["lat"] for r in with_point) / len(with_point)
|
||||
lon = sum(r["lon"] for r in with_point) / len(with_point)
|
||||
out[name] = {"name": name, "municipality": municipality, "population": population_of(name, municipality, tatorter, municipalities, population), "lat": f"{lat:.4f}", "lon": f"{lon:.4f}", "codes": kept}
|
||||
print(f"municipality by {dict(how)}; {len(by_locality) - len(out)} postorter dropped", file=sys.stderr)
|
||||
return out
|
||||
|
||||
|
||||
def streets(segments, codes, localities, per_locality):
|
||||
"""The names with most segments per locality, each segment at its nearest code centroid."""
|
||||
nearest = Nearest((r["lat"], r["lon"], r["locality"]) for r in codes if r["lat"] is not None and r["locality"] in localities)
|
||||
count = collections.Counter()
|
||||
for name, lat, lon in segments:
|
||||
if name[0].isalpha():
|
||||
count[(nearest.find(lat, lon), name)] += 1
|
||||
of = collections.defaultdict(list)
|
||||
for (locality, name), n in count.items():
|
||||
of[locality].append((n, name))
|
||||
return {locality: [{"name": name, "locality": locality, "segments": n} for n, name in sorted(named, key=lambda s: (-s[0], s[1]))[:per_locality]] for locality, named in of.items()}
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
p.add_argument("--cache", default=str(CACHE))
|
||||
p.add_argument("--key-file", help="file holding the Trafikverket API key; TRAFIKVERKET_API_KEY otherwise")
|
||||
p.add_argument("--out", default=str(OUT))
|
||||
p.add_argument("--streets-per-locality", type=int, default=10)
|
||||
a = p.parse_args()
|
||||
key = Path(a.key_file).read_text().strip() if a.key_file else os.environ.get("TRAFIKVERKET_API_KEY")
|
||||
if not key:
|
||||
sys.exit("set TRAFIKVERKET_API_KEY or pass --key-file")
|
||||
cache, out = Path(a.cache), Path(a.out)
|
||||
cache.mkdir(parents=True, exist_ok=True)
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
regions, municipalities = scb_codes(cache)
|
||||
population = scb_population(cache)
|
||||
codes = geonames(cache)
|
||||
places = localities(codes, scb_tatorter(cache), municipalities, population)
|
||||
named = streets(nvdb_segments(cache, key), codes, places, a.streets_per_locality)
|
||||
places = {name: l for name, l in places.items() if name in named}
|
||||
empty = sorted(m for m in municipalities if not any(l["municipality"] == m for l in places.values()))
|
||||
if empty:
|
||||
sys.exit(f"municipalities without a locality: {empty}")
|
||||
|
||||
tsv.write(out / "region.tsv", ["code", "name", "population", "timezone"], [{"code": c, "name": n, "population": population[c], "timezone": TIMEZONE} for c, n in sorted(regions.items())])
|
||||
tsv.write(out / "municipality.tsv", ["code", "name", "region", "population"], [{"code": c, "name": n, "region": c[:2], "population": population[c]} for c, n in sorted(municipalities.items())])
|
||||
tsv.write(out / "locality.tsv", ["name", "municipality", "population", "lat", "lon"], [l for _, l in sorted(places.items())])
|
||||
tsv.write(out / "postal-code.tsv", ["code", "locality"], sorted(({"code": f"{c[:3]} {c[3:]}", "locality": l["name"]} for l in places.values() for c in l["codes"]), key=lambda r: r["code"]))
|
||||
tsv.write(out / "street.tsv", ["name", "locality", "segments"], [s for locality in sorted(named) for s in named[locality]])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild data/geo/US/*.tsv from the Census Bureau's Gazetteer, population estimates, ZCTA relationships and TIGER/Line files (public domain).
|
||||
|
||||
data-import/geo-us.py [--cache DIR] [--min-population N] [--streets-per-locality N] [--out DIR]
|
||||
"""
|
||||
import argparse
|
||||
import collections
|
||||
import concurrent.futures
|
||||
import csv
|
||||
import io
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import source
|
||||
import tsv
|
||||
|
||||
GAZETTEER = "https://www2.census.gov/geo/docs/maps-data/data/gazetteer/2026_Gazetteer/2026_Gaz_{}_national.zip"
|
||||
POPULATION = "https://www2.census.gov/programs-surveys/popest/datasets/2020-2025/{}"
|
||||
STATES = POPULATION.format("state/totals/NST-EST2025-ALLDATA.csv")
|
||||
COUNTIES = POPULATION.format("counties/totals/co-est2025-alldata.csv")
|
||||
PLACES = POPULATION.format("cities/totals/sub-est2025.csv")
|
||||
ZCTA_PLACE = "https://www2.census.gov/geo/docs/maps-data/data/rel2020/zcta520/tab20_zcta520_place20_natl.txt"
|
||||
TIGER = "https://www2.census.gov/geo/tiger/TIGER2025/{0}/tl_2025_{1}_{2}.zip"
|
||||
OUT = Path(__file__).resolve().parent.parent / "data" / "geo" / "US"
|
||||
CACHE = Path(__file__).resolve().parent / "cache"
|
||||
ESTIMATE = "POPESTIMATE2025"
|
||||
CDP = "57"
|
||||
HIGHWAY = re.compile(r"\b(I- |Hwy |Highway |Loop |Rte |Route |Rd )\d")
|
||||
SUFFIX = re.compile(r" (city and borough|city|town|village|borough|municipality|comunidad|zona urbana|metropolitan government|metro government|consolidated government|unified government|urban county|corporation|plantation)( \(balance\))?$")
|
||||
# Places whose Census name is a merged government's; the postal city is what an address carries.
|
||||
NAMES = {"1303440": "Athens", "1304204": "Augusta", "1349008": "Macon", "2148006": "Louisville", "3011397": "Butte", "4732742": "Hartsville", "4752006": "Nashville"}
|
||||
TIMEZONES = {
|
||||
"AK": "America/Anchorage", "AL": "America/Chicago", "AR": "America/Chicago", "AZ": "America/Phoenix",
|
||||
"CA": "America/Los_Angeles", "CO": "America/Denver", "CT": "America/New_York", "DC": "America/New_York",
|
||||
"DE": "America/New_York", "FL": "America/New_York", "GA": "America/New_York", "HI": "Pacific/Honolulu",
|
||||
"IA": "America/Chicago", "ID": "America/Boise", "IL": "America/Chicago", "IN": "America/Indiana/Indianapolis",
|
||||
"KS": "America/Chicago", "KY": "America/New_York", "LA": "America/Chicago", "MA": "America/New_York",
|
||||
"MD": "America/New_York", "ME": "America/New_York", "MI": "America/Detroit", "MN": "America/Chicago",
|
||||
"MO": "America/Chicago", "MS": "America/Chicago", "MT": "America/Denver", "NC": "America/New_York",
|
||||
"ND": "America/Chicago", "NE": "America/Chicago", "NH": "America/New_York", "NJ": "America/New_York",
|
||||
"NM": "America/Denver", "NV": "America/Los_Angeles", "NY": "America/New_York", "OH": "America/New_York",
|
||||
"OK": "America/Chicago", "OR": "America/Los_Angeles", "PA": "America/New_York", "PR": "America/Puerto_Rico",
|
||||
"RI": "America/New_York", "SC": "America/New_York", "SD": "America/Chicago", "TN": "America/Chicago",
|
||||
"TX": "America/Chicago", "UT": "America/Denver", "VA": "America/New_York", "VT": "America/New_York",
|
||||
"WA": "America/Los_Angeles", "WI": "America/Chicago", "WV": "America/New_York", "WY": "America/Denver",
|
||||
}
|
||||
|
||||
|
||||
def text(data):
|
||||
try:
|
||||
return data.decode("utf-8-sig")
|
||||
except UnicodeDecodeError:
|
||||
return data.decode("latin-1")
|
||||
|
||||
|
||||
def gazetteer(cache, kind):
|
||||
z = zipfile.ZipFile(io.BytesIO(source.fetch(GAZETTEER.format(kind), cache, f"gaz_{kind}.zip", magic=b"PK")))
|
||||
rows = text(z.read(z.namelist()[0])).splitlines()
|
||||
header = [h.strip() for h in rows[0].split("|")]
|
||||
return [dict(zip(header, (c.strip() for c in row.split("|")))) for row in rows[1:]]
|
||||
|
||||
|
||||
def csv_rows(cache, url, name):
|
||||
return list(csv.DictReader(io.StringIO(text(source.fetch(url, cache, name)))))
|
||||
|
||||
|
||||
def dbf_rows(data, wanted):
|
||||
"""The records of a dBASE file, the wanted fields only."""
|
||||
count, header_len, record_len = struct.unpack("<xxxxIHH", data[:12])
|
||||
fields, pos = [], 32
|
||||
while data[pos] != 0x0D:
|
||||
name = data[pos:pos + 11].split(b"\0")[0].decode()
|
||||
fields.append((name, data[pos + 16]))
|
||||
pos += 32
|
||||
pos = header_len
|
||||
for _ in range(count):
|
||||
record = data[pos:pos + record_len]
|
||||
pos += record_len
|
||||
if record[:1] == b"*":
|
||||
continue
|
||||
row, at = {}, 1
|
||||
for name, length in fields:
|
||||
if name in wanted:
|
||||
row[name] = record[at:at + length].decode("utf-8", "replace").strip()
|
||||
at += length
|
||||
yield row
|
||||
|
||||
|
||||
def tiger_zip(cache, kind, county):
|
||||
return source.fetch(TIGER.format(kind.upper(), county, kind), cache, f"tl_{county}_{kind}.zip", magic=b"PK")
|
||||
|
||||
|
||||
def tiger(cache, kind, county, wanted):
|
||||
z = zipfile.ZipFile(io.BytesIO(tiger_zip(cache, kind, county)))
|
||||
return dbf_rows(z.read(f"tl_2025_{county}_{kind}.dbf"), wanted)
|
||||
|
||||
|
||||
def place_name(geoid, name):
|
||||
if geoid in NAMES:
|
||||
return NAMES[geoid]
|
||||
stripped = SUFFIX.sub("", name)
|
||||
if stripped == name:
|
||||
print(f"{geoid}: no suffix stripped from {name!r}", file=sys.stderr)
|
||||
return stripped
|
||||
|
||||
|
||||
def localities(cache, min_population, counties):
|
||||
"""Each shipped place with its county, population and centroid."""
|
||||
place_population, county_part = {}, collections.defaultdict(list)
|
||||
for r in csv_rows(cache, PLACES, "sub-est2025.csv"):
|
||||
if r["SUMLEV"] == "162":
|
||||
place_population[r["STATE"] + r["PLACE"]] = int(r[ESTIMATE])
|
||||
elif r["SUMLEV"] == "157":
|
||||
county_part[r["STATE"] + r["PLACE"]].append((int(r[ESTIMATE]), r["STATE"] + r["COUNTY"]))
|
||||
out = {}
|
||||
for r in gazetteer(cache, "place"):
|
||||
geoid, population = r["GEOID"], place_population.get(r["GEOID"], 0)
|
||||
if r["FUNCSTAT"] not in ("A", "F", "N") or r["LSAD"] == CDP or population < min_population or not county_part.get(geoid):
|
||||
continue
|
||||
county = max(county_part[geoid])[1]
|
||||
if county not in counties:
|
||||
print(f"{geoid} {r['NAME']}: county {county} unknown, dropped", file=sys.stderr)
|
||||
continue
|
||||
out[geoid] = {"code": geoid, "name": place_name(geoid, r["NAME"]), "municipality": county, "population": population, "lat": r["INTPTLAT"], "lon": r["INTPTLONG"]}
|
||||
return out
|
||||
|
||||
|
||||
def postal_codes(cache, localities):
|
||||
"""Each ZCTA whose largest part inside an incorporated place lies in a shipped place."""
|
||||
parts = {}
|
||||
for r in csv.DictReader(io.StringIO(text(source.fetch(ZCTA_PLACE, cache, "zcta-place.txt"))), delimiter="|"):
|
||||
if r["GEOID_ZCTA5_20"] and r["GEOID_PLACE_20"] and not r["NAMELSAD_PLACE_20"].endswith(" CDP"):
|
||||
parts.setdefault(r["GEOID_ZCTA5_20"], []).append((int(r["AREALAND_PART"]), r["GEOID_PLACE_20"]))
|
||||
largest = {zcta: max(p)[1] for zcta, p in parts.items()}
|
||||
return {zcta: place for zcta, place in largest.items() if place in localities}
|
||||
|
||||
|
||||
def streets(cache, counties, locality_of_zcta, per_locality):
|
||||
"""The names with most TIGER address ranges per place, and the address ranges per ZCTA."""
|
||||
with concurrent.futures.ThreadPoolExecutor(3) as pool:
|
||||
list(pool.map(lambda c: (tiger_zip(cache, "addr", c), tiger_zip(cache, "featnames", c)), counties))
|
||||
addresses, count = collections.Counter(), collections.Counter()
|
||||
for county in counties:
|
||||
zips = collections.defaultdict(set)
|
||||
for r in tiger(cache, "addr", county, {"TLID", "ZIP"}):
|
||||
if r["ZIP"] in locality_of_zcta:
|
||||
zips[r["TLID"]].add(r["ZIP"])
|
||||
addresses[r["ZIP"]] += 1
|
||||
for r in tiger(cache, "featnames", county, {"TLID", "FULLNAME", "PAFLAG"}):
|
||||
if r["PAFLAG"] == "P" and r["FULLNAME"] and not HIGHWAY.search(r["FULLNAME"]):
|
||||
for z in zips.get(r["TLID"], ()):
|
||||
count[(locality_of_zcta[z], r["FULLNAME"])] += 1
|
||||
of = collections.defaultdict(list)
|
||||
for (locality, name), n in count.items():
|
||||
of[locality].append((n, name))
|
||||
named = {locality: [{"name": name, "locality": locality, "addresses": n} for n, name in sorted(ranked, key=lambda s: (-s[0], s[1]))[:per_locality]] for locality, ranked in of.items()}
|
||||
return named, addresses
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
p.add_argument("--cache", default=str(CACHE))
|
||||
p.add_argument("--min-population", type=int, default=25000)
|
||||
p.add_argument("--out", default=str(OUT))
|
||||
p.add_argument("--streets-per-locality", type=int, default=10)
|
||||
a = p.parse_args()
|
||||
cache, out = Path(a.cache), Path(a.out)
|
||||
cache.mkdir(parents=True, exist_ok=True)
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
state_population = {r["STATE"]: r[ESTIMATE] for r in csv_rows(cache, STATES, "nst-est2025.csv") if r["SUMLEV"] == "040"}
|
||||
regions = {r["USPS"]: {"abbr": r["USPS"], "code": r["GEOID"], "name": r["NAME"], "population": state_population[r["GEOID"]], "timezone": TIMEZONES[r["USPS"]]} for r in gazetteer(cache, "state")}
|
||||
county_population = {r["STATE"] + r["COUNTY"]: r[ESTIMATE] for r in csv_rows(cache, COUNTIES, "co-est2025.csv") if r["SUMLEV"] == "050"}
|
||||
counties, unestimated = {}, collections.Counter()
|
||||
for r in gazetteer(cache, "counties"):
|
||||
if r["GEOID"] not in county_population:
|
||||
unestimated[r["USPS"]] += 1
|
||||
continue
|
||||
counties[r["GEOID"]] = {"code": r["GEOID"], "name": r["NAME"], "region": r["USPS"], "population": county_population[r["GEOID"]]}
|
||||
print(f"counties without a population estimate, dropped: {dict(unestimated)}", file=sys.stderr)
|
||||
|
||||
places = localities(cache, a.min_population, counties)
|
||||
locality_of_zcta = postal_codes(cache, places)
|
||||
named, addresses = streets(cache, sorted({l["municipality"] for l in places.values()}), locality_of_zcta, a.streets_per_locality)
|
||||
for geoid in [l for l in places if l not in named]:
|
||||
print(f"{geoid} {places[geoid]['name']}: no streets, dropped", file=sys.stderr)
|
||||
del places[geoid]
|
||||
kept_counties = {l["municipality"] for l in places.values()}
|
||||
kept_regions = {counties[c]["region"] for c in kept_counties}
|
||||
|
||||
tsv.write(out / "region.tsv", ["abbr", "code", "name", "population", "timezone"], [r for _, r in sorted(regions.items()) if r["abbr"] in kept_regions])
|
||||
tsv.write(out / "municipality.tsv", ["code", "name", "region", "population"], [c for _, c in sorted(counties.items()) if c["code"] in kept_counties])
|
||||
tsv.write(out / "locality.tsv", ["code", "name", "municipality", "population", "lat", "lon"], [l for _, l in sorted(places.items())])
|
||||
tsv.write(out / "postal-code.tsv", ["code", "locality", "addresses"], [{"code": z, "locality": l, "addresses": addresses[z]} for z, l in sorted(locality_of_zcta.items()) if addresses[z] and l in places])
|
||||
tsv.write(out / "street.tsv", ["name", "locality", "addresses"], [s for locality in sorted(named) for s in named[locality]])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild data/misc/httpmethod.tsv from the IANA HTTP Method Registry.
|
||||
|
||||
data-import/httpmethod.py [--source URL_OR_FILE] [--cache DIR] [--out FILE]
|
||||
|
||||
A method ships when the register cites RFC 9110 section 9.3 or RFC 5789 for it, which
|
||||
is the eight HTTP defines and PATCH; the register's other entries do not. The
|
||||
register's `yes` and `no` ship as `true` and `false`, which a consumer's boolean reads.
|
||||
"""
|
||||
import argparse
|
||||
import csv
|
||||
import io
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import source
|
||||
import tsv
|
||||
|
||||
SOURCE = "https://www.iana.org/assignments/http-methods/methods.csv"
|
||||
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "httpmethod.tsv"
|
||||
CACHE = Path(__file__).resolve().parent / "cache"
|
||||
COLUMNS = ["idempotent", "method", "safe"]
|
||||
CORE = re.compile(r"\[RFC9110, Section 9\.3\.|\[RFC5789, Section 2\]")
|
||||
BOOLEAN = {"yes": "true", "no": "false"}
|
||||
SELECTOR = '[]{}"|' # mirrors inSelector in table.go
|
||||
EXPECTED = 9
|
||||
|
||||
|
||||
def rows(text):
|
||||
for r in csv.DictReader(io.StringIO(text)):
|
||||
method = (r["Method Name"] or "").strip()
|
||||
if not CORE.search(r["Reference"] or ""):
|
||||
continue
|
||||
if set(method) & set(SELECTOR):
|
||||
sys.exit(f"{method}: a method holding one of {SELECTOR} is one a selector cannot spell")
|
||||
answers = {}
|
||||
for column in ("Safe", "Idempotent"):
|
||||
answer = (r[column] or "").strip()
|
||||
if answer not in BOOLEAN:
|
||||
sys.exit(f"{method}: {column} {answer!r} is neither yes nor no")
|
||||
answers[column] = BOOLEAN[answer]
|
||||
yield {"idempotent": answers["Idempotent"], "method": method, "safe": answers["Safe"]}
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
p.add_argument("--cache", default=str(CACHE))
|
||||
p.add_argument("--source", default=SOURCE)
|
||||
p.add_argument("--out", default=str(OUT))
|
||||
a = p.parse_args()
|
||||
table = sorted(rows(source.fetch(a.source, a.cache, "http-methods.csv").decode("utf-8")), key=lambda r: r["method"])
|
||||
if len(table) != EXPECTED:
|
||||
sys.exit(f"{len(table)} methods cite RFC 9110 section 9.3 or RFC 5789, not {EXPECTED}; the register's Reference column has moved")
|
||||
tsv.write(a.out, COLUMNS, table)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild data/misc/httpstatus.tsv from the IANA HTTP Status Code Registry.
|
||||
|
||||
data-import/httpstatus.py [--source URL_OR_FILE] [--cache DIR] [--out FILE]
|
||||
|
||||
What ships is the codes in use: a range, an unassigned code and a qualified reason are all skipped.
|
||||
"""
|
||||
import argparse
|
||||
import csv
|
||||
import io
|
||||
from pathlib import Path
|
||||
|
||||
import source
|
||||
import tsv
|
||||
|
||||
SOURCE = "https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv"
|
||||
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "httpstatus.tsv"
|
||||
CACHE = Path(__file__).resolve().parent / "cache"
|
||||
COLUMNS = ["code", "reason"]
|
||||
|
||||
|
||||
def rows(text):
|
||||
for r in csv.DictReader(io.StringIO(text)):
|
||||
code, reason = r["Value"].strip(), r["Description"].strip()
|
||||
if code.isdigit() and reason and "(" not in reason and reason != "Unassigned":
|
||||
yield {"code": code, "reason": reason}
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
p.add_argument("--cache", default=str(CACHE))
|
||||
p.add_argument("--source", default=SOURCE)
|
||||
p.add_argument("--out", default=str(OUT))
|
||||
a = p.parse_args()
|
||||
table = rows(source.fetch(a.source, a.cache, "http-status-codes-1.csv").decode("utf-8"))
|
||||
tsv.write(a.out, COLUMNS, sorted(table, key=lambda r: int(r["code"])))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild data/misc/language.tsv from datasets/language-codes (PDDL), the LoC ISO 639-2 register.
|
||||
|
||||
data-import/language.py [--source URL_OR_FILE] [--cache DIR] [--out FILE]
|
||||
|
||||
Only the 639-2 entries carrying a 639-1 code ship, since the alpha-2 code is the key.
|
||||
"""
|
||||
import argparse
|
||||
import csv
|
||||
import io
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import source
|
||||
import tsv
|
||||
|
||||
SOURCE = "https://raw.githubusercontent.com/datasets/language-codes/main/data/language-codes-full.csv"
|
||||
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "language.tsv"
|
||||
CACHE = Path(__file__).resolve().parent / "cache"
|
||||
COLUMNS = ["code", "code3", "name"]
|
||||
|
||||
|
||||
def name(english):
|
||||
"""The register lists every synonym; the first, without its qualifier, is the one a selector spells."""
|
||||
return re.sub(r"\s*\([^)]*\)", "", english.split(";")[0]).strip()
|
||||
|
||||
|
||||
def rows(text):
|
||||
for r in csv.DictReader(io.StringIO(text)):
|
||||
if r["alpha2"]:
|
||||
yield {"code": r["alpha2"], "code3": r["alpha3-t"] or r["alpha3-b"], "name": name(r["English"])}
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
p.add_argument("--cache", default=str(CACHE))
|
||||
p.add_argument("--source", default=SOURCE)
|
||||
p.add_argument("--out", default=str(OUT))
|
||||
a = p.parse_args()
|
||||
table = rows(source.fetch(a.source, a.cache, "language-codes-full.csv").decode("utf-8"))
|
||||
tsv.write(a.out, COLUMNS, sorted(table, key=lambda r: r["code"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild data/misc/loglevel.tsv from RFC 5424 and POSIX <syslog.h>.
|
||||
|
||||
data-import/loglevel.py [--source URL_OR_FILE] [--posix URL_OR_FILE] [--cache DIR] [--out FILE]
|
||||
|
||||
Every severity in Table 2 ships, paired to POSIX's keywords by the order each source
|
||||
lists them, which holds only because a keyword prefixes the severity RFC 5424 numbers
|
||||
alike.
|
||||
"""
|
||||
import argparse
|
||||
import html
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import source
|
||||
import tsv
|
||||
|
||||
SOURCE = "https://www.rfc-editor.org/rfc/rfc5424.txt"
|
||||
POSIX = "https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/syslog.h.html"
|
||||
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "loglevel.tsv"
|
||||
CACHE = Path(__file__).resolve().parent / "cache"
|
||||
COLUMNS = ["code", "keyword", "severity"]
|
||||
TABLE = re.compile(r"Table 1\.\s+Syslog Message Facilities(.*?)Table 2\.\s+Syslog Message Severities", re.S)
|
||||
ROW = re.compile(r"^ +(\d) +([A-Z][a-z]+): ", re.M)
|
||||
SECTION = re.compile(r"severity level portion of the.*?macros:(.*?)The following shall be declared as functions", re.S)
|
||||
MACRO = re.compile(r"\bLOG_([A-Z]+)\b")
|
||||
EXPECTED = 8
|
||||
|
||||
|
||||
def severities(text):
|
||||
table = TABLE.search(text)
|
||||
if not table:
|
||||
sys.exit("the RFC holds no text between the facility and the severity caption; its tables have moved")
|
||||
return ROW.findall(table.group(1))
|
||||
|
||||
|
||||
def keywords(page):
|
||||
section = SECTION.search(html.unescape(re.sub(r"<[^>]+>", "", page)))
|
||||
if not section:
|
||||
sys.exit("POSIX spells no severity list between its own caption and the function declarations; the page has moved")
|
||||
return [m.lower() for m in MACRO.findall(section.group(1))]
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
p.add_argument("--cache", default=str(CACHE))
|
||||
p.add_argument("--source", default=SOURCE)
|
||||
p.add_argument("--posix", default=POSIX)
|
||||
p.add_argument("--out", default=str(OUT))
|
||||
a = p.parse_args()
|
||||
table = sorted(severities(source.fetch(a.source, a.cache, "rfc5424.txt").decode("utf-8")))
|
||||
codes = [code for code, _ in table]
|
||||
if codes != [str(i) for i in range(EXPECTED)]:
|
||||
sys.exit(f"the severity table numbers {codes}, not 0 through {EXPECTED - 1}")
|
||||
words = keywords(source.fetch(a.posix, a.cache, "posix-syslog.h.html").decode("utf-8"))
|
||||
if len(words) != EXPECTED:
|
||||
sys.exit(f"POSIX lists {len(words)} severity macros, not {EXPECTED}: {words}")
|
||||
rows = []
|
||||
for (code, severity), keyword in zip(table, words):
|
||||
if not severity.lower().startswith(keyword):
|
||||
sys.exit(f"severity {code} is {severity} in the RFC and {keyword} in POSIX, which is no prefix of it")
|
||||
rows.append({"code": code, "keyword": keyword, "severity": severity})
|
||||
tsv.write(a.out, COLUMNS, rows)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild data/misc/mimetype.tsv from mime-db (MIT), the IANA media type registry with extensions.
|
||||
|
||||
data-import/mimetype.py [--source URL_OR_FILE] [--cache DIR] [--out FILE]
|
||||
|
||||
Only types mime-db records as IANA-registered, with a filename extension, ship.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import source
|
||||
import tsv
|
||||
|
||||
SOURCE = "https://raw.githubusercontent.com/jshttp/mime-db/master/db.json"
|
||||
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "mimetype.tsv"
|
||||
CACHE = Path(__file__).resolve().parent / "cache"
|
||||
COLUMNS = ["ext", "type"]
|
||||
FIXUPS = {"application/mp4": "mp4s", "audio/mpeg": "mp3", "audio/ogg": "ogg", "video/quicktime": "mov"}
|
||||
|
||||
|
||||
def rows(db):
|
||||
for mimetype, entry in db.items():
|
||||
if entry.get("source") == "iana" and entry.get("extensions"):
|
||||
yield {"ext": "." + FIXUPS.get(mimetype, entry["extensions"][0]), "type": mimetype}
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
p.add_argument("--cache", default=str(CACHE))
|
||||
p.add_argument("--source", default=SOURCE)
|
||||
p.add_argument("--out", default=str(OUT))
|
||||
a = p.parse_args()
|
||||
table = rows(json.loads(source.fetch(a.source, a.cache, "mime-db.json").decode("utf-8")))
|
||||
tsv.write(a.out, COLUMNS, sorted(table, key=lambda r: r["type"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild data/sv_SE/first-name.tsv and last-name.tsv from SCB's whole-population name counts (CC0, "Källa: SCB").
|
||||
|
||||
data-import/names-se.py [--source URL_OR_FILE] [--first N] [--last N] [--cache DIR] [--out DIR]
|
||||
|
||||
A tilltalsnamn goes under each sex that carries it, so a name both sexes carry has two rows.
|
||||
"""
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import source
|
||||
import tsv
|
||||
import xlsx
|
||||
|
||||
SOURCE = "https://www.scb.se/contentassets/9fe7dbb460994c72b835163dbc491ef9/namn-med-minst-tva-barare-31-december-2022.xlsx"
|
||||
OUT = Path(__file__).resolve().parent.parent / "data" / "sv_SE"
|
||||
CACHE = Path(__file__).resolve().parent / "cache"
|
||||
SHEETS = {"f": "Tilltalsnamn kvinnor", "m": "Tilltalsnamn män"}
|
||||
SURNAMES = "Efternamn"
|
||||
NAME = re.compile(r"^[^\W\d_]{2,}([ '-][^\W\d_]{2,})*$")
|
||||
PARTICLES = {"af", "av", "da", "de", "del", "den", "der", "di", "dos", "du", "la", "le", "van", "von"}
|
||||
|
||||
|
||||
def cased(name):
|
||||
"""SCB's uppercase name as it is written: each part capitalised, a particle before another part lowercased."""
|
||||
parts = name.lower().split(" ")
|
||||
return " ".join(w if w in PARTICLES and len(parts) > 1 else w.title() for w in parts)
|
||||
|
||||
|
||||
def counted(rows, top):
|
||||
"""The top names of a sheet by bearers, initials and single letters dropped."""
|
||||
names = [(cells[0], int(cells[1])) for cells in rows if len(cells) >= 2 and cells[1].isdigit() and NAME.match(cells[0])]
|
||||
return sorted(names, key=lambda n: (-n[1], n[0]))[:top]
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
p.add_argument("--cache", default=str(CACHE))
|
||||
p.add_argument("--first", type=int, default=2000, help="names per sex")
|
||||
p.add_argument("--last", type=int, default=5000)
|
||||
p.add_argument("--out", default=str(OUT))
|
||||
p.add_argument("--source", default=SOURCE)
|
||||
a = p.parse_args()
|
||||
data = source.fetch(a.source, a.cache, "scb-namn-2022.xlsx", magic=b"PK")
|
||||
first = [{"name": cased(n), "sex": sex, "count": c} for sex, sheet in SHEETS.items() for n, c in counted(xlsx.rows(data, sheet), a.first)]
|
||||
last = [{"name": cased(n), "count": c} for n, c in counted(xlsx.rows(data, SURNAMES), a.last)]
|
||||
out = Path(a.out)
|
||||
tsv.write(out / "first-name.tsv", ["name", "sex", "count"], sorted(first, key=lambda r: (r["name"], r["sex"])))
|
||||
tsv.write(out / "last-name.tsv", ["name", "count"], sorted(last, key=lambda r: r["name"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild data/en_US/first-name.tsv and last-name.tsv from SSA baby names and the Census 2010 surnames (public domain).
|
||||
|
||||
data-import/names-us.py [--names URL_OR_FILE] [--surnames URL_OR_FILE] [--from-year YEAR] [--first N] [--last N] [--cache DIR] [--out DIR]
|
||||
|
||||
A given name is counted over the births of --from-year and later, under each sex it was given to, so a
|
||||
name both sexes carry has two rows. --names takes SSA's names.zip or a merged name,sex,count,year file;
|
||||
--surnames takes the Census names.zip or its Names_2010Census.csv.
|
||||
"""
|
||||
import argparse
|
||||
import collections
|
||||
import csv
|
||||
import io
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import source
|
||||
import tsv
|
||||
|
||||
NAMES = "https://raw.githubusercontent.com/hackerb9/ssa-baby-names/master/alldata.txt"
|
||||
SURNAMES = "https://www2.census.gov/topics/genealogy/2010surnames/names.zip"
|
||||
OUT = Path(__file__).resolve().parent.parent / "data" / "en_US"
|
||||
CACHE = Path(__file__).resolve().parent / "cache"
|
||||
MC = re.compile(r"^Mc([a-z])")
|
||||
PLACEHOLDERS = {"Baby", "Female", "Infant", "Male", "Notnamed", "Unknown", "Unnamed"}
|
||||
|
||||
|
||||
def csv_or_zip_rows(data, member):
|
||||
"""The rows of a CSV, or of every member of a zip named like member, name,sex,count[,year]."""
|
||||
if data.startswith(b"PK"):
|
||||
z = zipfile.ZipFile(io.BytesIO(data))
|
||||
for n in sorted(z.namelist()):
|
||||
if re.fullmatch(member, n):
|
||||
year = re.sub(r"\D", "", n)
|
||||
for r in csv.reader(io.StringIO(z.read(n).decode("utf-8-sig"))):
|
||||
yield r + [year] if year else r
|
||||
return
|
||||
yield from csv.reader(io.StringIO(data.decode("utf-8-sig")))
|
||||
|
||||
|
||||
def given(data, from_year):
|
||||
counts = collections.Counter()
|
||||
for r in csv_or_zip_rows(data, r"yob\d{4}\.txt"):
|
||||
if len(r) >= 4 and r[3].isdigit() and int(r[3]) >= from_year and r[0] not in PLACEHOLDERS:
|
||||
counts[(r[0], r[1].lower())] += int(r[2])
|
||||
return counts
|
||||
|
||||
|
||||
def surname(name):
|
||||
"""A Census uppercase surname as it is written: capitalised, and Mc before a capital."""
|
||||
return MC.sub(lambda m: "Mc" + m.group(1).upper(), name.title())
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
p.add_argument("--cache", default=str(CACHE))
|
||||
p.add_argument("--first", type=int, default=2000, help="names per sex")
|
||||
p.add_argument("--from-year", type=int, default=1930)
|
||||
p.add_argument("--last", type=int, default=5000)
|
||||
p.add_argument("--names", default=NAMES)
|
||||
p.add_argument("--out", default=str(OUT))
|
||||
p.add_argument("--surnames", default=SURNAMES)
|
||||
a = p.parse_args()
|
||||
counts = given(source.fetch(a.names, a.cache, "ssa-names.txt"), a.from_year)
|
||||
first = []
|
||||
for sex in ("f", "m"):
|
||||
top = sorted(((n, c) for (n, s), c in counts.items() if s == sex), key=lambda n: (-n[1], n[0]))[:a.first]
|
||||
first += [{"name": n, "sex": sex, "count": c} for n, c in top]
|
||||
rows = csv_or_zip_rows(source.fetch(a.surnames, a.cache, "census-surnames-2010.zip"), r"Names_2010Census\.csv")
|
||||
last = [{"name": surname(r[0]), "count": int(r[2])} for r in rows if len(r) >= 3 and r[2].isdigit() and r[0].isalpha()]
|
||||
last = sorted(last, key=lambda r: (-r["count"], r["name"]))[:a.last]
|
||||
out = Path(a.out)
|
||||
tsv.write(out / "first-name.tsv", ["name", "sex", "count"], sorted(first, key=lambda r: (r["name"], r["sex"])))
|
||||
tsv.write(out / "last-name.tsv", ["name", "count"], sorted(last, key=lambda r: r["name"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild data/misc/port.tsv from the IANA Service Name and Transport Protocol Port Number Registry.
|
||||
|
||||
data-import/port.py [--source URL_OR_FILE] [--cache DIR] [--out FILE]
|
||||
|
||||
A row needs a service name and a numeric TCP port. A port the registry lists more than
|
||||
once keeps the first service it describes, or the first of them where it describes none,
|
||||
so a number selects one row.
|
||||
"""
|
||||
import argparse
|
||||
import csv
|
||||
import io
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import source
|
||||
import tsv
|
||||
|
||||
SOURCE = "https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.csv"
|
||||
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "port.tsv"
|
||||
CACHE = Path(__file__).resolve().parent / "cache"
|
||||
COLUMNS = ["number", "service"]
|
||||
FLOOR = 4000
|
||||
|
||||
|
||||
def rows(text):
|
||||
best, described = {}, set()
|
||||
for r in csv.DictReader(io.StringIO(text)):
|
||||
service, number = (r["Service Name"] or "").strip(), (r["Port Number"] or "").strip()
|
||||
transport, description = (r["Transport Protocol"] or "").strip(), (r["Description"] or "").strip()
|
||||
if transport != "tcp" or not service or not number.isdigit():
|
||||
continue
|
||||
if not 1 <= int(number) <= 65535:
|
||||
sys.exit(f"{service}: port {number} is outside 1-65535")
|
||||
if number not in best or (description and number not in described):
|
||||
best[number] = {"number": number, "service": service}
|
||||
if description:
|
||||
described.add(number)
|
||||
return sorted(best.values(), key=lambda r: int(r["number"]))
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
p.add_argument("--cache", default=str(CACHE))
|
||||
p.add_argument("--source", default=SOURCE)
|
||||
p.add_argument("--out", default=str(OUT))
|
||||
a = p.parse_args()
|
||||
table = rows(source.fetch(a.source, a.cache, "service-names-port-numbers.csv").decode("utf-8"))
|
||||
if len(table) < FLOOR:
|
||||
sys.exit(f"only {len(table)} ports named a TCP service; the registry's columns have moved")
|
||||
tsv.write(a.out, COLUMNS, table)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild data/misc/protocol.tsv from the IANA Protocol Numbers registry.
|
||||
|
||||
data-import/protocol.py [--source URL_OR_FILE] [--cache DIR] [--out FILE]
|
||||
|
||||
A row needs a number and a keyword, which drops the seven numbers assigned to a class of
|
||||
protocols rather than to one; a keyword the registry reserves or marks deprecated goes
|
||||
too. Where the registry names no protocol beside the keyword, the keyword is the name.
|
||||
"""
|
||||
import argparse
|
||||
import csv
|
||||
import io
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import source
|
||||
import tsv
|
||||
|
||||
SOURCE = "https://www.iana.org/assignments/protocol-numbers/protocol-numbers-1.csv"
|
||||
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "protocol.tsv"
|
||||
CACHE = Path(__file__).resolve().parent / "cache"
|
||||
COLUMNS = ["keyword", "name", "number"]
|
||||
SKIP = ("Reserved", "deprecated")
|
||||
SELECTOR = '[]{}"|' # mirrors inSelector in table.go
|
||||
|
||||
|
||||
def rows(text):
|
||||
for r in csv.DictReader(io.StringIO(text)):
|
||||
number, keyword = (r["Decimal"] or "").strip(), (r["Keyword"] or "").strip()
|
||||
if not number.isdigit() or not keyword or any(s in keyword for s in SKIP):
|
||||
continue
|
||||
if not 0 <= int(number) <= 255:
|
||||
sys.exit(f"{keyword}: protocol number {number} is outside 0-255")
|
||||
yield {"keyword": keyword, "name": " ".join((r["Protocol"] or "").split()) or keyword, "number": number}
|
||||
|
||||
|
||||
def refuse_an_unselectable_row(table):
|
||||
"""What the loader refuses at New, and a name that names two rows rather than one."""
|
||||
keywords, names = {}, {}
|
||||
for r in table:
|
||||
for column in ("keyword", "name"):
|
||||
if set(r[column]) & set(SELECTOR):
|
||||
sys.exit(f"{r['keyword']}: {column} {r[column]!r} holds one of {SELECTOR}, which a selector cannot spell")
|
||||
if r["keyword"] in keywords:
|
||||
sys.exit(f"{r['keyword']}: repeats the keyword of protocol {keywords[r['keyword']]}")
|
||||
keywords[r["keyword"]] = r["number"]
|
||||
for r in table:
|
||||
if r["name"] != r["keyword"] and r["name"] in keywords:
|
||||
sys.exit(f"{r['keyword']}: name {r['name']!r} is another protocol's keyword")
|
||||
if r["name"] in names:
|
||||
sys.exit(f"{r['keyword']}: name {r['name']!r} repeats {names[r['name']]}'s, so it selects neither")
|
||||
names[r["name"]] = r["keyword"]
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
p.add_argument("--cache", default=str(CACHE))
|
||||
p.add_argument("--source", default=SOURCE)
|
||||
p.add_argument("--out", default=str(OUT))
|
||||
a = p.parse_args()
|
||||
table = sorted(rows(source.fetch(a.source, a.cache, "protocol-numbers-1.csv").decode("utf-8")), key=lambda r: int(r["number"]))
|
||||
refuse_an_unselectable_row(table)
|
||||
tsv.write(a.out, COLUMNS, table)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,29 @@
|
||||
"""A source fetched once into the cache."""
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def fetch(source, cache, name, magic=b"", data=None, headers=None):
|
||||
"""The bytes of a URL, downloaded into cache/name once, or of a local file."""
|
||||
if not re.match(r"^https?://", source):
|
||||
return Path(source).read_bytes()
|
||||
path = Path(cache) / name
|
||||
if path.exists():
|
||||
return path.read_bytes()
|
||||
for attempt in range(1, 6):
|
||||
req = urllib.request.Request(source, data=data, headers={"User-Agent": "fejkdata data-import", **(headers or {})})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=600) as r:
|
||||
body = r.read()
|
||||
except OSError:
|
||||
body = b""
|
||||
if body and body.startswith(magic) and b"Request Rejected" not in body[:512]:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(body)
|
||||
return body
|
||||
if attempt < 5:
|
||||
time.sleep(10 * attempt)
|
||||
sys.exit(f"{source}: no valid download in 5 attempts")
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild data/misc/territory.tsv from datasets/country-codes (PDDL).
|
||||
|
||||
data-import/territory.py [--source URL_OR_FILE] [--cache DIR] [--out FILE]
|
||||
"""
|
||||
import argparse
|
||||
import csv
|
||||
import io
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import source
|
||||
import tsv
|
||||
|
||||
SOURCE = "https://raw.githubusercontent.com/datasets/country-codes/main/data/country-codes.csv"
|
||||
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "territory.tsv"
|
||||
CACHE = Path(__file__).resolve().parent / "cache"
|
||||
COLUMNS = ["alpha2", "alpha3", "calling-code", "capital", "country", "currency", "flag", "languages", "name", "numeric", "tld"]
|
||||
SOVEREIGN = re.compile(r"^(?:Part of|Territor(?:y|ies) of|Crown dependency of|Commonwealth of|Associated with) ([A-Z]{2})$")
|
||||
STANDALONE = {"In contention", "International"}
|
||||
# Gaps in the source, keyed by alpha2.
|
||||
FIXUPS = {"TR": {"currency": "TRY"}}
|
||||
|
||||
|
||||
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 country(alpha2, independent):
|
||||
"""The sovereign state the register records; a territory it records none for stands alone."""
|
||||
m = SOVEREIGN.match(independent)
|
||||
if m:
|
||||
return m.group(1)
|
||||
if independent == "Yes" or independent in STANDALONE:
|
||||
return alpha2
|
||||
sys.exit(f"{alpha2}: is_independent {independent!r} names no sovereign this script can read")
|
||||
|
||||
|
||||
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"],
|
||||
"country": country(alpha2, r["is_independent"]),
|
||||
"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 main():
|
||||
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
p.add_argument("--cache", default=str(CACHE))
|
||||
p.add_argument("--source", default=SOURCE)
|
||||
p.add_argument("--out", default=str(OUT))
|
||||
a = p.parse_args()
|
||||
table = sorted(rows(source.fetch(a.source, a.cache, "country-codes.csv").decode("utf-8")), key=lambda r: r["alpha2"])
|
||||
shipped = {r["alpha2"] for r in table}
|
||||
for r in table:
|
||||
if r["country"] not in shipped:
|
||||
sys.exit(f"{r['alpha2']}: country {r['country']} is no shipped territory; a sovereign the set drops leaves its territories unreadable")
|
||||
tsv.write(a.out, COLUMNS, table)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild data/misc/timezone.tsv from the IANA tzdb tarball (public domain) and GeoNames (CC BY 4.0).
|
||||
|
||||
data-import/timezone.py [--source URL_OR_FILE] [--cities URL_OR_FILE] [--cache DIR] [--out FILE] [--territories FILE]
|
||||
|
||||
The offset is the zone's standard offset, the first field of its Zone rule's last
|
||||
continuation line, with a Link resolved to its target. The weight is the population
|
||||
GeoNames records in the zone's cities of 15,000 or more, never below that threshold,
|
||||
so a drawn zone is one people live in.
|
||||
"""
|
||||
import argparse
|
||||
import csv
|
||||
import io
|
||||
import re
|
||||
import sys
|
||||
import tarfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import source
|
||||
import tsv
|
||||
|
||||
SOURCE = "https://data.iana.org/time-zones/tzdata-latest.tar.gz"
|
||||
CITIES = "https://download.geonames.org/export/dump/cities15000.zip"
|
||||
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "timezone.tsv"
|
||||
TERRITORIES = Path(__file__).resolve().parent.parent / "data" / "misc" / "territory.tsv"
|
||||
CACHE = Path(__file__).resolve().parent / "cache"
|
||||
COLUMNS = ["offset", "territory", "weight", "zone"]
|
||||
REGIONS = ["africa", "antarctica", "asia", "australasia", "backward", "etcetera", "europe", "northamerica", "southamerica"]
|
||||
# GeoNames' own cutoff, so a zone it records no city in weighs less than one would.
|
||||
FLOOR = 15000
|
||||
|
||||
|
||||
def member(tar, name):
|
||||
try:
|
||||
f = tar.extractfile(name)
|
||||
except KeyError:
|
||||
f = None
|
||||
if f is None:
|
||||
sys.exit(f"{name}: the tarball no longer holds it as a file; the tzdb layout has moved")
|
||||
return f.read().decode("utf-8")
|
||||
|
||||
|
||||
def offsets(tar):
|
||||
"""Every zone's standard offset, and every link's target."""
|
||||
std, links = {}, {}
|
||||
for name in REGIONS:
|
||||
zone = None
|
||||
for raw in member(tar, name).splitlines():
|
||||
line = raw.split("#")[0].rstrip()
|
||||
if not line.strip():
|
||||
continue
|
||||
if line.startswith("Zone"):
|
||||
f = line.split()
|
||||
zone, std[f[1]] = f[1], f[2]
|
||||
elif line.startswith("Link"):
|
||||
f = line.split()
|
||||
links[f[2]], zone = f[1], None
|
||||
elif line[0] in " \t" and zone:
|
||||
std[zone] = line.split()[0]
|
||||
else:
|
||||
zone = None
|
||||
return std, links
|
||||
|
||||
|
||||
def resolve(zone, std, links):
|
||||
for _ in range(10):
|
||||
if zone in std:
|
||||
return std[zone]
|
||||
if zone not in links:
|
||||
return None
|
||||
zone = links[zone]
|
||||
return None
|
||||
|
||||
|
||||
def utc_offset(raw):
|
||||
"""±HH:MM from a tzdb STDOFF field, which writes the minutes and seconds only when it has them."""
|
||||
m = re.match(r"^(-)?(\d{1,2})(?::(\d{2})(?::(\d{2}))?)?$", raw)
|
||||
if not m or (m.group(4) or "00") != "00":
|
||||
return None
|
||||
return f"{'-' if m.group(1) else '+'}{int(m.group(2)):02d}:{m.group(3) or '00'}"
|
||||
|
||||
|
||||
def population(body):
|
||||
"""The population GeoNames records per timezone, summed over its cities."""
|
||||
with zipfile.ZipFile(io.BytesIO(body)) as z:
|
||||
text = z.read("cities15000.txt").decode("utf-8")
|
||||
per = {}
|
||||
for city in csv.reader(io.StringIO(text), delimiter="\t", quoting=csv.QUOTE_NONE):
|
||||
if len(city) > 17 and city[17] and city[14].isdigit():
|
||||
per[city[17]] = per.get(city[17], 0) + int(city[14])
|
||||
return per
|
||||
|
||||
|
||||
def rows(tar, territories, pop):
|
||||
std, links = offsets(tar)
|
||||
for line in member(tar, "zone.tab").splitlines():
|
||||
if line.startswith("#") or not line.strip():
|
||||
continue
|
||||
fields = line.split("\t")
|
||||
if len(fields) < 3:
|
||||
sys.exit(f"zone.tab: {line!r} has {len(fields)} fields; a row names a territory, a location and a zone")
|
||||
territory, zone = fields[0], fields[2]
|
||||
if territory not in territories:
|
||||
continue
|
||||
raw = resolve(zone, std, links)
|
||||
if raw is None:
|
||||
sys.exit(f"{zone}: the tarball gives it no Zone rule and no Link to one")
|
||||
offset = utc_offset(raw)
|
||||
if offset is None:
|
||||
sys.exit(f"{zone}: standard offset {raw!r} is not a ±HH:MM the table can spell")
|
||||
yield {"offset": offset, "territory": territory, "weight": max(pop.get(zone, 0), FLOOR), "zone": zone}
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
p.add_argument("--cache", default=str(CACHE))
|
||||
p.add_argument("--source", default=SOURCE)
|
||||
p.add_argument("--out", default=str(OUT))
|
||||
p.add_argument("--cities", default=CITIES)
|
||||
p.add_argument("--territories", default=str(TERRITORIES))
|
||||
a = p.parse_args()
|
||||
shipped = {r["alpha2"] for r in csv.DictReader(io.StringIO(Path(a.territories).read_text(encoding="utf-8")), delimiter="\t")}
|
||||
pop = population(source.fetch(a.cities, a.cache, "cities15000.zip", magic=b"PK"))
|
||||
body = source.fetch(a.source, a.cache, "tzdata-latest.tar.gz", magic=b"\x1f\x8b")
|
||||
with tarfile.open(fileobj=io.BytesIO(body)) as tar:
|
||||
table = sorted(rows(tar, shipped, pop), key=lambda r: r["zone"])
|
||||
print(f"tzdb {tar.extractfile('version').read().decode('utf-8').strip()}", file=sys.stderr)
|
||||
linked = {r["territory"] for r in table}
|
||||
if missing := shipped - linked:
|
||||
sys.exit(f"no zone for {sorted(missing)}; a parent row without a child is a load error")
|
||||
if (floored := sum(1 for r in table if r["weight"] == FLOOR)) > len(table) // 2:
|
||||
sys.exit(f"{floored} of {len(table)} zones fell to the floor; GeoNames' population or timezone column has moved")
|
||||
tsv.write(a.out, COLUMNS, table)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild data/misc/tld.tsv from the IANA Root Zone Database.
|
||||
|
||||
data-import/tld.py [--source URL_OR_FILE] [--zone URL_OR_FILE] [--cache DIR] [--out FILE]
|
||||
|
||||
A TLD ships when the database names a manager for it; one it marks "Not assigned" is a
|
||||
delegation the root zone no longer holds, and the zone file is fetched to prove the two
|
||||
agree. The key is the A-label the root zone holds; `unicode` is the form the database
|
||||
displays, less the bidi marks it wraps a right-to-left label in, and proved to punycode
|
||||
back to the key.
|
||||
"""
|
||||
import argparse
|
||||
import html
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import source
|
||||
import tsv
|
||||
|
||||
SOURCE = "https://www.iana.org/domains/root/db"
|
||||
ZONE = "https://data.iana.org/TLD/tlds-alpha-by-domain.txt"
|
||||
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "tld.tsv"
|
||||
CACHE = Path(__file__).resolve().parent / "cache"
|
||||
COLUMNS = ["tld", "type", "unicode"]
|
||||
TYPES = ("country-code", "generic", "generic-restricted", "infrastructure", "sponsored")
|
||||
BIDI = {0x200E: None, 0x200F: None}
|
||||
ROW = re.compile(r'<a href="/domains/root/db/([^"]+)\.html">([^<]+)</a>.*?</td>\s*<td>([^<]*)</td>\s*<td>(.*?)</td>', re.S)
|
||||
|
||||
|
||||
def rows(page):
|
||||
for a_label, shown, kind, manager in ROW.findall(page):
|
||||
if "Not assigned" in manager:
|
||||
continue
|
||||
kind = kind.strip()
|
||||
if kind not in TYPES:
|
||||
sys.exit(f"{a_label}: the database types it {kind!r}, which is none of {TYPES}")
|
||||
yield {"tld": "." + a_label, "type": kind, "unicode": html.unescape(shown).strip().translate(BIDI)}
|
||||
|
||||
|
||||
def prove_each_unicode_form_is_its_key(table):
|
||||
"""The displayed form is the key's own spelling, which punycode encodes back to."""
|
||||
for r in table:
|
||||
label = r["unicode"][1:]
|
||||
encoded = label if label.isascii() else "xn--" + label.encode("punycode").decode("ascii")
|
||||
if "." + encoded != r["tld"]:
|
||||
sys.exit(f"{r['tld']}: the database displays {r['unicode']!r}, which is no spelling of it")
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
p.add_argument("--cache", default=str(CACHE))
|
||||
p.add_argument("--source", default=SOURCE)
|
||||
p.add_argument("--zone", default=ZONE)
|
||||
p.add_argument("--out", default=str(OUT))
|
||||
a = p.parse_args()
|
||||
table = sorted(rows(source.fetch(a.source, a.cache, "root-db.html").decode("utf-8")), key=lambda r: r["tld"])
|
||||
prove_each_unicode_form_is_its_key(table)
|
||||
listing = source.fetch(a.zone, a.cache, "tlds-alpha-by-domain.txt").decode("utf-8")
|
||||
delegated = {"." + line.strip().lower() for line in listing.splitlines() if line.strip() and not line.startswith("#")}
|
||||
shipped = {r["tld"] for r in table}
|
||||
if len(table) != len(delegated) or shipped != delegated:
|
||||
sys.exit(f"the database assigns {len(table)} rows where the root zone holds {len(delegated)} TLDs: {sorted(shipped ^ delegated)[:10]}")
|
||||
tsv.write(a.out, COLUMNS, table)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,18 @@
|
||||
"""A table written as the loader admits it."""
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def write(path, columns, rows):
|
||||
"""Write the rows as a TSV; the table needs two rows, and every cell must be non-empty and free of tabs, newlines and braces."""
|
||||
lines = ["\t".join(columns)]
|
||||
for row in rows:
|
||||
cells = [str(row[c]) for c in columns]
|
||||
if not all(cells) or any(re.search(r"[\t\n{}]", c) for c in cells):
|
||||
raise ValueError(f"{path}: a cell is empty or holds a tab, newline or brace: {row}")
|
||||
lines.append("\t".join(cells))
|
||||
if len(lines) < 3:
|
||||
raise ValueError(f"{path}: has {len(lines) - 1} rows below its header; a table is at least two rows")
|
||||
Path(path).write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
print(f"{path}: {len(lines) - 1} rows", file=sys.stderr)
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild data/misc/useragent.tsv from microlinkhq/top-user-agents (MIT).
|
||||
|
||||
data-import/useragent.py [--source URL] [--cache DIR] [--out FILE]
|
||||
|
||||
The desktop and mobile lists partition the whole, so `device` is read rather than
|
||||
parsed. A row ships when the string names a browser and an operating system both.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import source
|
||||
import tsv
|
||||
|
||||
SOURCE = "https://raw.githubusercontent.com/microlinkhq/top-user-agents/master/src"
|
||||
OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "useragent.tsv"
|
||||
CACHE = Path(__file__).resolve().parent / "cache"
|
||||
COLUMNS = ["browser", "device", "os", "ua"]
|
||||
# First match wins: a Chromium fork this list does not name ships as Chrome.
|
||||
BROWSERS = [("Edge", r"Edg(A|iOS)?/"), ("Opera", r"OPR/"), ("Samsung Internet", r"SamsungBrowser/"),
|
||||
("Chrome", r"(Chrome|CriOS)/"), ("Firefox", r"(Firefox|FxiOS)/"), ("Safari", r"Version/[\d.]+ .*Safari")]
|
||||
SYSTEMS = [("iOS", r"iPhone|iPad|CPU OS "), ("Android", r"Android"), ("ChromeOS", r"CrOS"),
|
||||
("Windows", r"Windows NT"), ("macOS", r"Macintosh|Mac OS X"), ("Linux", r"X11.*Linux|Ubuntu")]
|
||||
|
||||
|
||||
def named(table, ua):
|
||||
for name, pattern in table:
|
||||
if re.search(pattern, ua):
|
||||
return name
|
||||
return ""
|
||||
|
||||
|
||||
def rows(desktop, mobile):
|
||||
seen, kept = set(), []
|
||||
for device, uas in (("desktop", desktop), ("mobile", mobile)):
|
||||
for ua in uas:
|
||||
browser, os = named(BROWSERS, ua), named(SYSTEMS, ua)
|
||||
if not browser or not os:
|
||||
print(f"dropped, naming no {'browser' if not browser else 'operating system'}: {ua}", file=sys.stderr)
|
||||
continue
|
||||
if ua in seen:
|
||||
sys.exit(f"{ua}: listed twice, which would draw it twice as often")
|
||||
seen.add(ua)
|
||||
kept.append({"browser": browser, "device": device, "os": os, "ua": ua})
|
||||
if len(kept) < 0.8 * (len(desktop) + len(mobile)):
|
||||
sys.exit(f"only {len(kept)} of {len(desktop) + len(mobile)} strings named both; the tokens have moved")
|
||||
return kept
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
p.add_argument("--cache", default=str(CACHE))
|
||||
p.add_argument("--source", default=SOURCE)
|
||||
p.add_argument("--out", default=str(OUT))
|
||||
a = p.parse_args()
|
||||
lists = [json.loads(source.fetch(f"{a.source}/{n}.json", a.cache, f"top-user-agents-{n}.json")) for n in ("desktop", "mobile")]
|
||||
tsv.write(a.out, COLUMNS, sorted(rows(*lists), key=lambda r: (r["browser"], r["os"], r["ua"])))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
"""The rows of an xlsx sheet."""
|
||||
import io
|
||||
import xml.etree.ElementTree as ET
|
||||
import zipfile
|
||||
|
||||
NS = {"m": "http://schemas.openxmlformats.org/spreadsheetml/2006/main", "r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships"}
|
||||
|
||||
|
||||
def rows(data, sheet=None):
|
||||
"""The rows of an xlsx sheet named sheet, the first sheet by default, each a list of cell texts."""
|
||||
z = zipfile.ZipFile(io.BytesIO(data))
|
||||
strings = ["".join(t.text or "" for t in si.iter("{%s}t" % NS["m"])) for si in ET.fromstring(z.read("xl/sharedStrings.xml")).findall("m:si", NS)]
|
||||
rels = {r.get("Id"): r.get("Target") for r in ET.fromstring(z.read("xl/_rels/workbook.xml.rels"))}
|
||||
sheets = {s.get("name"): rels[s.get("{%s}id" % NS["r"])] for s in ET.fromstring(z.read("xl/workbook.xml")).iter("{%s}sheet" % NS["m"])}
|
||||
target = sheets[sheet] if sheet else next(iter(sheets.values()))
|
||||
for row in ET.fromstring(z.read("xl/" + target)).findall(".//m:row", NS):
|
||||
cells = []
|
||||
for c in row.findall("m:c", NS):
|
||||
v = c.find("m:v", NS)
|
||||
cells.append("" if v is None else strings[int(v.text)] if c.get("t") == "s" else v.text)
|
||||
yield cells
|
||||
@@ -3,29 +3,68 @@ package fejkdata
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// loadData loads every given directory into one namespace tree and returns its
|
||||
// root children. A directory becomes a group; each *.json file in it compiles to
|
||||
// a node keyed by its base name (address.json -> "address"); each subdirectory
|
||||
// becomes a nested group, so folders turn into dot-path segments. Paths are
|
||||
// merged left to right: matching groups merge by their children, and any other
|
||||
// clash (a leaf, or a leaf-vs-group) is won by the last directory loaded. Once
|
||||
// merged, linkRefs binds every {..path} reference against the final tree.
|
||||
func loadData(paths []string) (map[string]node, error) {
|
||||
// dataSource is one tree to load: an fs.FS and the directory in it to start from.
|
||||
// label prefixes file names in errors; onDisk marks path as a directory that must
|
||||
// exist.
|
||||
type dataSource struct {
|
||||
fsys fs.FS
|
||||
label string
|
||||
onDisk bool
|
||||
path string
|
||||
root string
|
||||
}
|
||||
|
||||
func (s dataSource) name(p string) string {
|
||||
if s.label == "" {
|
||||
return p
|
||||
}
|
||||
return path.Join(s.label, p)
|
||||
}
|
||||
|
||||
// loadData loads every source into one namespace tree and returns its root
|
||||
// children. A directory becomes a folder; each *.json file in it compiles to a node
|
||||
// keyed by its base name (address.json -> "address"); each subdirectory becomes a
|
||||
// nested folder, so folders turn into dot-path segments. Sources merge left to right:
|
||||
// matching folders merge by their children, and any other clash is won by the last
|
||||
// source loaded. Once merged, linkRefs binds every reference against the
|
||||
// final tree.
|
||||
func loadData(sources []dataSource) (map[string]node, error) {
|
||||
root := map[string]node{}
|
||||
for _, p := range paths {
|
||||
g, err := loadDir(p)
|
||||
for _, src := range sources {
|
||||
if src.onDisk {
|
||||
if src.path == "" {
|
||||
return nil, fmt.Errorf("a data path is empty")
|
||||
}
|
||||
info, err := os.Stat(src.path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("data path %s: %w", src.path, err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return nil, fmt.Errorf("%s is not a directory", src.path)
|
||||
}
|
||||
}
|
||||
dir := src.root
|
||||
if dir == "" {
|
||||
dir = "."
|
||||
}
|
||||
g, err := loadDir(src, dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mergeChildren(root, g.children)
|
||||
}
|
||||
if len(root) == 0 {
|
||||
return nil, fmt.Errorf("no .json data found in %v", paths)
|
||||
return nil, fmt.Errorf("no .json data found")
|
||||
}
|
||||
setTablePaths(root)
|
||||
if err := linkTables(root); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := linkRefs(root); err != nil {
|
||||
return nil, err
|
||||
@@ -33,78 +72,120 @@ func loadData(paths []string) (map[string]node, error) {
|
||||
if err := checkNoCycles(root); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := checkBoundLevelsHeld(root); err != nil {
|
||||
if err := checkScope(treeScope(root)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return root, nil
|
||||
}
|
||||
|
||||
// loadDir compiles one directory into a group. os.ReadDir yields entries sorted
|
||||
// loadDir compiles one directory into a folder. fs.ReadDir yields entries sorted
|
||||
// by name, so the tree is built deterministically. Empty subdirectories (no JSON
|
||||
// anywhere under them) are skipped rather than added as empty namespaces.
|
||||
func loadDir(dir string) (*group, error) {
|
||||
info, err := os.Stat(dir)
|
||||
func loadDir(src dataSource, dir string) (*folder, error) {
|
||||
entries, err := fs.ReadDir(src.fsys, dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("%s: %w", src.name(dir), err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return nil, fmt.Errorf("%s is not a directory", dir)
|
||||
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
|
||||
}
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g := &group{children: map[string]node{}}
|
||||
for _, e := range entries {
|
||||
if strings.HasPrefix(e.Name(), ".") { // hidden: a checkout or an editor's file, never data
|
||||
continue
|
||||
}
|
||||
full := filepath.Join(dir, e.Name())
|
||||
full := path.Join(dir, e.Name())
|
||||
if e.IsDir() {
|
||||
child, err := loadDir(full)
|
||||
if err != nil {
|
||||
if err := loadFolder(src, g, full, e.Name()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(child.children) == 0 {
|
||||
continue
|
||||
}
|
||||
if err := checkName(e.Name()); err != nil {
|
||||
return nil, fmt.Errorf("%s: folder %w", full, err)
|
||||
}
|
||||
g.children[e.Name()] = child
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(e.Name(), ".json") {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSuffix(e.Name(), ".json")
|
||||
if err := checkName(name); err != nil {
|
||||
return nil, fmt.Errorf("%s: category %w", full, err)
|
||||
}
|
||||
b, err := os.ReadFile(full)
|
||||
if err != nil {
|
||||
if err := loadFile(src, g, full, e.Name(), files); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var raw any
|
||||
if err := json.Unmarshal(b, &raw); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", full, err)
|
||||
}
|
||||
n, err := compile(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", full, 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)))
|
||||
}
|
||||
g.children[name] = n
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// mergeChildren overlays src onto dst. Two groups under the same key merge
|
||||
// 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.
|
||||
func loadFolder(src dataSource, g *folder, full, name string) error {
|
||||
child, err := loadDir(src, full)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(child.children) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := checkName(name); err != nil {
|
||||
return fmt.Errorf("%s: folder %w", src.name(full), err)
|
||||
}
|
||||
g.children[name] = child
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadFile compiles a *.json file into a category named after it; any other file
|
||||
// is skipped.
|
||||
func loadFile(src dataSource, g *folder, full, file string, files *categoryFiles) error {
|
||||
if !strings.HasSuffix(file, ".json") {
|
||||
return nil
|
||||
}
|
||||
name := strings.TrimSuffix(file, ".json")
|
||||
if err := checkName(name); err != nil {
|
||||
return fmt.Errorf("%s: category %w", src.name(full), err)
|
||||
}
|
||||
b, err := fs.ReadFile(src.fsys, full)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", src.name(full), err)
|
||||
}
|
||||
var raw any
|
||||
if err := json.Unmarshal(b, &raw); err != nil {
|
||||
return fmt.Errorf("%s: %w", src.name(full), err)
|
||||
}
|
||||
n, err := compileCategory(raw, name, files)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", src.name(full), err)
|
||||
}
|
||||
g.children[name] = n
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergeChildren overlays src onto dst. Two folders under the same key merge
|
||||
// recursively (so locales/categories from several paths combine); every other
|
||||
// key is replaced, making the last-loaded directory win on a conflict.
|
||||
func mergeChildren(dst, src map[string]node) {
|
||||
for k, v := range src {
|
||||
if dg, ok := dst[k].(*group); ok {
|
||||
if sg, ok := v.(*group); ok {
|
||||
if dg, ok := dst[k].(*folder); ok {
|
||||
if sg, ok := v.(*folder); ok {
|
||||
mergeChildren(dg.children, sg.children)
|
||||
continue
|
||||
}
|
||||
|
||||
+5
-21
@@ -1,24 +1,8 @@
|
||||
[
|
||||
{
|
||||
"format": "{street-number} {street}\n{locality}, {region} {postal-code}",
|
||||
"street": [
|
||||
{
|
||||
"format": "{name} {suffix}",
|
||||
"name": ["Adams", "Ashby", "Aspen", "Bay", "Birch", "Bridge", "Cedar", "Chestnut", "Church", "Clark", "Cypress", "Dogwood", "Elm", "Forest", "Franklin", "Garden", "Grove", "Hawthorn", "Hickory", "Highland", "Jackson", "Jefferson", "Juniper", "Lake", "Laurel", "Liberty", "Lincoln", "Madison", "Magnolia", "Maple", "Market", "Meadow", "Mill", "Oak", "Park", "Pine", "Poplar", "Prospect", "Ridge", "River", "Spruce", "Sunset", "Sycamore", "Union", "Walnut", "Washington", "Willow", "Wilson"],
|
||||
"suffix": ["Avenue", "Boulevard", "Circle", "Court", "Drive", "Lane", "Place", "Road", "Street", "Terrace", "Trail", "Way"]
|
||||
"street": "{/geo.US.address.street}",
|
||||
"street-number": "{/geo.US.address.street-number}",
|
||||
"locality": "{/geo.US.address.locality}",
|
||||
"region": "{/geo.US.address.region}",
|
||||
"postal-code": "{/geo.US.address.postal-code}"
|
||||
}
|
||||
],
|
||||
"street-number": [
|
||||
{ "format": "10" },
|
||||
{ "format": "100", "weight": 2 },
|
||||
{ "format": "1000", "weight": 0.5 },
|
||||
{ "format": "10100", "weight": 0.3 }
|
||||
],
|
||||
"locality": ["Albany", "Atlanta", "Austin", "Baltimore", "Boston", "Charlotte", "Chicago", "Cincinnati", "Cleveland", "Columbus", "Dallas", "Denver", "Detroit", "El Paso", "Fort Worth", "Fresno", "Houston", "Indianapolis", "Jacksonville", "Kansas City", "Las Vegas", "Long Beach", "Los Angeles", "Memphis", "Mesa", "Miami", "Milwaukee", "Minneapolis", "Nashville", "New Orleans", "Oakland", "Oklahoma City", "Omaha", "Orlando", "Philadelphia", "Phoenix", "Pittsburgh", "Portland", "Raleigh", "Sacramento", "San Antonio", "San Diego", "San Jose", "Seattle", "St. Louis", "Tampa", "Tucson", "Tulsa"],
|
||||
"region": ["AL", "AZ", "CA", "CO", "CT", "FL", "GA", "IL", "IN", "KY", "LA", "MA", "MD", "MI", "MN", "MO", "NC", "NJ", "NV", "NY", "OH", "OK", "OR", "PA", "TN", "TX", "VA", "WA", "WI"],
|
||||
"postal-code": [
|
||||
{ "format": "10000" },
|
||||
{ "format": "10000-0000", "weight": 0.3 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[
|
||||
{
|
||||
"format": "##{x}{x}{x}{x}{x}{x}",
|
||||
"format": "#{x}{x}{x}{x}{x}{x}",
|
||||
"x": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"],
|
||||
"weight": 2
|
||||
},
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
[
|
||||
{
|
||||
"format": "{base} {suffix}",
|
||||
"base": [
|
||||
@@ -9,6 +8,5 @@
|
||||
},
|
||||
["Atlas Industries", "Beacon Labs", "Cedar & Vine", "Compass Group", "Evergreen Trading", "Harbor Logistics", "Hudson Partners", "Liberty Holdings", "Meridian Capital", "Northgate Studios", "Pinnacle Systems", "Riverside", "Sequoia Foods", "Smith & Sons", "Stonebridge Ventures", "Sunrise Bakery", "Wexford Holdings"]
|
||||
],
|
||||
"suffix": ["Co.", "Corp.", "Group", "Inc.", "Inc.", "LLC", "LLC", "Ltd."]
|
||||
"suffix": ["Co.", "Corp.", "Group", { "format": "Inc.", "weight": 2 }, { "format": "LLC", "weight": 2 }, "Ltd."]
|
||||
}
|
||||
]
|
||||
|
||||
+1
-15
@@ -1,15 +1 @@
|
||||
[
|
||||
{
|
||||
"format": "{month}/{day}/{year}",
|
||||
"month": ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"],
|
||||
"day": ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28"],
|
||||
"year": [
|
||||
{ "format": "#1970" },
|
||||
{ "format": "#1980" },
|
||||
{ "format": "#1990", "weight": 2 },
|
||||
{ "format": "2#0#00", "weight": 3 },
|
||||
{ "format": "2#0#10", "weight": 3 },
|
||||
{ "format": "2#020", "weight": 2 }
|
||||
]
|
||||
}
|
||||
]
|
||||
"{date(1970-01-01,2029-12-31,'01/02/2006')}"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
[
|
||||
{
|
||||
"format": "{local}@{domain}",
|
||||
"local": [
|
||||
@@ -7,23 +6,21 @@
|
||||
"weight": 3,
|
||||
"first": ["alex", "amelia", "andrew", "anna", "ava", "benjamin", "carter", "charlotte", "chloe", "chris", "daniel", "david", "dylan", "elijah", "ella", "emily", "emma", "ethan", "evelyn", "gabriel", "grace", "hannah", "harper", "henry", "isaac", "isabella", "jack", "jacob", "james", "john", "joseph", "julia", "leah", "liam", "linda", "lily", "logan", "lucas", "luke", "mary", "mason", "matthew", "maya", "mia", "michael", "natalie", "noah", "nora", "oliver", "olivia", "owen", "patricia", "robert", "ryan", "samuel", "sarah", "sofia", "sophia", "thomas", "victoria", "william", "zoe"],
|
||||
"last": ["adams", "allen", "anderson", "bailey", "baker", "bennett", "brooks", "brown", "campbell", "carter", "clark", "collins", "cook", "cooper", "davis", "edwards", "evans", "fisher", "flores", "foster", "garcia", "gonzalez", "gray", "green", "hall", "harris", "hayes", "hernandez", "hill", "howard", "hughes", "jackson", "jenkins", "johnson", "jones", "kelly", "king", "lee", "lewis", "long", "lopez", "martin", "martinez", "miller", "mitchell", "moore", "morgan", "morris", "murphy", "nelson", "parker", "perez", "perry", "phillips", "powell", "price", "reed", "reyes", "rivera", "roberts", "robinson", "rodriguez", "rogers", "ross", "russell", "sanchez", "sanders", "scott", "smith", "stewart", "sullivan", "taylor", "thomas", "thompson", "torres", "turner", "walker", "ward", "watson", "white", "williams", "wilson", "wood", "wright", "young"],
|
||||
"sep": [".", ".", "_", "-", ""],
|
||||
"n": ["", "", "", "1", "7", "21", "42", "88", "99", "2024"]
|
||||
"sep": [{ "format": ".", "weight": 2 }, "_", "-", ""],
|
||||
"n": [{ "format": "", "weight": 3 }, "1", "7", "21", "42", "88", "99", "2024"]
|
||||
},
|
||||
{
|
||||
"format": "{adj}{noun}{n}",
|
||||
"weight": 2,
|
||||
"adj": ["blue", "cool", "dark", "epic", "fast", "fluffy", "frosty", "funky", "fuzzy", "golden", "grumpy", "happy", "hyper", "jazzy", "lazy", "lucky", "mega", "mighty", "neon", "ninja", "quiet", "rad", "rusty", "salty", "shiny", "silent", "sleepy", "snappy", "sneaky", "spicy", "super", "swift", "turbo", "wild", "witty", "zany"],
|
||||
"noun": ["badger", "banana", "comet", "dragon", "falcon", "ferret", "goblin", "hamster", "koala", "llama", "mango", "muffin", "narwhal", "ninja", "noodle", "otter", "panda", "penguin", "pickle", "pirate", "pixel", "potato", "pretzel", "raccoon", "raptor", "robot", "taco", "toast", "trunk", "turtle", "unicorn", "viking", "waffle", "walrus", "wizard", "yeti"],
|
||||
"n": ["", "", "7", "13", "14", "42", "77", "99", "123", "420", "2024"]
|
||||
"n": [{ "format": "", "weight": 2 }, "7", "13", "14", "42", "77", "99", "123", "420", "2024"]
|
||||
},
|
||||
{
|
||||
"format": "{w}{n}",
|
||||
"weight": 1,
|
||||
"w": ["blooper", "blupp", "boop", "byteme", "coolcat", "derpina", "doomguy", "epicgamer", "fizzbuzz", "glitchlord", "glorptron", "gronkzilla", "kapowski", "lazerwolf", "megashark", "mooncat", "narfle", "nightowl", "noodlemaster", "oofster", "pixelfox", "pixelpusher", "plonkers", "retrowave", "snork", "splatmaster", "stardust", "superdog", "voidwalker", "vroom", "wahoo", "waooooh", "wootwoot", "yeetus", "zapper", "zoomzoom"],
|
||||
"n": ["", "", "7", "14", "42", "69", "99", "123", "420", "1337", "2000", "9000"]
|
||||
"n": [{ "format": "", "weight": 2 }, "7", "14", "42", "69", "99", "123", "420", "1337", "2000", "9000"]
|
||||
}
|
||||
],
|
||||
"domain": ["example.com", "example.org", "example.net", "example.io", "example.co", "example.dev", "example.app", "mail.example.com", "mail.example.org", "webmail.example.net", "inbox.example.com", "post.example.org", "demo.example.net", "test.example.org", "dev.example.io"]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{ "format": "{name}", "rows": "first-name.tsv", "name": "name", "parent": "sex", "weight": "count" }
|
||||
File diff suppressed because it is too large
Load Diff
+7
-5
@@ -2,9 +2,11 @@
|
||||
{
|
||||
"format": "{o}.{o}.{o}.{o}",
|
||||
"weight": 8,
|
||||
"o": [{ "format": "0", "weight": 3 }, { "format": "10", "weight": 4 }, { "format": "#100", "weight": 2 }]
|
||||
},
|
||||
{
|
||||
"format": "{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}"
|
||||
}
|
||||
"o": [
|
||||
{ "format": "{digits(1)}", "weight": 3 },
|
||||
{ "format": "{int(10,99)}", "weight": 4 },
|
||||
{ "format": "1{digits(2)}", "weight": 2 }
|
||||
]
|
||||
},
|
||||
"{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}:{hex(4)}"
|
||||
]
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"format": "{area}-{group}-{serial}",
|
||||
"area": "9{digits(2)}",
|
||||
"group": [
|
||||
{ "format": "{int(50,65)}", "weight": 16 },
|
||||
{ "format": "{int(70,88)}", "weight": 19 },
|
||||
{ "format": "{int(90,92)}", "weight": 3 },
|
||||
{ "format": "{int(94,99)}", "weight": 6 }
|
||||
],
|
||||
"serial": [
|
||||
{ "format": "000{int(1,9)}", "weight": 9 },
|
||||
{ "format": "00{int(10,99)}", "weight": 90 },
|
||||
{ "format": "0{int(100,999)}", "weight": 900 },
|
||||
{ "format": "{int(1000,9999)}", "weight": 9000 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "format": "{name}", "rows": "last-name.tsv", "key": "name", "weight": "count" }
|
||||
File diff suppressed because it is too large
Load Diff
+5
-14
@@ -1,16 +1,7 @@
|
||||
[
|
||||
{
|
||||
"format": "{prefix}{femalefirst|malefirst} {last}",
|
||||
"femalefirst": ["Abigail", "Addison", "Amelia", "Aria", "Aubrey", "Audrey", "Aurora", "Ava", "Bella", "Brooklyn", "Camila", "Caroline", "Charlotte", "Chloe", "Claire", "Eleanor", "Elizabeth", "Ella", "Ellie", "Emily", "Emma", "Evelyn", "Gianna", "Grace", "Hannah", "Harper", "Hazel", "Isabella", "Layla", "Leah", "Lillian", "Lily", "Lucy", "Luna", "Madison", "Maya", "Mia", "Mila", "Naomi", "Natalie", "Nora", "Olivia", "Paisley", "Penelope", "Riley", "Savannah", "Scarlett", "Sofia", "Sophia", "Stella", "Victoria", "Violet", "Zoe"],
|
||||
"malefirst": ["Aiden", "Alexander", "Andrew", "Anthony", "Asher", "Benjamin", "Caleb", "Carter", "Charles", "Christopher", "Daniel", "David", "Dylan", "Elijah", "Ethan", "Ezra", "Gabriel", "Grayson", "Henry", "Isaac", "Jack", "Jackson", "Jacob", "James", "Jayden", "John", "Joseph", "Joshua", "Julian", "Levi", "Liam", "Lincoln", "Logan", "Lucas", "Luke", "Mason", "Mateo", "Matthew", "Michael", "Nathan", "Noah", "Oliver", "Owen", "Samuel", "Sebastian", "Theodore", "Thomas", "William", "Wyatt"],
|
||||
"last": ["Adams", "Allen", "Anderson", "Bailey", "Baker", "Bell", "Bennett", "Brooks", "Brown", "Campbell", "Carter", "Clark", "Collins", "Cook", "Cooper", "Cox", "Davis", "Edwards", "Evans", "Flores", "Foster", "Garcia", "Gonzalez", "Gray", "Green", "Hall", "Harris", "Hayes", "Hernandez", "Hill", "Howard", "Hughes", "Jackson", "James", "Jenkins", "Johnson", "Jones", "Kelly", "King", "Lee", "Lewis", "Long", "Lopez", "Martin", "Martinez", "Miller", "Mitchell", "Moore", "Morgan", "Morris", "Murphy", "Nelson", "Parker", "Perez", "Perry", "Peterson", "Phillips", "Powell", "Price", "Ramirez", "Reed", "Richardson", "Rivera", "Roberts", "Robinson", "Rodriguez", "Rogers", "Ross", "Russell", "Sanchez", "Sanders", "Scott", "Smith", "Stewart", "Sullivan", "Taylor", "Thomas", "Thompson", "Torres", "Turner", "Walker", "Ward", "Watson", "White", "Williams", "Wilson", "Wood", "Wright", "Young"],
|
||||
"prefix": [
|
||||
"",
|
||||
{
|
||||
"format": "{title} ",
|
||||
"title": ["Dr", "Miss", "Mr", "Mrs", "Ms", "Mx", "Prof"],
|
||||
"weight": 0.1
|
||||
"format": "{prefix}{first} {last}",
|
||||
"first": "{.first-name.name}",
|
||||
"last": "{.last-name.name}",
|
||||
"prefix": [null, { "format": "{.title.name} ", "weight": 0.1 }],
|
||||
"sex": "{.sex.name}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
"format": "({area}) {exch}-{line}",
|
||||
"weight": 2,
|
||||
"area": ["202", "212", "213", "305", "312", "404", "415", "469", "503", "512", "602", "617", "646", "702", "718", "720", "737", "773", "786", "808", "917"],
|
||||
"exch": [{ "format": "100" }],
|
||||
"line": [{ "format": "0000" }]
|
||||
"exch": "{int(100,999)}",
|
||||
"line": "{digits(4)}"
|
||||
},
|
||||
{
|
||||
"format": "{area}-{exch}-{line}",
|
||||
"area": ["202", "212", "213", "305", "312", "404", "415", "469", "503", "512", "602", "617", "646", "702", "718", "720", "737", "773", "786", "808", "917"],
|
||||
"exch": [{ "format": "100" }],
|
||||
"line": [{ "format": "0000" }]
|
||||
"exch": "{int(100,999)}",
|
||||
"line": "{digits(4)}"
|
||||
}
|
||||
]
|
||||
|
||||
+7
-14
@@ -1,19 +1,12 @@
|
||||
[
|
||||
{
|
||||
"format": "${amt}.{cents}",
|
||||
"amt": [
|
||||
{ "format": "1", "weight": 2 },
|
||||
{ "format": "10", "weight": 4 },
|
||||
{ "format": "100", "weight": 3 },
|
||||
{ "format": "1#,000", "weight": 1 },
|
||||
{ "format": "10#,000", "weight": 0.4 },
|
||||
{ "format": "100#,000", "weight": 0.1 }
|
||||
{ "format": "{int(1,9)}", "weight": 2 },
|
||||
{ "format": "{int(10,99)}", "weight": 4 },
|
||||
{ "format": "{int(100,999)}", "weight": 3 },
|
||||
"{int(1,9)},{digits(3)}",
|
||||
{ "format": "{int(10,99)},{digits(3)}", "weight": 0.4 },
|
||||
{ "format": "{int(100,999)},{digits(3)}", "weight": 0.1 }
|
||||
],
|
||||
"cents": [
|
||||
{ "format": "00", "weight": 3 },
|
||||
"49",
|
||||
"95",
|
||||
"99"
|
||||
]
|
||||
"cents": [{ "format": "{digits(2)}", "weight": 3 }, "49", "95", "99"]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"noun": ["anchor", "archer", "beacon", "bridge", "captain", "cavern", "cottage", "ember", "engine", "falcon", "ferry", "forest", "fox", "garden", "glacier", "harbor", "hermit", "hollow", "island", "lantern", "lighthouse", "mariner", "meadow", "mountain", "orchard", "otter", "pilgrim", "prairie", "quarry", "raven", "ridge", "river", "sailor", "scholar", "signal", "sparrow", "stranger", "summit", "thicket", "tower", "traveler", "valley", "voyager", "wanderer", "willow", "woodland"],
|
||||
"verb": ["abandons", "anchors", "awakens", "befriends", "builds", "calms", "carries", "chases", "conceals", "crosses", "embraces", "follows", "gathers", "greets", "guards", "guides", "haunts", "heralds", "honors", "kindles", "leads", "mends", "mirrors", "outlasts", "ponders", "reveals", "salutes", "shapes", "shelters", "shields", "summons", "tends", "traces", "weathers"],
|
||||
"prep": ["above", "across", "against", "alongside", "amid", "around", "beneath", "beside", "between", "beyond", "near", "over", "past", "through", "toward", "under", "within"],
|
||||
"end": [".", ".", ".", ".", "!"]
|
||||
"end": [{ "format": ".", "weight": 4 }, "!"]
|
||||
},
|
||||
{
|
||||
"format": "{lead} {noun} {adv} {verb} {prep} {adj} {noun}{end}",
|
||||
@@ -18,7 +18,7 @@
|
||||
"verb": ["builds", "carries", "chases", "crosses", "follows", "gathers", "guards", "guides", "leads", "mirrors", "outlasts", "reveals", "shapes", "shelters", "summons", "tends", "traces", "weathers"],
|
||||
"adj": ["ancient", "bright", "distant", "fierce", "fragile", "gentle", "golden", "hidden", "lonely", "quiet", "restless", "rugged", "silent", "stormy", "weary", "wild"],
|
||||
"prep": ["across", "against", "alongside", "amid", "beneath", "between", "beyond", "near", "over", "past", "through", "toward", "within"],
|
||||
"end": [".", ".", ".", "!"]
|
||||
"end": [{ "format": ".", "weight": 3 }, "!"]
|
||||
},
|
||||
{
|
||||
"format": "{q} {adj} {noun} {verb} {prep} {adj} {noun}?",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{ "format": "{name}", "rows": "sex.tsv", "key": "code", "name": "name" }
|
||||
@@ -0,0 +1,3 @@
|
||||
code name
|
||||
f female
|
||||
m male
|
||||
|
+18
-2
@@ -1,3 +1,19 @@
|
||||
[
|
||||
{ "format": "100-00-0000" }
|
||||
{
|
||||
"format": "{area}-{group}-{serial}",
|
||||
"area": [
|
||||
{ "format": "00{int(1,9)}", "weight": 9 },
|
||||
{ "format": "0{int(10,99)}", "weight": 90 },
|
||||
{ "format": "{int(100,665)}", "weight": 566 },
|
||||
{ "format": "{int(667,899)}", "weight": 233 }
|
||||
],
|
||||
"group": [
|
||||
{ "format": "0{int(1,9)}", "weight": 9 },
|
||||
{ "format": "{int(10,99)}", "weight": 90 }
|
||||
],
|
||||
"serial": [
|
||||
{ "format": "000{int(1,9)}", "weight": 9 },
|
||||
{ "format": "00{int(10,99)}", "weight": 90 },
|
||||
{ "format": "0{int(100,999)}", "weight": 900 },
|
||||
{ "format": "{int(1000,9999)}", "weight": 9000 }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,8 +1 @@
|
||||
[
|
||||
{
|
||||
"format": "{hour}:{minute} {ampm}",
|
||||
"hour": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"],
|
||||
"minute": [{ "format": "{t}0", "t": ["0", "1", "2", "3", "4", "5"] }],
|
||||
"ampm": ["AM", "PM"]
|
||||
}
|
||||
]
|
||||
"{time('3:04 PM')}"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{ "format": "{name}", "rows": "title.tsv", "name": "name", "parent": "sex", "weight": "share" }
|
||||
@@ -0,0 +1,11 @@
|
||||
name sex share
|
||||
Dr f 6
|
||||
Dr m 6
|
||||
Miss f 4
|
||||
Mr m 60
|
||||
Mrs f 22
|
||||
Ms f 30
|
||||
Mx f 1
|
||||
Mx m 1
|
||||
Prof f 3
|
||||
Prof m 3
|
||||
|
+11
-6
@@ -1,12 +1,17 @@
|
||||
[
|
||||
{
|
||||
"format": "https://{host}{path}",
|
||||
"host": ["example.com", "www.example.com", "example.org", "www.example.org", "example.net", "example.io", "example.co", "blog.example.com", "blog.example.net", "shop.example.com", "store.example.org", "app.example.io", "api.example.com", "docs.example.org", "news.example.net", "mail.example.com"],
|
||||
"path": [
|
||||
"",
|
||||
"",
|
||||
{ "format": "/{seg}", "seg": ["about", "account", "blog", "careers", "cart", "categories", "checkout", "contact", "dashboard", "downloads", "events", "faq", "features", "help", "home", "items", "login", "news", "order", "pricing", "privacy", "products", "profile", "register", "search", "settings", "signup", "support", "terms", "welcome"] },
|
||||
{ "format": "/{seg}/{sub}", "weight": 0.5, "seg": ["blog", "docs", "help", "products", "category", "user"], "sub": ["getting-started", "overview", "guide", "2024", "latest", "popular", "archive", "details"] }
|
||||
]
|
||||
{ "format": "", "weight": 2 },
|
||||
{
|
||||
"format": "/{seg}",
|
||||
"seg": ["about", "account", "blog", "careers", "cart", "categories", "checkout", "contact", "dashboard", "downloads", "events", "faq", "features", "help", "home", "items", "login", "news", "order", "pricing", "privacy", "products", "profile", "register", "search", "settings", "signup", "support", "terms", "welcome"]
|
||||
},
|
||||
{
|
||||
"format": "/{seg}/{sub}",
|
||||
"weight": 0.5,
|
||||
"seg": ["blog", "docs", "help", "products", "category", "user"],
|
||||
"sub": ["getting-started", "overview", "guide", "2024", "latest", "popular", "archive", "details"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,21 +4,20 @@
|
||||
"weight": 2,
|
||||
"first": ["alex", "ash", "avery", "bailey", "blake", "cameron", "casey", "charlie", "chris", "dakota", "drew", "elliot", "emerson", "finley", "frankie", "harley", "hayden", "jamie", "jordan", "kai", "kendall", "lane", "logan", "morgan", "parker", "quinn", "reese", "riley", "rowan", "sam", "sawyer", "skyler", "spencer", "taylor"],
|
||||
"last": ["adams", "brooks", "carter", "cole", "evans", "fisher", "fox", "grant", "gray", "hayes", "hunt", "lane", "lowe", "marsh", "mason", "morris", "north", "page", "quinn", "reed", "rhodes", "shaw", "stone", "vale", "wells", "west", "wolfe"],
|
||||
"sep": ["", "", "", ".", "_"],
|
||||
"n": ["", "", "", "", "1", "7", "12", "23", "42", "77", "99", "2024"]
|
||||
"sep": [{ "format": "", "weight": 3 }, ".", "_"],
|
||||
"n": [{ "format": "", "weight": 4 }, "1", "7", "12", "23", "42", "77", "99", "2024"]
|
||||
},
|
||||
{
|
||||
"format": "{adj}{sep}{noun}{n}",
|
||||
"weight": 2,
|
||||
"adj": ["bluish", "brave", "chill", "cosmic", "cool", "dizzy", "electric", "epic", "fluffy", "frosty", "funky", "fuzzy", "golden", "groovy", "grumpy", "happy", "hyper", "jazzy", "lucky", "mega", "mighty", "neon", "nimble", "quirky", "rad", "rusty", "salty", "shiny", "silent", "sleepy", "snappy", "sneaky", "sparkly", "spicy", "sunny", "super", "swift", "turbo", "wild", "witty", "zany"],
|
||||
"noun": ["badger", "biscuit", "cactus", "comet", "donut", "dragon", "falcon", "ferret", "gizmo", "goblin", "hamster", "koala", "llama", "mango", "muffin", "narwhal", "noodle", "otter", "panda", "penguin", "pickle", "pirate", "pixel", "potato", "pretzel", "raccoon", "raptor", "robot", "taco", "toast", "trunk", "turtle", "unicorn", "viking", "waffle", "walrus", "wizard", "yeti"],
|
||||
"sep": ["", "", "", ".", "_"],
|
||||
"n": ["", "", "7", "13", "42", "77", "99", "123", "420", "2024"]
|
||||
"sep": [{ "format": "", "weight": 3 }, ".", "_"],
|
||||
"n": [{ "format": "", "weight": 2 }, "7", "13", "42", "77", "99", "123", "420", "2024"]
|
||||
},
|
||||
{
|
||||
"format": "{w}{n}",
|
||||
"weight": 1,
|
||||
"w": ["blooper", "blupp", "boop", "byteme", "coolcat", "derpina", "doomguy", "epicgamer", "fizzbuzz", "glitchlord", "glorptron", "gronkzilla", "kapowski", "lazerwolf", "megashark", "mooncat", "narfle", "nightowl", "noodlemaster", "oofster", "pixelfox", "pixelpusher", "plonkers", "retrowave", "snork", "splatmaster", "stardust", "superdog", "voidwalker", "vroom", "wahoo", "waooooh", "wootwoot", "yeetus", "zapper", "zoomzoom"],
|
||||
"n": ["", "", "7", "14", "42", "69", "99", "123", "420", "1337", "2000", "9000"]
|
||||
"n": [{ "format": "", "weight": 2 }, "7", "14", "42", "69", "99", "123", "420", "1337", "2000", "9000"]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
[
|
||||
{
|
||||
"format": "{pre}{n}.{n}.{n}{suffix}",
|
||||
"n": [{ "format": "0", "weight": 3 }, { "format": "10" }],
|
||||
"n": [{ "format": "{digits(1)}", "weight": 3 }, "{int(10,99)}"],
|
||||
"pre": ["", { "format": "v", "weight": 0.4 }],
|
||||
"suffix": ["", { "format": "-{tag}.{m}", "tag": ["alpha", "beta", "rc"], "m": ["1", "2", "3"], "weight": 0.3 }]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"format": "{street} {street-number}\n{postal-code} {locality}",
|
||||
"street": "{.street.name}",
|
||||
"street-number": [
|
||||
"{int(1,9)}",
|
||||
"{int(10,99)}",
|
||||
{ "format": "{int(100,999)}", "weight": 0.2 },
|
||||
{ "format": "{int(1,9)}{upper(1)}", "weight": 0.2 },
|
||||
{ "format": "{int(10,99)}{upper(1)}", "weight": 0.1 },
|
||||
{ "format": "{int(100,999)}{upper(1)}", "weight": 0.05 }
|
||||
],
|
||||
"postal-code": "{.postal-code.code}",
|
||||
"locality": "{.locality.name}"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "format": "{name}", "rows": "locality.tsv", "key": "name", "parent": "municipality", "weight": "population" }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
{ "format": "{name}", "rows": "municipality.tsv", "key": "code", "name": "name", "parent": "region", "weight": "population" }
|
||||
@@ -0,0 +1,291 @@
|
||||
code name region population
|
||||
0114 Upplands Väsby 01 50323
|
||||
0115 Vallentuna 01 35119
|
||||
0117 Österåker 01 49787
|
||||
0120 Värmdö 01 46635
|
||||
0123 Järfälla 01 88950
|
||||
0125 Ekerö 01 28910
|
||||
0126 Huddinge 01 114304
|
||||
0127 Botkyrka 01 95905
|
||||
0128 Salem 01 17507
|
||||
0136 Haninge 01 100895
|
||||
0138 Tyresö 01 49179
|
||||
0139 Upplands-Bro 01 32868
|
||||
0140 Nykvarn 01 12342
|
||||
0160 Täby 01 77744
|
||||
0162 Danderyd 01 32425
|
||||
0163 Sollentuna 01 77624
|
||||
0180 Stockholm 01 995574
|
||||
0181 Södertälje 01 102911
|
||||
0182 Nacka 01 112112
|
||||
0183 Sundbyberg 01 56274
|
||||
0184 Solna 01 85789
|
||||
0186 Lidingö 01 48377
|
||||
0187 Vaxholm 01 11822
|
||||
0188 Norrtälje 01 66585
|
||||
0191 Sigtuna 01 52767
|
||||
0192 Nynäshamn 01 30579
|
||||
0305 Håbo 03 22973
|
||||
0319 Älvkarleby 03 9552
|
||||
0330 Knivsta 03 21193
|
||||
0331 Heby 03 14345
|
||||
0360 Tierp 03 21104
|
||||
0380 Uppsala 03 248016
|
||||
0381 Enköping 03 48591
|
||||
0382 Östhammar 03 22138
|
||||
0428 Vingåker 04 8750
|
||||
0461 Gnesta 04 11458
|
||||
0480 Nyköping 04 58344
|
||||
0481 Oxelösund 04 12031
|
||||
0482 Flen 04 15362
|
||||
0483 Katrineholm 04 34154
|
||||
0484 Eskilstuna 04 107203
|
||||
0486 Strängnäs 04 39313
|
||||
0488 Trosa 04 14927
|
||||
0509 Ödeshög 05 5237
|
||||
0512 Ydre 05 3626
|
||||
0513 Kinda 05 9957
|
||||
0560 Boxholm 05 5517
|
||||
0561 Åtvidaberg 05 11467
|
||||
0562 Finspång 05 21623
|
||||
0563 Valdemarsvik 05 7525
|
||||
0580 Linköping 05 168035
|
||||
0581 Norrköping 05 144980
|
||||
0582 Söderköping 05 14789
|
||||
0583 Motala 05 43505
|
||||
0584 Vadstena 05 7490
|
||||
0586 Mjölby 05 28695
|
||||
0604 Aneby 06 6797
|
||||
0617 Gnosjö 06 9131
|
||||
0642 Mullsjö 06 7594
|
||||
0643 Habo 06 13456
|
||||
0662 Gislaved 06 28936
|
||||
0665 Vaggeryd 06 14825
|
||||
0680 Jönköping 06 147654
|
||||
0682 Nässjö 06 31587
|
||||
0683 Värnamo 06 34542
|
||||
0684 Sävsjö 06 11563
|
||||
0685 Vetlanda 06 27528
|
||||
0686 Eksjö 06 17792
|
||||
0687 Tranås 06 18604
|
||||
0760 Uppvidinge 07 9061
|
||||
0761 Lessebo 07 8289
|
||||
0763 Tingsryd 07 11966
|
||||
0764 Alvesta 07 19830
|
||||
0765 Älmhult 07 17653
|
||||
0767 Markaryd 07 9938
|
||||
0780 Växjö 07 98334
|
||||
0781 Ljungby 07 28280
|
||||
0821 Högsby 08 5321
|
||||
0834 Torsås 08 6984
|
||||
0840 Mörbylånga 08 16224
|
||||
0860 Hultsfred 08 13673
|
||||
0861 Mönsterås 08 13069
|
||||
0862 Emmaboda 08 9006
|
||||
0880 Kalmar 08 72704
|
||||
0881 Nybro 08 19951
|
||||
0882 Oskarshamn 08 26923
|
||||
0883 Västervik 08 36447
|
||||
0884 Vimmerby 08 15384
|
||||
0885 Borgholm 08 10666
|
||||
0980 Gotland 09 60971
|
||||
1060 Olofström 10 13000
|
||||
1080 Karlskrona 10 66301
|
||||
1081 Ronneby 10 28741
|
||||
1082 Karlshamn 10 31751
|
||||
1083 Sölvesborg 10 17430
|
||||
1214 Svalöv 12 14543
|
||||
1230 Staffanstorp 12 27303
|
||||
1231 Burlöv 12 20101
|
||||
1233 Vellinge 12 37816
|
||||
1256 Östra Göinge 12 13978
|
||||
1257 Örkelljunga 12 10277
|
||||
1260 Bjuv 12 15985
|
||||
1261 Kävlinge 12 32477
|
||||
1262 Lomma 12 24715
|
||||
1263 Svedala 12 23581
|
||||
1264 Skurup 12 17099
|
||||
1265 Sjöbo 12 19337
|
||||
1266 Hörby 12 15562
|
||||
1267 Höör 12 17518
|
||||
1270 Tomelilla 12 13639
|
||||
1272 Bromölla 12 12470
|
||||
1273 Osby 12 12947
|
||||
1275 Perstorp 12 7235
|
||||
1276 Klippan 12 17714
|
||||
1277 Åstorp 12 16449
|
||||
1278 Båstad 12 16026
|
||||
1280 Malmö 12 365644
|
||||
1281 Lund 12 131590
|
||||
1282 Landskrona 12 47309
|
||||
1283 Helsingborg 12 152091
|
||||
1284 Höganäs 12 28430
|
||||
1285 Eslöv 12 34922
|
||||
1286 Ystad 12 32106
|
||||
1287 Trelleborg 12 47269
|
||||
1290 Kristianstad 12 86379
|
||||
1291 Simrishamn 12 18890
|
||||
1292 Ängelholm 12 45110
|
||||
1293 Hässleholm 12 52114
|
||||
1315 Hylte 13 10196
|
||||
1380 Halmstad 13 106084
|
||||
1381 Laholm 13 26595
|
||||
1382 Falkenberg 13 47337
|
||||
1383 Varberg 13 69070
|
||||
1384 Kungsbacka 13 85792
|
||||
1401 Härryda 14 40003
|
||||
1402 Partille 14 41060
|
||||
1407 Öckerö 14 12771
|
||||
1415 Stenungsund 14 27851
|
||||
1419 Tjörn 14 16092
|
||||
1421 Orust 14 15352
|
||||
1427 Sotenäs 14 9104
|
||||
1430 Munkedal 14 10354
|
||||
1435 Tanum 14 12773
|
||||
1438 Dals-Ed 14 4606
|
||||
1439 Färgelanda 14 6376
|
||||
1440 Ale 14 32576
|
||||
1441 Lerum 14 43570
|
||||
1442 Vårgårda 14 12474
|
||||
1443 Bollebygd 14 9802
|
||||
1444 Grästorp 14 5555
|
||||
1445 Essunga 14 5560
|
||||
1446 Karlsborg 14 7023
|
||||
1447 Gullspång 14 5031
|
||||
1452 Tranemo 14 11839
|
||||
1460 Bengtsfors 14 9076
|
||||
1461 Mellerud 14 9052
|
||||
1462 Lilla Edet 14 14442
|
||||
1463 Mark 14 35155
|
||||
1465 Svenljunga 14 10747
|
||||
1466 Herrljunga 14 9497
|
||||
1470 Vara 14 16088
|
||||
1471 Götene 14 13286
|
||||
1472 Tibro 14 11338
|
||||
1473 Töreboda 14 9043
|
||||
1480 Göteborg 14 608993
|
||||
1481 Mölndal 14 71420
|
||||
1482 Kungälv 14 50313
|
||||
1484 Lysekil 14 13907
|
||||
1485 Uddevalla 14 57010
|
||||
1486 Strömstad 14 13482
|
||||
1487 Vänersborg 14 40041
|
||||
1488 Trollhättan 14 59003
|
||||
1489 Alingsås 14 42722
|
||||
1490 Borås 14 114872
|
||||
1491 Ulricehamn 14 24985
|
||||
1492 Åmål 14 11906
|
||||
1493 Mariestad 14 24583
|
||||
1494 Lidköping 14 40425
|
||||
1495 Skara 14 18707
|
||||
1496 Skövde 14 57995
|
||||
1497 Hjo 14 9350
|
||||
1498 Tidaholm 14 12805
|
||||
1499 Falköping 14 32806
|
||||
1715 Kil 17 12061
|
||||
1730 Eda 17 8412
|
||||
1737 Torsby 17 11321
|
||||
1760 Storfors 17 3788
|
||||
1761 Hammarö 17 16992
|
||||
1762 Munkfors 17 3625
|
||||
1763 Forshaga 17 11520
|
||||
1764 Grums 17 9004
|
||||
1765 Årjäng 17 9825
|
||||
1766 Sunne 17 13356
|
||||
1780 Karlstad 17 98084
|
||||
1781 Kristinehamn 17 23756
|
||||
1782 Filipstad 17 9776
|
||||
1783 Hagfors 17 11418
|
||||
1784 Arvika 17 25547
|
||||
1785 Säffle 17 14899
|
||||
1814 Lekeberg 18 8606
|
||||
1860 Laxå 18 5423
|
||||
1861 Hallsberg 18 16120
|
||||
1862 Degerfors 18 9278
|
||||
1863 Hällefors 18 6321
|
||||
1864 Ljusnarsberg 18 4369
|
||||
1880 Örebro 18 160140
|
||||
1881 Kumla 18 22681
|
||||
1882 Askersund 18 11477
|
||||
1883 Karlskoga 18 30180
|
||||
1884 Nora 18 10639
|
||||
1885 Lindesberg 18 23141
|
||||
1904 Skinnskatteberg 19 4256
|
||||
1907 Surahammar 19 9845
|
||||
1960 Kungsör 19 8694
|
||||
1961 Hallstahammar 19 16653
|
||||
1962 Norberg 19 5452
|
||||
1980 Västerås 19 160634
|
||||
1981 Sala 19 22843
|
||||
1982 Fagersta 19 13072
|
||||
1983 Köping 19 25729
|
||||
1984 Arboga 19 13980
|
||||
2021 Vansbro 20 6752
|
||||
2023 Malung-Sälen 20 10254
|
||||
2026 Gagnef 20 10384
|
||||
2029 Leksand 20 16137
|
||||
2031 Rättvik 20 10998
|
||||
2034 Orsa 20 6851
|
||||
2039 Älvdalen 20 6882
|
||||
2061 Smedjebacken 20 10823
|
||||
2062 Mora 20 20540
|
||||
2080 Falun 20 59945
|
||||
2081 Borlänge 20 51425
|
||||
2082 Säter 20 11223
|
||||
2083 Hedemora 20 15281
|
||||
2084 Avesta 20 22417
|
||||
2085 Ludvika 20 26634
|
||||
2101 Ockelbo 21 5715
|
||||
2104 Hofors 21 9281
|
||||
2121 Ovanåker 21 11341
|
||||
2132 Nordanstig 21 9262
|
||||
2161 Ljusdal 21 18445
|
||||
2180 Gävle 21 103838
|
||||
2181 Sandviken 21 38360
|
||||
2182 Söderhamn 21 24545
|
||||
2183 Bollnäs 21 26243
|
||||
2184 Hudiksvall 21 37528
|
||||
2260 Ånge 22 9044
|
||||
2262 Timrå 22 17521
|
||||
2280 Härnösand 22 24515
|
||||
2281 Sundsvall 22 99048
|
||||
2282 Kramfors 22 17491
|
||||
2283 Sollefteå 22 18396
|
||||
2284 Örnsköldsvik 22 55443
|
||||
2303 Ragunda 23 5146
|
||||
2305 Bräcke 23 6035
|
||||
2309 Krokom 23 15680
|
||||
2313 Strömsund 23 11023
|
||||
2321 Åre 23 12693
|
||||
2326 Berg 23 7108
|
||||
2361 Härjedalen 23 10175
|
||||
2380 Östersund 23 64979
|
||||
2401 Nordmaling 24 6942
|
||||
2403 Bjurholm 24 2359
|
||||
2404 Vindeln 24 5417
|
||||
2409 Robertsfors 24 6690
|
||||
2417 Norsjö 24 3968
|
||||
2418 Malå 24 2962
|
||||
2421 Storuman 24 5577
|
||||
2422 Sorsele 24 2357
|
||||
2425 Dorotea 24 2294
|
||||
2460 Vännäs 24 9132
|
||||
2462 Vilhelmina 24 6229
|
||||
2463 Åsele 24 2694
|
||||
2480 Umeå 24 134249
|
||||
2481 Lycksele 24 12118
|
||||
2482 Skellefteå 24 78150
|
||||
2505 Arvidsjaur 25 6089
|
||||
2506 Arjeplog 25 2599
|
||||
2510 Jokkmokk 25 4701
|
||||
2513 Överkalix 25 3201
|
||||
2514 Kalix 25 15391
|
||||
2518 Övertorneå 25 4057
|
||||
2521 Pajala 25 5857
|
||||
2523 Gällivare 25 17233
|
||||
2560 Älvsbyn 25 7774
|
||||
2580 Luleå 25 79645
|
||||
2581 Piteå 25 42447
|
||||
2582 Boden 25 28049
|
||||
2583 Haparanda 25 9151
|
||||
2584 Kiruna 25 22426
|
||||
|
@@ -0,0 +1 @@
|
||||
{ "format": "{code}", "rows": "postal-code.tsv", "key": "code", "parent": "locality" }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
{ "format": "{name}", "rows": "region.tsv", "key": "code", "name": "name", "weight": "population" }
|
||||
@@ -0,0 +1,22 @@
|
||||
code name population timezone
|
||||
01 Stockholms län 2473307 Europe/Stockholm
|
||||
03 Uppsala län 407912 Europe/Stockholm
|
||||
04 Södermanlands län 301542 Europe/Stockholm
|
||||
05 Östergötlands län 472446 Europe/Stockholm
|
||||
06 Jönköpings län 370009 Europe/Stockholm
|
||||
07 Kronobergs län 203351 Europe/Stockholm
|
||||
08 Kalmar län 246352 Europe/Stockholm
|
||||
09 Gotlands län 60971 Europe/Stockholm
|
||||
10 Blekinge län 157223 Europe/Stockholm
|
||||
12 Skåne län 1428626 Europe/Stockholm
|
||||
13 Hallands län 345074 Europe/Stockholm
|
||||
14 Västra Götalands län 1772821 Europe/Stockholm
|
||||
17 Värmlands län 283384 Europe/Stockholm
|
||||
18 Örebro län 308375 Europe/Stockholm
|
||||
19 Västmanlands län 281158 Europe/Stockholm
|
||||
20 Dalarnas län 286546 Europe/Stockholm
|
||||
21 Gävleborgs län 284558 Europe/Stockholm
|
||||
22 Västernorrlands län 241458 Europe/Stockholm
|
||||
23 Jämtlands län 132839 Europe/Stockholm
|
||||
24 Västerbottens län 281138 Europe/Stockholm
|
||||
25 Norrbottens län 248620 Europe/Stockholm
|
||||
|
@@ -0,0 +1 @@
|
||||
{ "format": "{name}", "rows": "street.tsv", "parent": "locality", "weight": "segments" }
|
||||
+14765
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"format": "{street-number} {street}\n{locality}, {region} {postal-code}",
|
||||
"street": "{.street.name}",
|
||||
"street-number": [
|
||||
"{int(10,99)}",
|
||||
{ "format": "{int(100,999)}", "weight": 2 },
|
||||
{ "format": "{int(1000,9999)}", "weight": 0.5 },
|
||||
{ "format": "{int(10,99)}{int(100,999)}", "weight": 0.3 }
|
||||
],
|
||||
"locality": "{.locality.name}",
|
||||
"region": "{.region.abbr}",
|
||||
"postal-code": "{.postal-code.code}"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "format": "{name}", "rows": "locality.tsv", "key": "code", "name": "name", "parent": "municipality", "weight": "population" }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
{ "format": "{name}", "rows": "municipality.tsv", "key": "code", "name": "name", "parent": "region", "weight": "population" }
|
||||
@@ -0,0 +1,666 @@
|
||||
code name region population
|
||||
01001 Autauga County AL 61920
|
||||
01003 Baldwin County AL 267761
|
||||
01031 Coffee County AL 56953
|
||||
01055 Etowah County AL 103886
|
||||
01069 Houston County AL 110318
|
||||
01073 Jefferson County AL 665742
|
||||
01077 Lauderdale County AL 97135
|
||||
01081 Lee County AL 189881
|
||||
01083 Limestone County AL 122928
|
||||
01089 Madison County AL 433516
|
||||
01097 Mobile County AL 411658
|
||||
01101 Montgomery County AL 225891
|
||||
01103 Morgan County AL 126483
|
||||
01113 Russell County AL 58898
|
||||
01117 Shelby County AL 238552
|
||||
01125 Tuscaloosa County AL 241368
|
||||
02020 Anchorage Municipality AK 287155
|
||||
02090 Fairbanks North Star Borough AK 93972
|
||||
02110 Juneau City and Borough AK 31609
|
||||
04003 Cochise County AZ 126332
|
||||
04005 Coconino County AZ 144368
|
||||
04013 Maricopa County AZ 4689558
|
||||
04015 Mohave County AZ 228102
|
||||
04019 Pima County AZ 1074685
|
||||
04021 Pinal County AZ 539380
|
||||
04025 Yavapai County AZ 252552
|
||||
04027 Yuma County AZ 224449
|
||||
05007 Benton County AR 332554
|
||||
05031 Craighead County AR 116957
|
||||
05045 Faulkner County AR 133979
|
||||
05051 Garland County AR 99695
|
||||
05055 Greene County AR 47411
|
||||
05069 Jefferson County AR 62987
|
||||
05085 Lonoke County AR 76664
|
||||
05091 Miller County AR 42357
|
||||
05115 Pope County AR 64976
|
||||
05119 Pulaski County AR 404611
|
||||
05125 Saline County AR 133288
|
||||
05131 Sebastian County AR 130641
|
||||
05143 Washington County AR 271213
|
||||
06001 Alameda County CA 1636630
|
||||
06007 Butte County CA 209211
|
||||
06013 Contra Costa County CA 1170070
|
||||
06019 Fresno County CA 1035456
|
||||
06023 Humboldt County CA 131647
|
||||
06025 Imperial County CA 181411
|
||||
06029 Kern County CA 927068
|
||||
06031 Kings County CA 154327
|
||||
06037 Los Angeles County CA 9694934
|
||||
06039 Madera County CA 167927
|
||||
06041 Marin County CA 253694
|
||||
06047 Merced County CA 297260
|
||||
06053 Monterey County CA 433729
|
||||
06055 Napa County CA 132949
|
||||
06059 Orange County CA 3149507
|
||||
06061 Placer County CA 442081
|
||||
06065 Riverside County CA 2544916
|
||||
06067 Sacramento County CA 1618460
|
||||
06069 San Benito County CA 70082
|
||||
06071 San Bernardino County CA 2224091
|
||||
06073 San Diego County CA 3282248
|
||||
06075 San Francisco County CA 826079
|
||||
06077 San Joaquin County CA 823815
|
||||
06079 San Luis Obispo County CA 282367
|
||||
06081 San Mateo County CA 743568
|
||||
06083 Santa Barbara County CA 442065
|
||||
06085 Santa Clara County CA 1914391
|
||||
06087 Santa Cruz County CA 258852
|
||||
06089 Shasta County CA 181648
|
||||
06095 Solano County CA 455376
|
||||
06097 Sonoma County CA 486444
|
||||
06099 Stanislaus County CA 557719
|
||||
06101 Sutter County CA 98787
|
||||
06107 Tulare County CA 485146
|
||||
06111 Ventura County CA 830851
|
||||
06113 Yolo County CA 224410
|
||||
08001 Adams County CO 554668
|
||||
08005 Arapahoe County CO 673820
|
||||
08013 Boulder County CO 328560
|
||||
08014 Broomfield County CO 79174
|
||||
08031 Denver County CO 740613
|
||||
08035 Douglas County CO 399396
|
||||
08041 El Paso County CO 757040
|
||||
08059 Jefferson County CO 580451
|
||||
08069 Larimer County CO 377292
|
||||
08077 Mesa County CO 162845
|
||||
08101 Pueblo County CO 169277
|
||||
08123 Weld County CO 378426
|
||||
09110 Capitol Planning Region CT 994115
|
||||
09120 Greater Bridgeport Planning Region CT 337697
|
||||
09130 Lower Connecticut River Valley Planning Region CT 177311
|
||||
09140 Naugatuck Valley Planning Region CT 463349
|
||||
09160 Northwest Hills Planning Region CT 114690
|
||||
09170 South Central Connecticut Planning Region CT 578741
|
||||
09180 Southeastern Connecticut Planning Region CT 284015
|
||||
09190 Western Connecticut Planning Region CT 640482
|
||||
10001 Kent County DE 194786
|
||||
10003 New Castle County DE 588026
|
||||
11001 District of Columbia DC 693645
|
||||
12001 Alachua County FL 290028
|
||||
12005 Bay County FL 204479
|
||||
12009 Brevard County FL 663982
|
||||
12011 Broward County FL 2013317
|
||||
12031 Duval County FL 1062963
|
||||
12033 Escambia County FL 333834
|
||||
12035 Flagler County FL 140360
|
||||
12057 Hillsborough County FL 1574115
|
||||
12061 Indian River County FL 172799
|
||||
12069 Lake County FL 456068
|
||||
12071 Lee County FL 875607
|
||||
12073 Leon County FL 299048
|
||||
12081 Manatee County FL 468200
|
||||
12083 Marion County FL 442660
|
||||
12086 Miami-Dade County FL 2802029
|
||||
12091 Okaloosa County FL 221810
|
||||
12095 Orange County FL 1528002
|
||||
12097 Osceola County FL 481718
|
||||
12099 Palm Beach County FL 1575726
|
||||
12103 Pinellas County FL 948563
|
||||
12105 Polk County FL 874790
|
||||
12111 St. Lucie County FL 402449
|
||||
12115 Sarasota County FL 479958
|
||||
12117 Seminole County FL 491884
|
||||
12127 Volusia County FL 606573
|
||||
13015 Bartow County GA 120800
|
||||
13021 Bibb County GA 157556
|
||||
13031 Bulloch County GA 86949
|
||||
13045 Carroll County GA 131036
|
||||
13051 Chatham County GA 311855
|
||||
13057 Cherokee County GA 299273
|
||||
13059 Clarke County GA 129921
|
||||
13067 Cobb County GA 793345
|
||||
13077 Coweta County GA 160240
|
||||
13089 DeKalb County GA 774394
|
||||
13095 Dougherty County GA 82616
|
||||
13097 Douglas County GA 154293
|
||||
13113 Fayette County GA 125156
|
||||
13115 Floyd County GA 101378
|
||||
13121 Fulton County GA 1098791
|
||||
13135 Gwinnett County GA 1018099
|
||||
13139 Hall County GA 226568
|
||||
13151 Henry County GA 264922
|
||||
13153 Houston County GA 178214
|
||||
13179 Liberty County GA 70313
|
||||
13185 Lowndes County GA 122867
|
||||
13215 Muscogee County GA 202171
|
||||
13245 Richmond County GA 206559
|
||||
13285 Troup County GA 72844
|
||||
13313 Whitfield County GA 106212
|
||||
16001 Ada County ID 546141
|
||||
16005 Bannock County ID 91591
|
||||
16019 Bonneville County ID 135771
|
||||
16027 Canyon County ID 275123
|
||||
16055 Kootenai County ID 191864
|
||||
16057 Latah County ID 41842
|
||||
16065 Madison County ID 55172
|
||||
16069 Nez Perce County ID 42905
|
||||
16083 Twin Falls County ID 97539
|
||||
17001 Adams County IL 64267
|
||||
17007 Boone County IL 53568
|
||||
17019 Champaign County IL 209972
|
||||
17031 Cook County IL 5194625
|
||||
17037 DeKalb County IL 101835
|
||||
17043 DuPage County IL 934298
|
||||
17089 Kane County IL 525757
|
||||
17093 Kendall County IL 145470
|
||||
17095 Knox County IL 47767
|
||||
17097 Lake County IL 719339
|
||||
17111 McHenry County IL 317751
|
||||
17113 McLean County IL 171419
|
||||
17115 Macon County IL 99300
|
||||
17119 Madison County IL 263110
|
||||
17143 Peoria County IL 178553
|
||||
17161 Rock Island County IL 141869
|
||||
17163 St. Clair County IL 250708
|
||||
17167 Sangamon County IL 194170
|
||||
17179 Tazewell County IL 130049
|
||||
17183 Vermilion County IL 71259
|
||||
17197 Will County IL 712253
|
||||
17201 Winnebago County IL 283674
|
||||
18003 Allen County IN 402329
|
||||
18005 Bartholomew County IN 85729
|
||||
18011 Boone County IN 80689
|
||||
18019 Clark County IN 130451
|
||||
18035 Delaware County IN 113106
|
||||
18039 Elkhart County IN 208774
|
||||
18043 Floyd County IN 82153
|
||||
18053 Grant County IN 66524
|
||||
18057 Hamilton County IN 387036
|
||||
18059 Hancock County IN 90969
|
||||
18063 Hendricks County IN 193510
|
||||
18067 Howard County IN 83904
|
||||
18081 Johnson County IN 174262
|
||||
18089 Lake County IN 504612
|
||||
18091 LaPorte County IN 111294
|
||||
18095 Madison County IN 135088
|
||||
18097 Marion County IN 992196
|
||||
18105 Monroe County IN 143345
|
||||
18127 Porter County IN 176049
|
||||
18141 St. Joseph County IN 272861
|
||||
18157 Tippecanoe County IN 190456
|
||||
18163 Vanderburgh County IN 181995
|
||||
18167 Vigo County IN 106512
|
||||
18177 Wayne County IN 66169
|
||||
19013 Black Hawk County IA 131532
|
||||
19033 Cerro Gordo County IA 42372
|
||||
19049 Dallas County IA 118457
|
||||
19061 Dubuque County IA 99381
|
||||
19103 Johnson County IA 160044
|
||||
19113 Linn County IA 232028
|
||||
19127 Marshall County IA 39890
|
||||
19153 Polk County IA 516546
|
||||
19155 Pottawattamie County IA 92996
|
||||
19163 Scott County IA 175259
|
||||
19169 Story County IA 101291
|
||||
19179 Wapello County IA 35210
|
||||
19193 Woodbury County IA 106649
|
||||
20045 Douglas County KS 120920
|
||||
20055 Finney County KS 37505
|
||||
20057 Ford County KS 33993
|
||||
20091 Johnson County KS 636906
|
||||
20103 Leavenworth County KS 84590
|
||||
20155 Reno County KS 61539
|
||||
20161 Riley County KS 72598
|
||||
20169 Saline County KS 53377
|
||||
20173 Sedgwick County KS 538433
|
||||
20177 Shawnee County KS 178607
|
||||
20209 Wyandotte County KS 170597
|
||||
21015 Boone County KY 145316
|
||||
21047 Christian County KY 70115
|
||||
21059 Daviess County KY 104898
|
||||
21067 Fayette County KY 329751
|
||||
21073 Franklin County KY 52649
|
||||
21093 Hardin County KY 113482
|
||||
21101 Henderson County KY 44255
|
||||
21111 Jefferson County KY 795222
|
||||
21113 Jessamine County KY 57147
|
||||
21117 Kenton County KY 175779
|
||||
21145 McCracken County KY 67553
|
||||
21151 Madison County KY 101696
|
||||
21209 Scott County KY 62262
|
||||
21227 Warren County KY 149375
|
||||
22015 Bossier Parish LA 131867
|
||||
22017 Caddo Parish LA 224226
|
||||
22019 Calcasieu Parish LA 208466
|
||||
22033 East Baton Rouge Parish LA 456180
|
||||
22045 Iberia Parish LA 66846
|
||||
22051 Jefferson Parish LA 431398
|
||||
22071 Orleans Parish LA 362154
|
||||
22073 Ouachita Parish LA 158542
|
||||
22079 Rapides Parish LA 125877
|
||||
22103 St. Tammany Parish LA 279108
|
||||
22109 Terrebonne Parish LA 104163
|
||||
23001 Androscoggin County ME 116487
|
||||
23005 Cumberland County ME 317222
|
||||
23019 Penobscot County ME 157967
|
||||
24003 Anne Arundel County MD 603380
|
||||
24021 Frederick County MD 302883
|
||||
24031 Montgomery County MD 1074582
|
||||
24033 Prince George's County MD 970374
|
||||
24043 Washington County MD 157731
|
||||
24045 Wicomico County MD 106899
|
||||
24510 Baltimore city MD 569997
|
||||
25001 Barnstable County MA 233539
|
||||
25003 Berkshire County MA 128224
|
||||
25005 Bristol County MA 593640
|
||||
25009 Essex County MA 826653
|
||||
25013 Hampden County MA 464338
|
||||
25015 Hampshire County MA 164065
|
||||
25017 Middlesex County MA 1669979
|
||||
25021 Norfolk County MA 739749
|
||||
25023 Plymouth County MA 546829
|
||||
25025 Suffolk County MA 791891
|
||||
25027 Worcester County MA 888502
|
||||
26017 Bay County MI 102123
|
||||
26025 Calhoun County MI 133408
|
||||
26049 Genesee County MI 401093
|
||||
26065 Ingham County MI 289709
|
||||
26075 Jackson County MI 159552
|
||||
26077 Kalamazoo County MI 263795
|
||||
26081 Kent County MI 675232
|
||||
26099 Macomb County MI 886221
|
||||
26111 Midland County MI 83754
|
||||
26121 Muskegon County MI 177901
|
||||
26125 Oakland County MI 1288337
|
||||
26139 Ottawa County MI 308459
|
||||
26145 Saginaw County MI 187688
|
||||
26147 St. Clair County MI 160486
|
||||
26161 Washtenaw County MI 370214
|
||||
26163 Wayne County MI 1769038
|
||||
27003 Anoka County MN 381605
|
||||
27013 Blue Earth County MN 70634
|
||||
27019 Carver County MN 114379
|
||||
27027 Clay County MN 67734
|
||||
27037 Dakota County MN 457710
|
||||
27053 Hennepin County MN 1284784
|
||||
27099 Mower County MN 40971
|
||||
27109 Olmsted County MN 166731
|
||||
27123 Ramsey County MN 541623
|
||||
27131 Rice County MN 69939
|
||||
27137 St. Louis County MN 200518
|
||||
27139 Scott County MN 159017
|
||||
27141 Sherburne County MN 104194
|
||||
27145 Stearns County MN 164110
|
||||
27147 Steele County MN 37464
|
||||
27163 Washington County MN 286895
|
||||
27169 Winona County MN 50523
|
||||
28033 DeSoto County MS 197918
|
||||
28035 Forrest County MS 79034
|
||||
28047 Harrison County MS 217136
|
||||
28049 Hinds County MS 211888
|
||||
28071 Lafayette County MS 59597
|
||||
28075 Lauderdale County MS 70317
|
||||
28081 Lee County MS 83731
|
||||
28089 Madison County MS 116298
|
||||
28105 Oktibbeha County MS 51896
|
||||
28121 Rankin County MS 162181
|
||||
28151 Washington County MS 40446
|
||||
29019 Boone County MO 191746
|
||||
29021 Buchanan County MO 83540
|
||||
29031 Cape Girardeau County MO 83999
|
||||
29037 Cass County MO 115859
|
||||
29043 Christian County MO 96725
|
||||
29047 Clay County MO 265032
|
||||
29051 Cole County MO 77908
|
||||
29077 Greene County MO 309286
|
||||
29095 Jackson County MO 732994
|
||||
29097 Jasper County MO 127428
|
||||
29183 St. Charles County MO 426499
|
||||
29189 St. Louis County MO 990911
|
||||
29510 St. Louis city MO 278144
|
||||
30013 Cascade County MT 85029
|
||||
30029 Flathead County MT 115429
|
||||
30031 Gallatin County MT 128740
|
||||
30049 Lewis and Clark County MT 75331
|
||||
30063 Missoula County MT 123513
|
||||
30093 Silver Bow County MT 36118
|
||||
30111 Yellowstone County MT 172692
|
||||
31001 Adams County NE 31071
|
||||
31019 Buffalo County NE 51172
|
||||
31053 Dodge County NE 38057
|
||||
31055 Douglas County NE 606460
|
||||
31079 Hall County NE 63633
|
||||
31109 Lancaster County NE 334049
|
||||
31119 Madison County NE 36106
|
||||
31141 Platte County NE 35649
|
||||
31153 Sarpy County NE 208303
|
||||
32003 Clark County NV 2407226
|
||||
32019 Lyon County NV 65088
|
||||
32031 Washoe County NV 509386
|
||||
32510 Carson City NV 58571
|
||||
33011 Hillsborough County NH 433415
|
||||
33013 Merrimack County NH 158078
|
||||
33017 Strafford County NH 135043
|
||||
34001 Atlantic County NJ 278657
|
||||
34003 Bergen County NJ 977026
|
||||
34007 Camden County NJ 535799
|
||||
34011 Cumberland County NJ 157148
|
||||
34013 Essex County NJ 896379
|
||||
34017 Hudson County NJ 735033
|
||||
34021 Mercer County NJ 399289
|
||||
34023 Middlesex County NJ 883335
|
||||
34025 Monmouth County NJ 651035
|
||||
34031 Passaic County NJ 531624
|
||||
34039 Union County NJ 601863
|
||||
35001 Bernalillo County NM 667601
|
||||
35005 Chaves County NM 63364
|
||||
35009 Curry County NM 46655
|
||||
35013 Doña Ana County NM 229091
|
||||
35015 Eddy County NM 62509
|
||||
35025 Lea County NM 74749
|
||||
35035 Otero County NM 70368
|
||||
35043 Sandoval County NM 159565
|
||||
35045 San Juan County NM 120340
|
||||
35049 Santa Fe County NM 156907
|
||||
36001 Albany County NY 321225
|
||||
36007 Broome County NY 195736
|
||||
36011 Cayuga County NY 74365
|
||||
36013 Chautauqua County NY 124126
|
||||
36015 Chemung County NY 80415
|
||||
36027 Dutchess County NY 300708
|
||||
36029 Erie County NY 946741
|
||||
36047 Kings County NY 2653963
|
||||
36055 Monroe County NY 750506
|
||||
36059 Nassau County NY 1398939
|
||||
36063 Niagara County NY 208912
|
||||
36065 Oneida County NY 226392
|
||||
36067 Onondaga County NY 466584
|
||||
36071 Orange County NY 417669
|
||||
36083 Rensselaer County NY 160510
|
||||
36091 Saratoga County NY 241343
|
||||
36093 Schenectady County NY 162581
|
||||
36103 Suffolk County NY 1546090
|
||||
36109 Tompkins County NY 104047
|
||||
36119 Westchester County NY 1015743
|
||||
37001 Alamance County NC 186177
|
||||
37019 Brunswick County NC 174702
|
||||
37021 Buncombe County NC 277417
|
||||
37025 Cabarrus County NC 249725
|
||||
37035 Catawba County NC 170172
|
||||
37049 Craven County NC 105025
|
||||
37051 Cumberland County NC 338473
|
||||
37057 Davidson County NC 180182
|
||||
37063 Durham County NC 347240
|
||||
37067 Forsyth County NC 401718
|
||||
37071 Gaston County NC 246558
|
||||
37081 Guilford County NC 562234
|
||||
37097 Iredell County NC 211798
|
||||
37101 Johnston County NC 256448
|
||||
37105 Lee County NC 70258
|
||||
37119 Mecklenburg County NC 1233383
|
||||
37127 Nash County NC 99365
|
||||
37129 New Hanover County NC 245959
|
||||
37133 Onslow County NC 217175
|
||||
37135 Orange County NC 152498
|
||||
37147 Pitt County NC 182936
|
||||
37151 Randolph County NC 149516
|
||||
37159 Rowan County NC 155096
|
||||
37179 Union County NC 267674
|
||||
37183 Wake County NC 1257235
|
||||
37191 Wayne County NC 122278
|
||||
37195 Wilson County NC 81150
|
||||
38015 Burleigh County ND 103251
|
||||
38017 Cass County ND 201794
|
||||
38035 Grand Forks County ND 74501
|
||||
38059 Morton County ND 34601
|
||||
38089 Stark County ND 34013
|
||||
38101 Ward County ND 68233
|
||||
38105 Williams County ND 41767
|
||||
39003 Allen County OH 100881
|
||||
39009 Athens County OH 63197
|
||||
39017 Butler County OH 400128
|
||||
39023 Clark County OH 135340
|
||||
39035 Cuyahoga County OH 1232925
|
||||
39041 Delaware County OH 242032
|
||||
39045 Fairfield County OH 169752
|
||||
39049 Franklin County OH 1361536
|
||||
39057 Greene County OH 174322
|
||||
39061 Hamilton County OH 838418
|
||||
39063 Hancock County OH 75034
|
||||
39085 Lake County OH 232217
|
||||
39089 Licking County OH 185564
|
||||
39093 Lorain County OH 323219
|
||||
39095 Lucas County OH 423347
|
||||
39099 Mahoning County OH 224706
|
||||
39101 Marion County OH 65115
|
||||
39103 Medina County OH 185025
|
||||
39109 Miami County OH 112634
|
||||
39113 Montgomery County OH 539598
|
||||
39119 Muskingum County OH 87014
|
||||
39133 Portage County OH 163404
|
||||
39139 Richland County OH 124893
|
||||
39151 Stark County OH 373771
|
||||
39153 Summit County OH 538376
|
||||
39155 Trumbull County OH 198972
|
||||
39159 Union County OH 73446
|
||||
39165 Warren County OH 257181
|
||||
39169 Wayne County OH 116758
|
||||
39173 Wood County OH 134176
|
||||
40027 Cleveland County OK 303973
|
||||
40031 Comanche County OK 122158
|
||||
40047 Garfield County OK 61779
|
||||
40101 Muskogee County OK 66708
|
||||
40109 Oklahoma County OK 822125
|
||||
40119 Payne County OK 83889
|
||||
40125 Pottawatomie County OK 75102
|
||||
40143 Tulsa County OK 698782
|
||||
40147 Washington County OK 54037
|
||||
41003 Benton County OR 97728
|
||||
41005 Clackamas County OR 426280
|
||||
41017 Deschutes County OR 213072
|
||||
41029 Jackson County OR 221795
|
||||
41033 Josephine County OR 87867
|
||||
41039 Lane County OR 381584
|
||||
41043 Linn County OR 132843
|
||||
41047 Marion County OR 355777
|
||||
41051 Multnomah County OR 795391
|
||||
41067 Washington County OR 611708
|
||||
41071 Yamhill County OR 110024
|
||||
42003 Allegheny County PA 1225035
|
||||
42011 Berks County PA 440072
|
||||
42013 Blair County PA 119541
|
||||
42027 Centre County PA 157393
|
||||
42043 Dauphin County PA 293351
|
||||
42045 Delaware County PA 580937
|
||||
42049 Erie County PA 265832
|
||||
42069 Lackawanna County PA 216502
|
||||
42071 Lancaster County PA 563159
|
||||
42075 Lebanon County PA 146380
|
||||
42077 Lehigh County PA 384383
|
||||
42079 Luzerne County PA 332126
|
||||
42081 Lycoming County PA 112587
|
||||
42091 Montgomery County PA 877643
|
||||
42095 Northampton County PA 324411
|
||||
42101 Philadelphia County PA 1574281
|
||||
42133 York County PA 473197
|
||||
44003 Kent County RI 173495
|
||||
44007 Providence County RI 678179
|
||||
45003 Aiken County SC 181515
|
||||
45007 Anderson County SC 219930
|
||||
45013 Beaufort County SC 204433
|
||||
45015 Berkeley County SC 274666
|
||||
45019 Charleston County SC 436200
|
||||
45035 Dorchester County SC 178397
|
||||
45041 Florence County SC 138504
|
||||
45045 Greenville County SC 583125
|
||||
45051 Horry County SC 427551
|
||||
45063 Lexington County SC 317588
|
||||
45077 Pickens County SC 139198
|
||||
45079 Richland County SC 434956
|
||||
45083 Spartanburg County SC 380857
|
||||
45085 Sumter County SC 105067
|
||||
45091 York County SC 306887
|
||||
46011 Brookings County SD 37635
|
||||
46013 Brown County SD 37561
|
||||
46099 Minnehaha County SD 212691
|
||||
46103 Pennington County SD 116792
|
||||
47001 Anderson County TN 82066
|
||||
47003 Bedford County TN 55273
|
||||
47009 Blount County TN 143820
|
||||
47011 Bradley County TN 115465
|
||||
47037 Davidson County TN 745904
|
||||
47063 Hamblen County TN 68843
|
||||
47065 Hamilton County TN 390833
|
||||
47093 Knox County TN 511453
|
||||
47113 Madison County TN 100790
|
||||
47119 Maury County TN 118131
|
||||
47125 Montgomery County TN 249935
|
||||
47141 Putnam County TN 86612
|
||||
47149 Rutherford County TN 386352
|
||||
47157 Shelby County TN 910226
|
||||
47163 Sullivan County TN 163759
|
||||
47165 Sumner County TN 215538
|
||||
47179 Washington County TN 141199
|
||||
47187 Williamson County TN 272061
|
||||
47189 Wilson County TN 175033
|
||||
48005 Angelina County TX 88154
|
||||
48027 Bell County TX 402248
|
||||
48029 Bexar County TX 2160088
|
||||
48037 Bowie County TX 92696
|
||||
48039 Brazoria County TX 419080
|
||||
48041 Brazos County TX 249088
|
||||
48061 Cameron County TX 433946
|
||||
48085 Collin County TX 1297179
|
||||
48091 Comal County TX 209166
|
||||
48099 Coryell County TX 85592
|
||||
48113 Dallas County TX 2661397
|
||||
48121 Denton County TX 1069346
|
||||
48135 Ector County TX 173801
|
||||
48139 Ellis County TX 240867
|
||||
48141 El Paso County TX 877858
|
||||
48157 Fort Bend County TX 975191
|
||||
48167 Galveston County TX 372207
|
||||
48181 Grayson County TX 153613
|
||||
48183 Gregg County TX 126095
|
||||
48187 Guadalupe County TX 201111
|
||||
48201 Harris County TX 5045026
|
||||
48209 Hays County TX 304390
|
||||
48215 Hidalgo County TX 921549
|
||||
48231 Hunt County TX 123336
|
||||
48245 Jefferson County TX 254321
|
||||
48251 Johnson County TX 218048
|
||||
48257 Kaufman County TX 209235
|
||||
48265 Kerr County TX 54037
|
||||
48277 Lamar County TX 51503
|
||||
48303 Lubbock County TX 328906
|
||||
48309 McLennan County TX 272020
|
||||
48323 Maverick County TX 58823
|
||||
48329 Midland County TX 187855
|
||||
48339 Montgomery County TX 781194
|
||||
48347 Nacogdoches County TX 66035
|
||||
48349 Navarro County TX 57181
|
||||
48355 Nueces County TX 352992
|
||||
48367 Parker County TX 184767
|
||||
48381 Randall County TX 152351
|
||||
48397 Rockwall County TX 140738
|
||||
48423 Smith County TX 252549
|
||||
48439 Tarrant County TX 2248466
|
||||
48441 Taylor County TX 150077
|
||||
48451 Tom Green County TX 120602
|
||||
48453 Travis County TX 1389670
|
||||
48465 Val Verde County TX 47835
|
||||
48469 Victoria County TX 92656
|
||||
48471 Walker County TX 83842
|
||||
48479 Webb County TX 281224
|
||||
48485 Wichita County TX 129555
|
||||
48491 Williamson County TX 752827
|
||||
49005 Cache County UT 145000
|
||||
49011 Davis County UT 381227
|
||||
49021 Iron County UT 67141
|
||||
49035 Salt Lake County UT 1220916
|
||||
49045 Tooele County UT 87461
|
||||
49049 Utah County UT 759859
|
||||
49053 Washington County UT 213670
|
||||
49057 Weber County UT 278174
|
||||
50007 Chittenden County VT 169115
|
||||
51059 Fairfax County VA 1167873
|
||||
51107 Loudoun County VA 449749
|
||||
51121 Montgomery County VA 98434
|
||||
51510 Alexandria city VA 160662
|
||||
51540 Charlottesville city VA 44388
|
||||
51550 Chesapeake city VA 255332
|
||||
51590 Danville city VA 41647
|
||||
51600 Fairfax city VA 26772
|
||||
51630 Fredericksburg city VA 30393
|
||||
51650 Hampton city VA 137315
|
||||
51660 Harrisonburg city VA 50839
|
||||
51680 Lynchburg city VA 81347
|
||||
51683 Manassas city VA 44332
|
||||
51700 Newport News city VA 183230
|
||||
51710 Norfolk city VA 231013
|
||||
51730 Petersburg city VA 33734
|
||||
51740 Portsmouth city VA 96777
|
||||
51760 Richmond city VA 237257
|
||||
51770 Roanoke city VA 99111
|
||||
51775 Salem city VA 25816
|
||||
51790 Staunton city VA 26801
|
||||
51800 Suffolk city VA 104699
|
||||
51810 Virginia Beach city VA 453737
|
||||
51840 Winchester city VA 28272
|
||||
53005 Benton County WA 221722
|
||||
53007 Chelan County WA 81941
|
||||
53011 Clark County WA 532119
|
||||
53015 Cowlitz County WA 114885
|
||||
53021 Franklin County WA 102612
|
||||
53025 Grant County WA 105727
|
||||
53033 King County WA 2344939
|
||||
53035 Kitsap County WA 283374
|
||||
53053 Pierce County WA 946288
|
||||
53057 Skagit County WA 132975
|
||||
53061 Snohomish County WA 870656
|
||||
53063 Spokane County WA 558344
|
||||
53067 Thurston County WA 304261
|
||||
53071 Walla Walla County WA 62361
|
||||
53073 Whatcom County WA 236392
|
||||
53075 Whitman County WA 48512
|
||||
53077 Yakima County WA 259185
|
||||
54011 Cabell County WV 91183
|
||||
54039 Kanawha County WV 172381
|
||||
54061 Monongalia County WV 107991
|
||||
54069 Ohio County WV 40496
|
||||
54107 Wood County WV 82385
|
||||
55009 Brown County WI 275803
|
||||
55025 Dane County WI 590375
|
||||
55031 Douglas County WI 43990
|
||||
55035 Eau Claire County WI 109033
|
||||
55039 Fond du Lac County WI 104669
|
||||
55059 Kenosha County WI 168448
|
||||
55063 La Crosse County WI 121339
|
||||
55071 Manitowoc County WI 81710
|
||||
55073 Marathon County WI 139432
|
||||
55079 Milwaukee County WI 924216
|
||||
55087 Outagamie County WI 195894
|
||||
55089 Ozaukee County WI 94346
|
||||
55097 Portage County WI 71943
|
||||
55101 Racine County WI 198919
|
||||
55105 Rock County WI 166472
|
||||
55117 Sheboygan County WI 118047
|
||||
55131 Washington County WI 139238
|
||||
55133 Waukesha County WI 417210
|
||||
55139 Winnebago County WI 174218
|
||||
56001 Albany County WY 38558
|
||||
56005 Campbell County WY 48145
|
||||
56021 Laramie County WY 102938
|
||||
56025 Natrona County WY 80526
|
||||
|
@@ -0,0 +1 @@
|
||||
{ "format": "{code}", "rows": "postal-code.tsv", "key": "code", "parent": "locality", "weight": "addresses" }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
{ "format": "{name}", "rows": "region.tsv", "key": "abbr", "name": "name", "weight": "population" }
|
||||
@@ -0,0 +1,51 @@
|
||||
abbr code name population timezone
|
||||
AK 02 Alaska 737270 America/Anchorage
|
||||
AL 01 Alabama 5193088 America/Chicago
|
||||
AR 05 Arkansas 3114791 America/Chicago
|
||||
AZ 04 Arizona 7623818 America/Phoenix
|
||||
CA 06 California 39355309 America/Los_Angeles
|
||||
CO 08 Colorado 6012561 America/Denver
|
||||
CT 09 Connecticut 3688496 America/New_York
|
||||
DC 11 District of Columbia 693645 America/New_York
|
||||
DE 10 Delaware 1059952 America/New_York
|
||||
FL 12 Florida 23462518 America/New_York
|
||||
GA 13 Georgia 11302748 America/New_York
|
||||
IA 19 Iowa 3238387 America/Chicago
|
||||
ID 16 Idaho 2029733 America/Boise
|
||||
IL 17 Illinois 12719141 America/Chicago
|
||||
IN 18 Indiana 6973333 America/Indiana/Indianapolis
|
||||
KS 20 Kansas 2977220 America/Chicago
|
||||
KY 21 Kentucky 4606864 America/New_York
|
||||
LA 22 Louisiana 4618189 America/Chicago
|
||||
MA 25 Massachusetts 7154084 America/New_York
|
||||
MD 24 Maryland 6265347 America/New_York
|
||||
ME 23 Maine 1414874 America/New_York
|
||||
MI 26 Michigan 10127884 America/Detroit
|
||||
MN 27 Minnesota 5830405 America/Chicago
|
||||
MO 29 Missouri 6270541 America/Chicago
|
||||
MS 28 Mississippi 2954160 America/Chicago
|
||||
MT 30 Montana 1144694 America/Denver
|
||||
NC 37 North Carolina 11197968 America/New_York
|
||||
ND 38 North Dakota 799358 America/Chicago
|
||||
NE 31 Nebraska 2018006 America/Chicago
|
||||
NH 33 New Hampshire 1415342 America/New_York
|
||||
NJ 34 New Jersey 9548215 America/New_York
|
||||
NM 35 New Mexico 2125498 America/Denver
|
||||
NV 32 Nevada 3282188 America/Los_Angeles
|
||||
NY 36 New York 20002427 America/New_York
|
||||
OH 39 Ohio 11900510 America/New_York
|
||||
OK 40 Oklahoma 4123288 America/Chicago
|
||||
OR 41 Oregon 4273586 America/Los_Angeles
|
||||
PA 42 Pennsylvania 13059432 America/New_York
|
||||
RI 44 Rhode Island 1114521 America/New_York
|
||||
SC 45 South Carolina 5570274 America/New_York
|
||||
SD 46 South Dakota 935094 America/Chicago
|
||||
TN 47 Tennessee 7315076 America/Chicago
|
||||
TX 48 Texas 31709821 America/Chicago
|
||||
UT 49 Utah 3538904 America/Denver
|
||||
VA 51 Virginia 8880107 America/New_York
|
||||
VT 50 Vermont 644663 America/New_York
|
||||
WA 53 Washington 8001020 America/Los_Angeles
|
||||
WI 55 Wisconsin 5972787 America/Chicago
|
||||
WV 54 West Virginia 1766147 America/New_York
|
||||
WY 56 Wyoming 588753 America/Denver
|
||||
|
@@ -0,0 +1 @@
|
||||
{ "format": "{name}", "rows": "street.tsv", "parent": "locality", "weight": "addresses" }
|
||||
+15841
File diff suppressed because it is too large
Load Diff
+1
-10
@@ -1,10 +1 @@
|
||||
[
|
||||
{ "format": "{maker} {model}", "maker": ["BMW"], "model": ["3 Series", "5 Series", "X3", "X5"] },
|
||||
{ "format": "{maker} {model}", "maker": ["Ford"], "model": ["Fiesta", "Focus", "Mustang", "Explorer"] },
|
||||
{ "format": "{maker} {model}", "maker": ["Honda"], "model": ["Civic", "Accord", "CR-V", "Jazz"] },
|
||||
{ "format": "{maker} {model}", "maker": ["Mercedes-Benz"], "model": ["A-Class", "C-Class", "E-Class", "GLC"] },
|
||||
{ "format": "{maker} {model}", "maker": ["Tesla"], "model": ["Model 3", "Model S", "Model X", "Model Y"] },
|
||||
{ "format": "{maker} {model}", "maker": ["Toyota"], "model": ["Corolla", "Camry", "RAV4", "Yaris"] },
|
||||
{ "format": "{maker} {model}", "maker": ["Volkswagen"], "model": ["Golf", "Passat", "Polo", "Tiguan"] },
|
||||
{ "format": "{maker} {model}", "maker": ["Volvo"], "model": ["XC40", "XC60", "XC90", "V60"] }
|
||||
]
|
||||
{ "format": "{make} {model}", "rows": "car.tsv" }
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
make model
|
||||
BMW 3 Series
|
||||
BMW 5 Series
|
||||
BMW X3
|
||||
BMW X5
|
||||
Ford Explorer
|
||||
Ford Fiesta
|
||||
Ford Focus
|
||||
Ford Mustang
|
||||
Honda Accord
|
||||
Honda CR-V
|
||||
Honda Civic
|
||||
Honda Jazz
|
||||
Mercedes-Benz A-Class
|
||||
Mercedes-Benz C-Class
|
||||
Mercedes-Benz E-Class
|
||||
Mercedes-Benz GLC
|
||||
Tesla Model 3
|
||||
Tesla Model S
|
||||
Tesla Model X
|
||||
Tesla Model Y
|
||||
Toyota Camry
|
||||
Toyota Corolla
|
||||
Toyota RAV4
|
||||
Toyota Yaris
|
||||
Volkswagen Golf
|
||||
Volkswagen Passat
|
||||
Volkswagen Polo
|
||||
Volkswagen Tiguan
|
||||
Volvo V60
|
||||
Volvo XC40
|
||||
Volvo XC60
|
||||
Volvo XC90
|
||||
|
@@ -1,7 +1 @@
|
||||
[
|
||||
{
|
||||
"format": "{lat}, {lon}",
|
||||
"lat": { "format": "{float(-90,90,6)}" },
|
||||
"lon": { "format": "{float(-180,180,6)}" }
|
||||
}
|
||||
]
|
||||
{ "format": "{lat}, {lon}", "lat": "{float(-90,90,6)}", "lon": "{float(-180,180,6)}" }
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
[
|
||||
{ "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"] }
|
||||
]
|
||||
@@ -1,5 +1,18 @@
|
||||
[
|
||||
{ "format": "4{d}{luhn()}", "d": { "format": "0", "repeat": 14 }, "weight": 4 },
|
||||
{ "format": "5{m}{d}{luhn()}", "m": ["1", "2", "3", "4", "5"], "d": { "format": "0", "repeat": 13 }, "weight": 3 },
|
||||
{ "format": "3{m}{d}{luhn()}", "m": ["4", "7"], "d": { "format": "0", "repeat": 12 }, "weight": 1 }
|
||||
{
|
||||
"format": "4{d}{luhn()}",
|
||||
"d": { "format": "{digits(1)}", "repeat": 14 },
|
||||
"weight": 4
|
||||
},
|
||||
{
|
||||
"format": "5{m}{d}{luhn()}",
|
||||
"m": ["1", "2", "3", "4", "5"],
|
||||
"d": { "format": "{digits(1)}", "repeat": 13 },
|
||||
"weight": 3
|
||||
},
|
||||
{
|
||||
"format": "3{m}{d}{luhn()}",
|
||||
"m": ["4", "7"],
|
||||
"d": { "format": "{digits(1)}", "repeat": 12 }
|
||||
}
|
||||
]
|
||||
|
||||
+1
-18
@@ -1,18 +1 @@
|
||||
[
|
||||
{ "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"] }
|
||||
]
|
||||
{ "format": "{code}", "rows": "currency.tsv", "key": "code", "name": "name" }
|
||||
|
||||
@@ -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
|
||||
|
@@ -0,0 +1 @@
|
||||
"{date(2000-01-01,2029-12-31,'2006-01-02T15:04:05Z')}"
|
||||
@@ -1,6 +1 @@
|
||||
[
|
||||
"😀", "😂", "😍", "🤔", "😎", "😭", "😡", "👍", "👎", "🙏",
|
||||
"👏", "🙌", "💪", "🔥", "✨", "🎉", "❤️", "💔", "💯", "👀",
|
||||
"🚀", "⭐", "🌈", "☀️", "🌙", "⚡", "❄️", "🍕", "🍔", "🍺",
|
||||
"☕", "🎂", "🐶", "🐱", "🦊", "🐢", "🦄", "🌸", "🌍", "💡"
|
||||
]
|
||||
["😀", "😂", "😍", "🤔", "😎", "😭", "😡", "👍", "👎", "🙏", "👏", "🙌", "💪", "🔥", "✨", "🎉", "❤️", "💔", "💯", "👀", "🚀", "⭐", "🌈", "☀️", "🌙", "⚡", "❄️", "🍕", "🍔", "🍺", "☕", "🎂", "🐶", "🐱", "🦊", "🐢", "🦄", "🌸", "🌍", "💡"]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{ "format": "{method}", "rows": "httpmethod.tsv", "key": "method" }
|
||||
@@ -0,0 +1,10 @@
|
||||
idempotent method safe
|
||||
false CONNECT false
|
||||
true DELETE false
|
||||
true GET true
|
||||
true HEAD true
|
||||
true OPTIONS true
|
||||
false PATCH false
|
||||
false POST false
|
||||
true PUT false
|
||||
true TRACE true
|
||||
|
@@ -1,18 +1 @@
|
||||
[
|
||||
{ "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"] }
|
||||
]
|
||||
{ "format": "{code} {reason}", "rows": "httpstatus.tsv", "key": "code", "name": "reason" }
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
code reason
|
||||
100 Continue
|
||||
101 Switching Protocols
|
||||
102 Processing
|
||||
103 Early Hints
|
||||
200 OK
|
||||
201 Created
|
||||
202 Accepted
|
||||
203 Non-Authoritative Information
|
||||
204 No Content
|
||||
205 Reset Content
|
||||
206 Partial Content
|
||||
207 Multi-Status
|
||||
208 Already Reported
|
||||
226 IM Used
|
||||
300 Multiple Choices
|
||||
301 Moved Permanently
|
||||
302 Found
|
||||
303 See Other
|
||||
304 Not Modified
|
||||
305 Use Proxy
|
||||
307 Temporary Redirect
|
||||
308 Permanent Redirect
|
||||
400 Bad Request
|
||||
401 Unauthorized
|
||||
402 Payment Required
|
||||
403 Forbidden
|
||||
404 Not Found
|
||||
405 Method Not Allowed
|
||||
406 Not Acceptable
|
||||
407 Proxy Authentication Required
|
||||
408 Request Timeout
|
||||
409 Conflict
|
||||
410 Gone
|
||||
411 Length Required
|
||||
412 Precondition Failed
|
||||
413 Content Too Large
|
||||
414 URI Too Long
|
||||
415 Unsupported Media Type
|
||||
416 Range Not Satisfiable
|
||||
417 Expectation Failed
|
||||
421 Misdirected Request
|
||||
422 Unprocessable Content
|
||||
423 Locked
|
||||
424 Failed Dependency
|
||||
425 Too Early
|
||||
426 Upgrade Required
|
||||
428 Precondition Required
|
||||
429 Too Many Requests
|
||||
431 Request Header Fields Too Large
|
||||
451 Unavailable For Legal Reasons
|
||||
500 Internal Server Error
|
||||
501 Not Implemented
|
||||
502 Bad Gateway
|
||||
503 Service Unavailable
|
||||
504 Gateway Timeout
|
||||
505 HTTP Version Not Supported
|
||||
506 Variant Also Negotiates
|
||||
507 Insufficient Storage
|
||||
508 Loop Detected
|
||||
511 Network Authentication Required
|
||||
|
+1
-18
@@ -1,18 +1 @@
|
||||
[
|
||||
{ "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"] }
|
||||
]
|
||||
{ "format": "{name}", "rows": "language.tsv", "key": "code", "name": "name" }
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
code code3 name
|
||||
aa aar Afar
|
||||
ab abk Abkhazian
|
||||
ae ave Avestan
|
||||
af afr Afrikaans
|
||||
ak aka Akan
|
||||
am amh Amharic
|
||||
an arg Aragonese
|
||||
ar ara Arabic
|
||||
as asm Assamese
|
||||
av ava Avaric
|
||||
ay aym Aymara
|
||||
az aze Azerbaijani
|
||||
ba bak Bashkir
|
||||
be bel Belarusian
|
||||
bg bul Bulgarian
|
||||
bi bis Bislama
|
||||
bm bam Bambara
|
||||
bn ben Bengali
|
||||
bo bod Tibetan
|
||||
br bre Breton
|
||||
bs bos Bosnian
|
||||
ca cat Catalan
|
||||
ce che Chechen
|
||||
ch cha Chamorro
|
||||
co cos Corsican
|
||||
cr cre Cree
|
||||
cs ces Czech
|
||||
cu chu Church Slavic
|
||||
cv chv Chuvash
|
||||
cy cym Welsh
|
||||
da dan Danish
|
||||
de deu German
|
||||
dv div Divehi
|
||||
dz dzo Dzongkha
|
||||
ee ewe Ewe
|
||||
el ell Modern Greek
|
||||
en eng English
|
||||
eo epo Esperanto
|
||||
es spa Spanish
|
||||
et est Estonian
|
||||
eu eus Basque
|
||||
fa fas Persian
|
||||
ff ful Fulah
|
||||
fi fin Finnish
|
||||
fj fij Fijian
|
||||
fo fao Faroese
|
||||
fr fra French
|
||||
fy fry Western Frisian
|
||||
ga gle Irish
|
||||
gd gla Gaelic
|
||||
gl glg Galician
|
||||
gn grn Guarani
|
||||
gu guj Gujarati
|
||||
gv glv Manx
|
||||
ha hau Hausa
|
||||
he heb Hebrew
|
||||
hi hin Hindi
|
||||
ho hmo Hiri Motu
|
||||
hr hrv Croatian
|
||||
ht hat Haitian
|
||||
hu hun Hungarian
|
||||
hy hye Armenian
|
||||
hz her Herero
|
||||
ia ina Interlingua
|
||||
id ind Indonesian
|
||||
ie ile Interlingue
|
||||
ig ibo Igbo
|
||||
ii iii Sichuan Yi
|
||||
ik ipk Inupiaq
|
||||
io ido Ido
|
||||
is isl Icelandic
|
||||
it ita Italian
|
||||
iu iku Inuktitut
|
||||
ja jpn Japanese
|
||||
jv jav Javanese
|
||||
ka kat Georgian
|
||||
kg kon Kongo
|
||||
ki kik Kikuyu
|
||||
kj kua Kuanyama
|
||||
kk kaz Kazakh
|
||||
kl kal Kalaallisut
|
||||
km khm Central Khmer
|
||||
kn kan Kannada
|
||||
ko kor Korean
|
||||
kr kau Kanuri
|
||||
ks kas Kashmiri
|
||||
ku kur Kurdish
|
||||
kv kom Komi
|
||||
kw cor Cornish
|
||||
ky kir Kirghiz
|
||||
la lat Latin
|
||||
lb ltz Luxembourgish
|
||||
lg lug Ganda
|
||||
li lim Limburgan
|
||||
ln lin Lingala
|
||||
lo lao Lao
|
||||
lt lit Lithuanian
|
||||
lu lub Luba-Katanga
|
||||
lv lav Latvian
|
||||
mg mlg Malagasy
|
||||
mh mah Marshallese
|
||||
mi mri Maori
|
||||
mk mkd Macedonian
|
||||
ml mal Malayalam
|
||||
mn mon Mongolian
|
||||
mr mar Marathi
|
||||
ms msa Malay
|
||||
mt mlt Maltese
|
||||
my mya Burmese
|
||||
na nau Nauru
|
||||
nb nob Norwegian Bokmål
|
||||
nd nde North Ndebele
|
||||
ne nep Nepali
|
||||
ng ndo Ndonga
|
||||
nl nld Dutch
|
||||
nn nno Norwegian Nynorsk
|
||||
no nor Norwegian
|
||||
nr nbl South Ndebele
|
||||
nv nav Navajo
|
||||
ny nya Chichewa
|
||||
oc oci Occitan
|
||||
oj oji Ojibwa
|
||||
om orm Oromo
|
||||
or ori Oriya
|
||||
os oss Ossetian
|
||||
pa pan Panjabi
|
||||
pi pli Pali
|
||||
pl pol Polish
|
||||
ps pus Pushto
|
||||
pt por Portuguese
|
||||
qu que Quechua
|
||||
rm roh Romansh
|
||||
rn run Rundi
|
||||
ro ron Romanian
|
||||
ru rus Russian
|
||||
rw kin Kinyarwanda
|
||||
sa san Sanskrit
|
||||
sc srd Sardinian
|
||||
sd snd Sindhi
|
||||
se sme Northern Sami
|
||||
sg sag Sango
|
||||
si sin Sinhala
|
||||
sk slk Slovak
|
||||
sl slv Slovenian
|
||||
sm smo Samoan
|
||||
sn sna Shona
|
||||
so som Somali
|
||||
sq sqi Albanian
|
||||
sr srp Serbian
|
||||
ss ssw Swati
|
||||
st sot Sotho, Southern
|
||||
su sun Sundanese
|
||||
sv swe Swedish
|
||||
sw swa Swahili
|
||||
ta tam Tamil
|
||||
te tel Telugu
|
||||
tg tgk Tajik
|
||||
th tha Thai
|
||||
ti tir Tigrinya
|
||||
tk tuk Turkmen
|
||||
tl tgl Tagalog
|
||||
tn tsn Tswana
|
||||
to ton Tonga
|
||||
tr tur Turkish
|
||||
ts tso Tsonga
|
||||
tt tat Tatar
|
||||
tw twi Twi
|
||||
ty tah Tahitian
|
||||
ug uig Uighur
|
||||
uk ukr Ukrainian
|
||||
ur urd Urdu
|
||||
uz uzb Uzbek
|
||||
ve ven Venda
|
||||
vi vie Vietnamese
|
||||
vo vol Volapük
|
||||
wa wln Walloon
|
||||
wo wol Wolof
|
||||
xh xho Xhosa
|
||||
yi yid Yiddish
|
||||
yo yor Yoruba
|
||||
za zha Zhuang
|
||||
zh zho Chinese
|
||||
zu zul Zulu
|
||||
|
@@ -0,0 +1 @@
|
||||
{ "format": "{keyword}", "rows": "loglevel.tsv", "key": "code", "name": "keyword" }
|
||||
@@ -0,0 +1,9 @@
|
||||
code keyword severity
|
||||
0 emerg Emergency
|
||||
1 alert Alert
|
||||
2 crit Critical
|
||||
3 err Error
|
||||
4 warning Warning
|
||||
5 notice Notice
|
||||
6 info Informational
|
||||
7 debug Debug
|
||||
|
+1
-3
@@ -1,3 +1 @@
|
||||
[
|
||||
{ "format": "{hex(2)}:{hex(2)}:{hex(2)}:{hex(2)}:{hex(2)}:{hex(2)}" }
|
||||
]
|
||||
"{hex(2)}:{hex(2)}:{hex(2)}:{hex(2)}:{hex(2)}:{hex(2)}"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user