manufacturing.lot-number
Deterministic lot or batch code YYDDD-LL-SSS from the production date (Julian day), line and sequence.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 16 tests, run in TypeScript, Python and Rust.
What it does
Builds a lot (batch) code from the production date, the line and a sequence number, in one fixed format:
YYDDD-LL-SSS
For example
lot_number(2026-01-01, 1, 1)→ 26001-01-001 first day of the year, first line, first lotlot_number(2026-03-01, 3, 12)→ 26060-03-012 1 March 2026 is day 60 in a common yearlot_number(2024-03-01, 3, 12)→ 24061-03-012 1 March 2024 is day 61: the leap day moves it
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
def lot_number(production_date: str, line: int, sequence: int) -> str
| production_date | date | the day the lot was made, not today |
| line | int | production line, 1 to 99 |
| sequence | int | the lot's number on that line that day, 1 to 999 |
| returns | string | e.g. "26060-03-012": 2026, day 60 (1 March), line 3, lot 12 |
Your code names it in one line, in the file that uses it
from fune.manufacturing.lot_number import lot_number # manufacturing.lot-number@^1
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from .dates_add_days import days_from_civil, parse_iso_date ← from dates.add-days ^1.0.0 · built alongside by fune
def lot_number(production_date: str, line: int, sequence: int) -> str:
"""Lot code YYDDD-LL-SSS: two-digit year, day of the year, line, sequence.
The day of the year counts from the epoch-day kernel in dates.add-days, so
leap years move every date after 28 February without a month table.
"""
if isinstance(line, bool) or not isinstance(line, int) or line < 1 or line > 99:
raise ValueError("line must be a whole number from 1 to 99, received %r" % (line,))
if isinstance(sequence, bool) or not isinstance(sequence, int) or sequence < 1 or sequence > 999:
raise ValueError("sequence must be a whole number from 1 to 999, received %r" % (sequence,))
date = parse_iso_date(production_date)
day_of_year = days_from_civil(date.year, date.month, date.day) - days_from_civil(date.year, 1, 1) + 1
return "%02d%03d-%02d-%03d" % (date.year % 100, day_of_year, line, sequence)Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Python package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add manufacturing.lot-number
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./manufacturing.lot-number-1.0.0-python.fune, or fetch it from a terminal with fune pull manufacturing.lot-number@1.0.0:python.
The whole function, every language, is one file too: manufacturing.lot-number-1.0.0.fune, 9,136 bytes, sha256 677f36c5f07cbd03dd5a9615041db4a2d68ec3e1c5752e068bb8cee7b9982c12. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
# fune: before manufacturing.lot-number
after — your function gets the result and the arguments, and returns the final result.
# fune: after manufacturing.lot-number
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
# fune: replace dates.add-days in manufacturing.lot-number
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show manufacturing.lot-number --steps.
# fune: step manufacturing.lot-number after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| first day of the year, first line, first lot | 2026-01-01, 1, 1 | → | 26001-01-001 |
| 1 March 2026 is day 60 in a common year | 2026-03-01, 3, 12 | → | 26060-03-012 |
| 1 March 2024 is day 61: the leap day moves it | 2024-03-01, 3, 12 | → | 24061-03-012 |
| 29 February 2024 is day 60 | 2024-02-29, 7, 5 | → | 24060-07-005 |
| 31 December in a common year is day 365 | 2026-12-31, 10, 100 | → | 26365-10-100 |
| 31 December in a leap year is day 366 | 2024-12-31, 2, 1 | → | 24366-02-001 |
| 2000 was a leap year (divisible by 400): 31 December is day 366 and the year is 00 | 2000-12-31, 1, 1 | → | 00366-01-001 |
| 1900 was not a leap year (divisible by 100): 1 March is day 60 | 1900-03-01, 1, 1 | → | 00060-01-001 |
| the largest line and sequence fill the fields | 2026-09-23, 99, 999 | → | 26266-99-999 |
| line 0 is an error | 2026-09-23, 0, 1 | → | error: line must be a whole number from 1 to 99 |
Show the other 6 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| line 100 does not fit two digits | 2026-09-23, 100, 1 | → | error: line must be a whole number from 1 to 99 |
| sequence 1000 does not fit three digits | 2026-09-23, 1, 1,000 | → | error: sequence must be a whole number from 1 to 999 |
| sequence 0 is an error | 2026-09-23, 1, 0 | → | error: sequence must be a whole number from 1 to 999 |
| a fractional line is an error | 2026-09-23, 1.5, 1 | → | error: line must be a whole number from 1 to 99 |
| a date that does not exist is an error | 2026-02-30, 1, 1 | → | error: is not a real calendar date |
| a date that is not ISO is an error | 23/09/2026, 1, 1 | → | error: is not an ISO date |
More from the author
- `YY` - the last two digits of the year - `DDD` - the day of the year, 001 to 365 (366 in a leap year): the "Julian date" printed on food, drink and pharmaceutical packs - `LL` - the production line, 01 to 99 - `SSS` - the lot's sequence number on that line that day, 001 to 999
So lot 12 on line 3 on 1 March 2026 is `26060-03-012`, and the same lot a year earlier, in a leap year, would be `24061-03-012` on 1 March 2024: 29 February moves every later day of the year by one, which is the mistake a hand-built month table makes.
**Deterministic.** The code depends only on its three arguments. The function never reads the clock and never counts: the caller owns the sequence (from the database row that records the lot) and passes the date the lot was made. The same arguments always give the same code, so a code can be regenerated for a recall.
**Fixed width.** Every code is 12 characters, so codes sort by date within a century, then line, then sequence, and fit a fixed field on a label.
**Two-digit years repeat every century.** `26060` is 1 March 2026 and 1 March 2126. Lots are not kept that long, but the code is not a date to parse back without context.
"Julian date" here is the day-of-year convention used on packaging, not the astronomical Julian Day Number. Day numbers come from `dates.add-days`, so leap years follow the full Gregorian rule (2000 was a leap year, 1900 was not).
A line outside 1-99, a sequence outside 1-999, or a date that does not exist (2026-02-30) is an error rather than a code that would not fit the format.
Files
| Path | Bytes |
|---|---|
| README.md | 1,711 |
| impl/python.py | 982 |
| impl/rust.rs | 1,372 |
| impl/typescript.ts | 1,065 |
| vectors.json | 2,039 |