This is the full developer documentation for Numerik # Numerik > Validate and parse Polish identification numbers with rich value objects and detailed error reasons. ## Overview [Section titled “Overview”](#overview) Numerik is a zero-dependency PHP 8.3+ library. Validation tells you *why* input failed, not just *that* it failed. ### Personal [Section titled “Personal”](#personal) PESEL Validates and parses Poland’s universal citizen identifier. Extracts birth date, gender, century, age, and ordinal number directly from the number. ID Card Validates Polish national identity card numbers (dowód osobisty). Three-letter series plus six digits, validated with the ICAO 9303 weighted checksum. Passport Validates Polish passport numbers. Two-letter series plus seven digits, validated with the same ICAO 9303 algorithm used in travel documents globally. ### Tax & Business [Section titled “Tax & Business”](#tax--business) NIP Validates tax identification numbers for individuals and legal entities. Returns the issuing tax office code and formatted display form. VAT-EU Validates intra-EU VAT numbers for Polish taxpayers (`PL` + NIP). Strips the country prefix and applies the NIP MOD-11 algorithm. Returns the NIP and formatted display form. REGON Handles both 9-digit (individual) and 14-digit (legal entity with local unit) REGON numbers, including multi-stage checksum validation. KRS Validates court register numbers with automatic zero-padding normalisation. Documents the offline vs. online verification limitation. ### Banking [Section titled “Banking”](#banking) NRB Validates Polish bank account numbers using the MOD-97 checksum. Accepts raw, spaced, and IBAN (`PL`-prefixed) formats. Returns sort code, bank code, and account number. IBAN Validates Polish IBANs (`PL` + NRB). Requires the country prefix and applies MOD-97. Returns NRB, sort code, bank code, and account number. ### Integrations [Section titled “Integrations”](#integrations) Laravel Drop-in Laravel validation rules for all identifiers via `slashlab/numerik-laravel`. Supports class-based and string-based syntax, with strict-mode control for PESEL. ## At a glance [Section titled “At a glance”](#at-a-glance) ```php use SlashLab\Numerik\Numerik; // Simple boolean check Numerik::pesel()->isValid('92060512186'); // true Numerik::nip()->isValid('5260250274'); // true // Rich result with failure details $result = Numerik::pesel()->validate('92060512185'); // wrong checksum digit $result->isFailed(); // true $result->getFirstFailure()->reason; // ValidationFailureReason::InvalidChecksum // Parse to a value object $pesel = Numerik::pesel()->parse('92060512186'); $pesel->getBirthDate()->format('Y-m-d'); // '1992-06-05' $pesel->getGender(); // Gender::Female $pesel->getAge(); // calculated from today $pesel->isAdult(); // true ``` ## Key properties [Section titled “Key properties”](#key-properties) * **Zero production dependencies** — `require` only `php: ^8.3`. * **Immutable value objects** — all value objects are `readonly` classes. * **Strict types everywhere** — `declare(strict_types=1)` in every file. * **Rich failure reasons** — `ValidationFailureReason` enum covers format, checksum, encoded-date, and semantic failures. * **No regular expressions** for validation logic — all checks are digit-array operations safe from ReDoS. * **Input length guard** — inputs over 32 characters are rejected immediately. # Contributing > How to set up Numerik locally, run checks, and submit a pull request. ## Development setup [Section titled “Development setup”](#development-setup) 1. **Fork and clone** ```bash git clone https://github.com/sqrcz/numerik.git cd numerik ``` 2. **Install PHP dependencies** ```bash composer install ``` 3. **Run the test suite** ```bash composer test ``` ## Local checks [Section titled “Local checks”](#local-checks) | Command | What it runs | | --------------------- | ------------------------------------------- | | `composer test` | PHPUnit test suite | | `composer test:cover` | PHPUnit with HTML + Clover coverage reports | | `composer stan` | PHPStan at level 10 | | `composer cs-fix` | PHP CS Fixer — auto-fix | | `composer cs-check` | PHP CS Fixer — check only (used in CI) | | `composer mutation` | Infection mutation testing | | `composer check` | `cs-check` + `stan` + `test` in sequence | Run `composer check` before opening a PR to catch all issues locally. ## Adding a new identifier [Section titled “Adding a new identifier”](#adding-a-new-identifier) Adding a new identifier (e.g. Dowód Osobisty, IBAN PL) requires touching several files. Work through this checklist: * [ ] Create `src/ValueObjects/NewIdentifier.php` (readonly class implementing `IdentifierInterface`) * [ ] Create `src/Identifiers/NewIdentifierIdentifier.php` (implements both `ValidatorInterface` and `ParserInterface`; accepts `bool $strict = true`) * [ ] Add new `ValidationFailureReason` cases if the identifier introduces new failure types * [ ] Add a factory method to `src/Numerik.php` (`Numerik::newIdentifier(bool $strict = true)`) * [ ] Add `tests/Fixtures/new_identifier_valid.txt` and `new_identifier_invalid.txt` * [ ] Add unit tests for the validator, parser, and value object * [ ] Add an identifier page under `docs/src/content/docs/identifiers/` * [ ] Update `CHANGELOG.md` ## Commit style [Section titled “Commit style”](#commit-style) Use [Conventional Commits](https://www.conventionalcommits.org): ```plaintext feat: add Dowód Osobisty validation fix: correct PESEL checksum for 2100s birth dates docs: update REGON algorithm description test: add NIP fixtures for invalid tax office codes chore: update PHPStan to 2.x ``` Types: `feat`, `fix`, `docs`, `test`, `refactor`, `chore`, `ci`. ## Pull request process [Section titled “Pull request process”](#pull-request-process) 1. Create a branch from `main` (e.g. `feature/dowod-osobisty`). 2. Make your changes. 3. Ensure `composer check` passes. 4. Open a PR against `main` and fill in the template. Note Even as a solo contributor PRs go through the branch protection rules on `main`. This is intentional — it ensures CI passes before merging. ## Reporting a bug [Section titled “Reporting a bug”](#reporting-a-bug) Use the [Bug Report](https://github.com/sqrcz/numerik/issues/new?template=bug_report.yml) template on GitHub. ## Security vulnerabilities [Section titled “Security vulnerabilities”](#security-vulnerabilities) **Do not open a public issue.** Report via [GitHub’s private advisory system](https://github.com/sqrcz/numerik/security/advisories/new). See [SECURITY.md](https://github.com/sqrcz/numerik/blob/main/SECURITY.md). # Installation & Quick Start > Install Numerik via Composer and validate your first Polish identifier in under a minute. ## Requirements [Section titled “Requirements”](#requirements) * PHP **8.3** or higher * Composer ## Installation [Section titled “Installation”](#installation) 1. **Require the package** ```bash composer require slashlab/numerik ``` 2. **No configuration needed** The package is autoloaded via PSR-4. There is no service provider, config file, or bootstrap step for the core library. 3. **Use it** ```php use SlashLab\Numerik\Numerik; Numerik::pesel()->isValid('92060512186'); // true ``` ## Quick start [Section titled “Quick start”](#quick-start) ### Boolean validation [Section titled “Boolean validation”](#boolean-validation) ```php use SlashLab\Numerik\Numerik; Numerik::pesel()->isValid('92060512186'); // true Numerik::idCard()->isValid('ABC123454'); // true Numerik::passport()->isValid('AB1234564'); // true Numerik::nip()->isValid('5260250274'); // true Numerik::vatEu()->isValid('PL5260250274'); // true Numerik::regon()->isValid('691761365'); // true Numerik::krs()->isValid('0000127206'); // true Numerik::nrb()->isValid('61102010260000000000000000'); // true Numerik::iban()->isValid('PL61102010260000000000000000'); // true ``` ### Rich validation result [Section titled “Rich validation result”](#rich-validation-result) `validate()` never throws. It returns a `ValidationResult` that carries the list of failures when validation does not pass. ```php use SlashLab\Numerik\Numerik; $result = Numerik::pesel()->validate('92060512186'); $result->isValid; // true $result->failures; // [] $result = Numerik::pesel()->validate('00000000000'); $result->isFailed(); // true $result->getFirstFailure()->reason->value; // 'all_zeros' $result->getFirstFailure()->message; // human-readable message ``` See [Validation Results](/guide/validation-results/) for the full API. ### Parsing to a value object [Section titled “Parsing to a value object”](#parsing-to-a-value-object) On success, `parse()` returns a value object. On failure, it throws a `ValidationException`. ```php use SlashLab\Numerik\Numerik; $pesel = Numerik::pesel()->parse('92060512186'); $pesel->getBirthDate()->format('Y-m-d'); // '1992-06-05' $pesel->getGender(); // Gender::Female $pesel->getAge(); // int — calculated from today $pesel->isAdult(); // true ``` Use `tryParse()` when you prefer `null` on failure instead of an exception: ```php $pesel = Numerik::pesel()->tryParse('bad-input'); // null ``` ### Choosing the right method [Section titled “Choosing the right method”](#choosing-the-right-method) | Method | Returns | Throws | Use when | | ------------ | ---------------------- | --------------------- | ------------------------------------------------------- | | `isValid()` | `bool` | never | Simple existence check | | `validate()` | `ValidationResult` | never | You need the failure reason | | `parse()` | value object | `ValidationException` | You need extracted data and want exceptions | | `tryParse()` | value object or `null` | never | You need extracted data but prefer null over exceptions | ## Next steps [Section titled “Next steps”](#next-steps) * Browse the [PESEL](/identifiers/pesel/), [ID Card](/identifiers/id-card/), [Passport](/identifiers/passport/), [NIP](/identifiers/nip/), [VAT-EU](/identifiers/vat-eu/), [REGON](/identifiers/regon/), [KRS](/identifiers/krs/), [NRB](/identifiers/nrb/), or [IBAN](/identifiers/iban/) reference for full method listings. * Read [Error Handling](/guide/error-handling/) to understand the exception hierarchy. * Read [Validation Results](/guide/validation-results/) for the complete `ValidationResult` API. * Using Laravel? See the [Laravel Integration](/integrations/laravel/) guide for drop-in validation rules. # Algorithms > Reference for the checksum algorithms used by Numerik to validate PESEL, NIP, REGON, NRB, ID Card, and Passport numbers. This page documents the checksum and structural validation algorithms implemented by Numerik. All validation is digit-array arithmetic — no regular expressions are used. ## PESEL [Section titled “PESEL”](#pesel) **Weights:** `1, 3, 7, 9, 1, 3, 7, 9, 1, 3` ### Steps [Section titled “Steps”](#steps) 1. Strip whitespace. Reject inputs longer than 32 characters. Assert exactly 11 digits; fail with `InvalidLength` otherwise. 2. Reject non-digit characters; fail with `InvalidCharacters`. 3. Decode the birth date using the century encoding table below. Fail with `InvalidMonth` if the stored month does not fall in any known range. Fail with `InvalidDate` if the decoded date is not a real calendar date. In strict mode, fail with `FutureDate` if the birth date is in the future. 4. Compute the checksum: multiply each of the first 10 digits by its corresponding weight and sum the results. Take `sum mod 10`, subtract from `10`, take `mod 10` again. The result must equal digit 11; fail with `InvalidChecksum` otherwise. 5. In strict mode, reject inputs where all 11 digits are identical; fail with `AllSameDigit`. ### Century encoding [Section titled “Century encoding”](#century-encoding) The PESEL month digits encode both the birth month and the birth century by adding an offset: | Stored month range | Real month | Birth century | | ------------------ | ---------- | ------------- | | 01 – 12 | 01 – 12 | 1900 – 1999 | | 21 – 32 | 01 – 12 | 2000 – 2099 | | 41 – 52 | 01 – 12 | 2100 – 2199 | | 61 – 72 | 01 – 12 | 2200 – 2299 | | 81 – 92 | 01 – 12 | 1800 – 1899 | ### Worked example [Section titled “Worked example”](#worked-example) Input: `9 2 0 6 0 5 1 2 1 8 6` | Position | Digit | Weight | Product | | -------- | ----- | ------ | ------- | | 1 | 9 | 1 | 9 | | 2 | 2 | 3 | 6 | | 3 | 0 | 7 | 0 | | 4 | 6 | 9 | 54 | | 5 | 0 | 1 | 0 | | 6 | 5 | 3 | 15 | | 7 | 1 | 7 | 7 | | 8 | 2 | 9 | 18 | | 9 | 1 | 1 | 1 | | 10 | 8 | 3 | 24 | Sum = 9 + 6 + 0 + 54 + 0 + 15 + 7 + 18 + 1 + 24 = **134** `134 mod 10 = 4` → `10 - 4 = 6` → `6 mod 10 = 6` — digit 11 is `6` ✓ Note The PESEL `92060512186` is used as an example throughout the docs. Its encoded date is `1992-06-05`, gender is Female (digit 10 is `8`, an even digit), ordinal is `1218` (digits 7–10). *** ## ICAO 9303 [Section titled “ICAO 9303”](#icao-9303) Used by both [ID Card](/identifiers/id-card/) and [Passport](/identifiers/passport/) — the same international standard applied to travel document numbers. **Weights:** `7, 3, 1` repeating over positions 1–8 (8 input characters). Position 9 is the check digit. **Character values:** * Digits `0`–`9` → face value (`0`–`9`) * Letters `A`–`Z` → `10`–`35` (`A`=10, `B`=11, … `Z`=35) ### Steps [Section titled “Steps”](#steps-1) 1. Strip spaces and hyphens. Convert to uppercase. Reject inputs over 32 characters. 2. Assert exactly 9 characters; fail with `InvalidLength`. 3. Validate character types per identifier (see individual pages). 4. Multiply each of the first 8 characters by its weight and sum the products. 5. Compute `sum mod 10`. The result must equal the 9th character; fail with `InvalidChecksum`. ### Calculation example [Section titled “Calculation example”](#calculation-example) Number: `A B C 1 2 3 4 5 4` (ID card) | Position | Char | Value | Weight | Product | | -------- | ---- | ----- | ------ | ------- | | 1 | A | 10 | 7 | 70 | | 2 | B | 11 | 3 | 33 | | 3 | C | 12 | 1 | 12 | | 4 | 1 | 1 | 7 | 7 | | 5 | 2 | 2 | 3 | 6 | | 6 | 3 | 3 | 1 | 3 | | 7 | 4 | 4 | 7 | 28 | | 8 | 5 | 5 | 3 | 15 | Sum = 70 + 33 + 12 + 7 + 6 + 3 + 28 + 15 = **174** `174 mod 10 = 4` — position 9 is `4` ✓ *** ## NIP [Section titled “NIP”](#nip) **Weights:** `6, 5, 7, 2, 3, 4, 5, 6, 7` ### Steps [Section titled “Steps”](#steps-2) 1. Strip hyphens and spaces. Reject inputs longer than 32 characters. Assert exactly 10 digits; fail with `InvalidLength`. 2. Reject non-digit characters; fail with `InvalidCharacters`. 3. Assert the first 3 digits are not `000`; fail with `InvalidFormat`. 4. Multiply each of the first 9 digits by its weight. Sum the products. Take `sum mod 11`. The result must equal digit 10; since a single digit can only be 0–9, a modulo result of `10` can never match and always fails with `InvalidChecksum`. ### Worked example [Section titled “Worked example”](#worked-example-1) Input: `5 2 6 0 2 5 0 2 7 4` | Position | Digit | Weight | Product | | -------- | ----- | ------ | ------- | | 1 | 5 | 6 | 30 | | 2 | 2 | 5 | 10 | | 3 | 6 | 7 | 42 | | 4 | 0 | 2 | 0 | | 5 | 2 | 3 | 6 | | 6 | 5 | 4 | 20 | | 7 | 0 | 5 | 0 | | 8 | 2 | 6 | 12 | | 9 | 7 | 7 | 49 | Sum = 30 + 10 + 42 + 0 + 6 + 20 + 0 + 12 + 49 = **169** `169 mod 11 = 4` — digit 10 is `4` ✓ *** ## VAT-EU [Section titled “VAT-EU”](#vat-eu) VAT-EU validation is a thin wrapper over NIP: 1. Strip spaces and hyphens. Reject inputs over 32 characters. 2. Assert the first two characters are `PL` (case-insensitive); fail with `InvalidFormat`. 3. Strip the prefix. Assert exactly 10 digits remain. 4. Apply the full [NIP](#nip) algorithm to the extracted digits. *** ## REGON [Section titled “REGON”](#regon) REGON uses a two-stage algorithm. The 14-digit form first validates the base 9 digits before validating the full 14. ### 9-digit REGON [Section titled “9-digit REGON”](#9-digit-regon) **Weights:** `8, 9, 2, 3, 4, 5, 6, 7` 1. Strip whitespace. Assert exactly 9 digits. 2. Multiply the first 8 digits by the 9-digit weights. Sum. Take `sum mod 11`. If the result is `10`, the checksum digit must be `0`. Otherwise the result must equal digit 9. ### 14-digit REGON [Section titled “14-digit REGON”](#14-digit-regon) **Weights:** `2, 4, 8, 5, 0, 9, 7, 3, 6, 1, 2, 4, 8` 1. Strip whitespace. Assert exactly 14 digits. 2. Validate digits 1–9 as a standalone 9-digit REGON (see above). 3. Multiply the first 13 digits by the 14-digit weights. Sum. Take `sum mod 11`. If the result is `10`, digit 14 must be `0`. Otherwise the result must equal digit 14. *** ## KRS [Section titled “KRS”](#krs) KRS has **no public checksum algorithm**. It is a sequential registry number assigned by the court register. Numerik validates format and range only: 1. Strip whitespace. Reject inputs longer than 32 characters. Assert ≤ 10 digits. 2. Reject non-digit characters. 3. Assert numeric value is greater than `0`; fail with `AllZeros`. 4. In strict mode, zero-pad to 10 digits and reject inputs where all digits are identical; fail with `AllSameDigit`. For authoritative verification that a KRS number is actually assigned, query the official [KRS registry API](https://prs.ms.gov.pl/krs/openApi). *** ## NRB [Section titled “NRB”](#nrb) NRB uses the standard IBAN MOD-97 algorithm: 1. Strip spaces and hyphens. Strip the optional `PL` country prefix. Reject inputs over 40 characters. 2. Assert exactly 26 digits remain. 3. Assert all characters are digits. 4. Rearrange as: digits 3–26 (the BBAN) + `2521` (numeric encoding of `PL`) + digits 1–2 (check digits). 5. Compute the resulting number modulo 97. The result must equal `1`. *** ## IBAN [Section titled “IBAN”](#iban) IBAN validation is a thin wrapper over NRB: 1. Strip spaces and hyphens. Reject inputs over 40 characters. 2. Assert the first two characters are `PL` (case-insensitive); fail with `InvalidFormat`. 3. Strip the prefix. Assert exactly 26 digits remain. 4. Apply the full [NRB](#nrb) MOD-97 algorithm to the extracted digits. # Error Handling > How Numerik uses exceptions. The ValidationException hierarchy and when each exception is thrown. * `isValid()` and `validate()` never throw — they always return a value. * `parse()` throws a `ValidationException` subclass on invalid input. * `tryParse()` never throws — it returns `null` on failure. All three methods live on the same identifier class (e.g. `PeselIdentifier`). ## Exception hierarchy [Section titled “Exception hierarchy”](#exception-hierarchy) All exceptions extend `SlashLab\Numerik\Exceptions\ValidationException`, which itself extends `\RuntimeException`. ```plaintext \RuntimeException └── ValidationException ├── InvalidFormatException — wrong length or invalid characters ├── InvalidChecksumException — checksum digit mismatch └── InvalidDateException — impossible date encoded inside the identifier ``` All exception classes live in the `SlashLab\Numerik\Exceptions` namespace. ## When each exception is thrown [Section titled “When each exception is thrown”](#when-each-exception-is-thrown) | Exception | Thrown when | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `InvalidFormatException` | Input length is wrong, unexpected characters are present, or a structural rule is violated (e.g. NIP tax office `000`). | | `InvalidChecksumException` | The checksum digit does not match the computed value. | | `InvalidDateException` | The date encoded inside a PESEL is impossible (invalid month, non-existent calendar date, or birth date in the future). | ## Catching exceptions [Section titled “Catching exceptions”](#catching-exceptions) ```php use SlashLab\Numerik\Numerik; use SlashLab\Numerik\Exceptions\ValidationException; use SlashLab\Numerik\Exceptions\InvalidFormatException; use SlashLab\Numerik\Exceptions\InvalidChecksumException; use SlashLab\Numerik\Exceptions\InvalidDateException; // Catch any validation failure try { $pesel = Numerik::pesel()->parse($input); } catch (ValidationException $e) { // handle any parse failure echo $e->getMessage(); } // Catch specific failures try { $pesel = Numerik::pesel()->parse($input); } catch (InvalidFormatException $e) { // wrong length or characters } catch (InvalidChecksumException $e) { // checksum mismatch } catch (InvalidDateException $e) { // impossible birth date encoded inside } ``` ## Choosing between parse() and validate() [Section titled “Choosing between parse() and validate()”](#choosing-between-parse-and-validate) Tip If you only need to know *whether* input is valid — use `validate()` or `isValid()`. These never throw and are safe to call on arbitrary untrusted input without wrapping in a try/catch. Use `parse()` when: * You need the value object’s extracted data. * You prefer the exception-based control flow (e.g. inside a service that already has a catch block). Use `tryParse()` when: * You need the value object, but you want `null` instead of an exception on bad input. * You are in a context where throwing would be inconvenient (e.g. inside a loop or a data import pipeline). ```php // Exception-based — throws on invalid input $pesel = Numerik::pesel()->parse($input); // Null-based — returns null on invalid input $pesel = Numerik::pesel()->tryParse($input); if ($pesel === null) { // handle invalid input } ``` ## Strict mode [Section titled “Strict mode”](#strict-mode) All identifier classes accept a `strict` constructor parameter (default `true`). In strict mode, inputs that pass every algorithmic check but are semantically suspicious — such as all-same-digit patterns — are rejected with `ValidationFailureReason::AllSameDigit`. ```php // Strict mode on (default) — rejects suspicious-but-valid inputs Numerik::pesel()->validate('11111111111'); // fails: AllSameDigit // Strict mode off — accepts anything structurally and algorithmically valid Numerik::pesel(strict: false)->validate('11111111111'); // passes ``` Turn strict mode off when processing legacy data that may contain placeholder values, or when you want to distinguish “structurally invalid” from “algorithmically valid but suspicious” in a data quality pipeline. ## Input length guard [Section titled “Input length guard”](#input-length-guard) All identifier methods reject inputs over **32 characters** immediately before any processing. This prevents potential DoS via very large strings. # Validation Results > Understanding the ValidationResult and ValidationFailure classes returned by Numerik validators. All four identifier classes (`PeselIdentifier`, `NipIdentifier`, `RegonIdentifier`, `KrsIdentifier`) expose the same two methods: `isValid()` and `validate()`. Neither ever throws an exception. ## ValidationResult [Section titled “ValidationResult”](#validationresult) `validate()` returns a `SlashLab\Numerik\Result\ValidationResult` readonly class. ### Properties [Section titled “Properties”](#properties) | Property | Type | Description | | ----------- | ------------------------- | -------------------------------------------------------- | | `$isValid` | `bool` | `true` when validation passed. | | `$failures` | `list` | Empty array on success; one or more failures on failure. | ### Methods [Section titled “Methods”](#methods) | Method | Return type | Description | | --------------------------------------------------- | ------------------------- | ----------------------------------------------- | | `isFailed()` | `bool` | Inverse of `$isValid`. | | `getFailures()` | `list` | Returns the failures array. | | `getFirstFailure()` | `ValidationFailure\|null` | First failure, or `null` if valid. | | `hasFailureReason(ValidationFailureReason $reason)` | `bool` | `true` if any failure matches the given reason. | ### Examples [Section titled “Examples”](#examples) ```php use SlashLab\Numerik\Numerik; use SlashLab\Numerik\Enums\ValidationFailureReason; // Passing result $result = Numerik::pesel()->validate('92060512186'); $result->isValid; // true $result->isFailed(); // false $result->failures; // [] $result->getFirstFailure(); // null // Failing result $result = Numerik::nip()->validate('0000000000'); $result->isValid; // false $result->isFailed(); // true // Inspect the first (and usually only) failure $failure = $result->getFirstFailure(); $failure->reason; // ValidationFailureReason::InvalidFormat $failure->message; // 'NIP tax office code cannot be 000.' // Check for a specific reason $result->hasFailureReason(ValidationFailureReason::InvalidChecksum); // false $result->hasFailureReason(ValidationFailureReason::InvalidFormat); // true ``` ## ValidationFailure [Section titled “ValidationFailure”](#validationfailure) Each item in `$failures` is a `SlashLab\Numerik\Result\ValidationFailure` readonly class. ### Properties [Section titled “Properties”](#properties-1) | Property | Type | Description | | ---------- | ------------------------- | ------------------------------------------- | | `$reason` | `ValidationFailureReason` | Enum case identifying the failure category. | | `$message` | `string` | Human-readable description in English. | ## ValidationFailureReason enum [Section titled “ValidationFailureReason enum”](#validationfailurereason-enum) `SlashLab\Numerik\Enums\ValidationFailureReason` is a backed string enum. ### Format failures [Section titled “Format failures”](#format-failures) | Case | Value | Description | | ------------------- | -------------------- | -------------------------------------------------------------------------------------------------- | | `InvalidLength` | `invalid_length` | Input has the wrong number of digits. | | `InvalidCharacters` | `invalid_characters` | Unexpected characters are present after stripping allowed separators. | | `InvalidFormat` | `invalid_format` | Correct length and characters, but a structural rule is violated (e.g. NIP tax office code `000`). | ### Checksum failures [Section titled “Checksum failures”](#checksum-failures) | Case | Value | Description | | ----------------- | ------------------ | -------------------------------------------------------- | | `InvalidChecksum` | `invalid_checksum` | The computed checksum does not match the checksum digit. | ### Encoded-data failures [Section titled “Encoded-data failures”](#encoded-data-failures) | Case | Value | Description | | -------------- | --------------- | ------------------------------------------------------------------- | | `InvalidDate` | `invalid_date` | The date encoded inside the identifier is not a real calendar date. | | `FutureDate` | `future_date` | The encoded birth date is in the future. | | `InvalidMonth` | `invalid_month` | The month encoding does not correspond to any known century range. | ### Semantic failures [Section titled “Semantic failures”](#semantic-failures) | Case | Value | Description | | -------------- | ---------------- | ---------------------------------------------------------------------- | | `AllZeros` | `all_zeros` | All digits are zero — structurally plausible but semantically invalid. | | `AllSameDigit` | `all_same_digit` | All digits are the same non-zero value. | ## Static factory methods [Section titled “Static factory methods”](#static-factory-methods) `ValidationResult` exposes three static constructors used internally and in tests: ```php // Success ValidationResult::pass(); // Failure with a list of failures ValidationResult::fail([ new ValidationFailure(ValidationFailureReason::InvalidChecksum, 'Checksum mismatch.'), ]); // Failure with a single reason — shorthand ValidationResult::failWithReason( ValidationFailureReason::InvalidLength, 'Expected 11 digits, got 10.', ); ``` # IBAN > Validate and parse Polish IBAN numbers (PL-prefixed NRB) with Numerik. IBAN (*International Bank Account Number*) is the international standard for identifying bank accounts. A Polish IBAN is the two-letter country prefix `PL` followed by a 26-digit NRB. Validation requires the `PL` prefix and applies the standard MOD-97 checksum to the NRB portion. Use `Numerik::nrb()` when you have a 26-digit number without the country prefix. Use `Numerik::iban()` when you expect the full international form. ## Usage [Section titled “Usage”](#usage) ```php use SlashLab\Numerik\Numerik; // Boolean Numerik::iban()->isValid('PL61102010260000000000000000'); // true // Spaced format and lowercase prefix are accepted Numerik::iban()->isValid('PL61 1020 1026 0000 0000 0000 0000'); // true Numerik::iban()->isValid('pl61102010260000000000000000'); // true // Bare NRB without prefix is rejected — use Numerik::nrb() for that Numerik::iban()->isValid('61102010260000000000000000'); // false // Rich result $result = Numerik::iban()->validate('PL61102010260000000000000000'); $result->isValid; // true // Parse to value object $iban = Numerik::iban()->parse('PL61102010260000000000000000'); // Null on failure instead of exception $iban = Numerik::iban()->tryParse('bad-input'); // null ``` ## Value object API [Section titled “Value object API”](#value-object-api) `parse()` and `tryParse()` return a `SlashLab\Numerik\ValueObjects\Iban` instance. ### Core [Section titled “Core”](#core) | Method | Return type | Description | | ----------------- | ----------- | ------------------------------------------------------ | | `getRaw()` | `string` | The original input, untouched. | | `getNormalized()` | `string` | `PL` + 26 digits, e.g. `PL61102010260000000000000000`. | | `__toString()` | `string` | Same as `getNormalized()`. | ### Formatting [Section titled “Formatting”](#formatting) | Method | Return type | Description | | ---------------- | ----------- | --------------------------------------------------------------- | | `getFormatted()` | `string` | Standard grouped display: `PL61 1020 1026 0000 0000 0000 0000`. | ### Structure [Section titled “Structure”](#structure) | Method | Return type | Description | | -------------------- | ----------- | ------------------------------------------------------ | | `getCountryCode()` | `string` | Always `PL`. | | `getNrb()` | `string` | The 26-digit NRB without the country prefix. | | `getCheckDigits()` | `string` | Two MOD-97 check digits (positions 3–4 of the IBAN). | | `getSortCode()` | `string` | 8-digit bank sort code. | | `getBankCode()` | `string` | First 3 digits of the sort code — identifies the bank. | | `getAccountNumber()` | `string` | Last 16 digits — the customer account number. | ## Examples [Section titled “Examples”](#examples) ```php $iban = Numerik::iban()->parse('PL61102010260000000000000000'); $iban->getRaw(); // 'PL61102010260000000000000000' $iban->getNormalized(); // 'PL61102010260000000000000000' $iban->getFormatted(); // 'PL61 1020 1026 0000 0000 0000 0000' $iban->getCountryCode(); // 'PL' $iban->getNrb(); // '61102010260000000000000000' $iban->getCheckDigits(); // '61' $iban->getSortCode(); // '10201026' $iban->getBankCode(); // '102' $iban->getAccountNumber(); // '0000000000000000' // Spaced and lowercase inputs normalise to the same value $a = Numerik::iban()->parse('PL61102010260000000000000000'); $b = Numerik::iban()->parse('PL61 1020 1026 0000 0000 0000 0000'); $c = Numerik::iban()->parse('pl61102010260000000000000000'); $a->getNormalized() === $b->getNormalized(); // true $b->getNormalized() === $c->getNormalized(); // true ``` ## Validation algorithm [Section titled “Validation algorithm”](#validation-algorithm) 1. Strip spaces and hyphens. Reject inputs over 40 characters. 2. Assert the first two characters are `PL` (case-insensitive); fail with `InvalidFormat`. 3. Strip the prefix. Assert exactly 26 digits remain; fail with `InvalidLength`. 4. Apply the NRB MOD-97 checksum — see the [Algorithms](/guide/algorithms/#nrb) reference. ## Failure reasons [Section titled “Failure reasons”](#failure-reasons) | Reason | Value | When | | ------------------- | -------------------- | ------------------------------------------------------------------------- | | `InvalidLength` | `invalid_length` | NRB portion is not exactly 26 digits, or raw input exceeds 40 characters. | | `InvalidFormat` | `invalid_format` | Input does not start with `PL`. | | `InvalidCharacters` | `invalid_characters` | NRB portion contains non-digit characters. | | `InvalidChecksum` | `invalid_checksum` | MOD-97 remainder is not 1. | # ID Card > Validate and parse Polish national identity card numbers (dowód osobisty) with Numerik. The Polish national identity card number (*numer dowodu osobistego*) consists of three uppercase letters followed by six digits, where the final digit is a checksum computed using the ICAO 9303 weighted algorithm. The letters O and Q are excluded from the series. ## Usage [Section titled “Usage”](#usage) ```php use SlashLab\Numerik\Numerik; // Boolean validation Numerik::idCard()->isValid('ABC123454'); // true // Lowercase input and hyphens are stripped during normalisation Numerik::idCard()->isValid('abc-123-454'); // true // Rich result $result = Numerik::idCard()->validate('ABC123454'); $result->isValid; // true // Parse to value object $idCard = Numerik::idCard()->parse('ABC123454'); // Null on failure instead of exception $idCard = Numerik::idCard()->tryParse('bad-input'); // null ``` ## Value object API [Section titled “Value object API”](#value-object-api) `parse()` returns a `SlashLab\Numerik\ValueObjects\IdCard` instance or throws on failure. `tryParse()` returns a `SlashLab\Numerik\ValueObjects\IdCard` instance or `null` if parsing fails. ### Core [Section titled “Core”](#core) | Method | Return type | Description | | ----------------- | ----------- | ------------------------------------------------- | | `getRaw()` | `string` | The original input, untouched. | | `getNormalized()` | `string` | 3 uppercase letters + 6 digits, e.g. `ABC123454`. | | `__toString()` | `string` | Same as `getNormalized()`. | ### Structure [Section titled “Structure”](#structure) | Method | Return type | Description | | ----------------------- | ----------- | -------------------------------------------------------- | | `getSeries()` | `string` | First 3 letters — the document series, e.g. `ABC`. | | `getSequentialNumber()` | `string` | 5-digit sequential number (positions 4–8), e.g. `12345`. | | `getCheckDigit()` | `string` | Single check digit at position 9, e.g. `4`. | ## Examples [Section titled “Examples”](#examples) ```php $idCard = Numerik::idCard()->parse('ABC123454'); $idCard->getRaw(); // 'ABC123454' $idCard->getNormalized(); // 'ABC123454' $idCard->getSeries(); // 'ABC' $idCard->getSequentialNumber(); // '12345' $idCard->getCheckDigit(); // '4' // Lowercase and hyphens normalise to the same value $idCard = Numerik::idCard()->parse('abc-123-454'); $idCard->getRaw(); // 'abc-123-454' $idCard->getNormalized(); // 'ABC123454' ``` ## Validation algorithm [Section titled “Validation algorithm”](#validation-algorithm) 1. Reject inputs over 32 characters; fail with `InvalidLength`. 2. Strip spaces and hyphens. Convert to uppercase. 3. Assert exactly 9 characters remain; fail with `InvalidLength`. 4. Assert the first 3 characters are letters; fail with `InvalidCharacters`. 5. Assert the series contains no O or Q; fail with `InvalidFormat`. 6. Assert characters 4–9 are digits; fail with `InvalidCharacters`. 7. Apply the ICAO 9303 checksum over positions 1–8; fail with `InvalidChecksum` if the result does not match position 9. See [Algorithms](/guide/algorithms/#icao-9303). ## Failure reasons [Section titled “Failure reasons”](#failure-reasons) | Reason | Value | When | | ------------------- | -------------------- | ------------------------------------------------------------------------------------------- | | `InvalidLength` | `invalid_length` | Number is not exactly 9 characters after normalisation, or raw input exceeds 32 characters. | | `InvalidCharacters` | `invalid_characters` | Non-letter characters in the series, or non-digit characters in the number portion. | | `InvalidFormat` | `invalid_format` | Series contains the letter O or Q. | | `InvalidChecksum` | `invalid_checksum` | ICAO 9303 check digit does not match. | ## Related [Section titled “Related”](#related) * [Passport](/identifiers/passport) — same ICAO 9303 checksum algorithm; identifies the same person in an international context. * [PESEL](/identifiers/pesel) — universal personal identifier linked to the same individual. # KRS > Validate and parse Polish KRS court register numbers with Numerik. KRS (*Krajowy Rejestr Sądowy*) is Poland’s national court register number assigned to legal entities. It is a 10-digit sequential registry number. Offline validation limitation KRS has no publicly documented checksum algorithm. Numerik validates **format and structure only** — it cannot verify whether a given KRS number is actually assigned in the registry. For authoritative verification, query the official [KRS registry API](https://prs.ms.gov.pl/krs/openApi). ## Usage [Section titled “Usage”](#usage) ```php use SlashLab\Numerik\Numerik; // Boolean Numerik::krs()->isValid('0000127206'); // true // Numbers without leading zeros are accepted and normalised Numerik::krs()->isValid('127206'); // true // Rich result $result = Numerik::krs()->validate('0000127206'); $result->isValid; // true // Parse to value object $krs = Numerik::krs()->parse('0000127206'); // Null on failure instead of exception $krs = Numerik::krs()->tryParse('bad-input'); // null ``` ## Value object API [Section titled “Value object API”](#value-object-api) `parse()` and `tryParse()` return a `SlashLab\Numerik\ValueObjects\Krs` instance. ### Core [Section titled “Core”](#core) | Method | Return type | Description | | ----------------- | ----------- | ------------------------------------------------- | | `getRaw()` | `string` | The original input, untouched. | | `getNormalized()` | `string` | Raw digits as provided (no extra padding stored). | | `__toString()` | `string` | Same as `getNormalized()`. | ### Formatting & metadata [Section titled “Formatting & metadata”](#formatting--metadata) | Method | Return type | Description | | ------------------- | ----------- | -------------------------------------------- | | `getFormatted()` | `string` | Zero-padded to 10 digits, e.g. `0000127206`. | | `getNumericValue()` | `int` | Numeric integer value of the KRS number. | ## Examples [Section titled “Examples”](#examples) ```php $krs = Numerik::krs()->parse('0000127206'); $krs->getRaw(); // '0000127206' $krs->getNormalized(); // '0000127206' $krs->getFormatted(); // '0000127206' $krs->getNumericValue(); // 127206 // Short form — leading zeros are not required on input $krs = Numerik::krs()->parse('127206'); $krs->getRaw(); // '127206' $krs->getNormalized(); // '127206' $krs->getFormatted(); // '0000127206' $krs->getNumericValue(); // 127206 ``` ## Validation algorithm [Section titled “Validation algorithm”](#validation-algorithm) KRS is a sequential registry number with no checksum. Validation is structural only: 1. Strip spaces. Assert between 1 and 10 digits remain. 2. Assert all remaining characters are digits. 3. Assert the numeric value is not zero. 4. In strict mode, zero-pad to 10 digits and reject inputs where all digits are identical; fail with `AllSameDigit`. A number that passes these checks is **structurally valid** — it conforms to the KRS format. It may or may not be an actually registered entity. ## Failure reasons [Section titled “Failure reasons”](#failure-reasons) | Reason | Value | When | | ------------------- | -------------------- | ------------------------------------------------------------------------------- | | `InvalidLength` | `invalid_length` | Input has more than 10 digits after normalisation. | | `InvalidCharacters` | `invalid_characters` | Non-digit characters remain after stripping whitespace. | | `AllZeros` | `all_zeros` | All digits are zero — numeric value is 0, which is not a valid registry number. | | `AllSameDigit` | `all_same_digit` | All 10 digits are identical — strict mode only. | # NIP > Validate and parse Polish NIP tax identification numbers with Numerik. NIP (*Numer Identyfikacji Podatkowej*) is Poland’s 10-digit tax identification number, used by both individuals and legal entities. ## Usage [Section titled “Usage”](#usage) ```php use SlashLab\Numerik\Numerik; // Boolean Numerik::nip()->isValid('5260250274'); // true // Rich result $result = Numerik::nip()->validate('5260250274'); $result->isValid; // true // Parse to value object $nip = Numerik::nip()->parse('5260250274'); // Null on failure instead of exception $nip = Numerik::nip()->tryParse('bad-input'); // null ``` NIP accepts hyphens and spaces as separators in the input: ```php Numerik::nip()->isValid('526-025-02-74'); // true Numerik::nip()->isValid('526 025 02 74'); // true ``` ## Value object API [Section titled “Value object API”](#value-object-api) `parse()` and `tryParse()` return a `SlashLab\Numerik\ValueObjects\Nip` instance. ### Core [Section titled “Core”](#core) | Method | Return type | Description | | ----------------- | ----------- | ------------------------------------------- | | `getRaw()` | `string` | The original input, untouched. | | `getNormalized()` | `string` | 10 raw digits (hyphens and spaces removed). | | `__toString()` | `string` | Same as `getNormalized()`. | ### Formatting & metadata [Section titled “Formatting & metadata”](#formatting--metadata) | Method | Return type | Description | | --------------------------- | ----------- | ----------------------------------------------------------------------------- | | `getFormatted()` | `string` | Canonical `NNN-NNN-NN-NN` display form. | | `getFormattedAlternative()` | `string` | Alternative `NNN-NN-NN-NNN` display form used by some legal entity documents. | | `getTaxOfficeCode()` | `string` | First 3 digits — indicates the issuing tax office. | ## Examples [Section titled “Examples”](#examples) ```php $nip = Numerik::nip()->parse('5260250274'); $nip->getRaw(); // '5260250274' $nip->getNormalized(); // '5260250274' $nip->getFormatted(); // '526-025-02-74' $nip->getFormattedAlternative(); // '526-02-50-274' $nip->getTaxOfficeCode(); // '526' // With formatted input $nip = Numerik::nip()->parse('526-025-02-74'); $nip->getRaw(); // '526-025-02-74' $nip->getNormalized(); // '5260250274' ``` ## Failure reasons [Section titled “Failure reasons”](#failure-reasons) | Reason | Value | When | | ------------------- | -------------------- | -------------------------------------------------------------- | | `InvalidLength` | `invalid_length` | Input is not exactly 10 digits after stripping separators. | | `InvalidCharacters` | `invalid_characters` | Characters other than digits, hyphens, and spaces are present. | | `InvalidFormat` | `invalid_format` | First 3 digits are `000` (no valid tax office has code 000). | | `InvalidChecksum` | `invalid_checksum` | Checksum digit does not match. | | `AllSameDigit` | `all_same_digit` | All digits are identical — strict mode only. | ## Validation algorithm [Section titled “Validation algorithm”](#validation-algorithm) Weights: `6, 5, 7, 2, 3, 4, 5, 6, 7` 1. Strip hyphens and spaces. Assert exactly 10 digits. 2. Assert the first 3 digits are not `000`. 3. Multiply each of the first 9 digits by its weight, sum, take `mod 11`. The result must equal digit 10; since a single digit can only be 0–9, a modulo result of `10` can never match and always fails with `InvalidChecksum`. See [Algorithms](/guide/algorithms/) for the full reference. # NRB > Validate and parse Polish NRB bank account numbers with Numerik. NRB (*Numer Rachunku Bankowego*) is the Polish domestic bank account number format. It consists of 26 digits: a 2-digit MOD-97 check, an 8-digit bank sort code, and a 16-digit account number. The international IBAN form is `PL` followed by the 26-digit NRB. ## Usage [Section titled “Usage”](#usage) ```php use SlashLab\Numerik\Numerik; // Boolean Numerik::nrb()->isValid('61102010260000000000000000'); // true // Spaced format is accepted and normalised Numerik::nrb()->isValid('61 1020 1026 0000 0000 0000 0000'); // true // IBAN format (PL prefix) is accepted and stripped during normalisation Numerik::nrb()->isValid('PL61102010260000000000000000'); // true Numerik::nrb()->isValid('PL61 1020 1026 0000 0000 0000 0000'); // true // Rich result $result = Numerik::nrb()->validate('61102010260000000000000000'); $result->isValid; // true // Parse to value object $nrb = Numerik::nrb()->parse('61102010260000000000000000'); // Null on failure instead of exception $nrb = Numerik::nrb()->tryParse('bad-input'); // null ``` ## Value object API [Section titled “Value object API”](#value-object-api) `parse()` and `tryParse()` return a `SlashLab\Numerik\ValueObjects\Nrb` instance. ### Core [Section titled “Core”](#core) | Method | Return type | Description | | ----------------- | ----------- | -------------------------------- | | `getRaw()` | `string` | The original input, untouched. | | `getNormalized()` | `string` | 26 digits, no prefix, no spaces. | | `__toString()` | `string` | Same as `getNormalized()`. | ### Formatting [Section titled “Formatting”](#formatting) | Method | Return type | Description | | -------------------- | ----------- | ------------------------------------------------------------------------ | | `getFormatted()` | `string` | Standard Polish display format: `CC BBBB BBBB AAAA AAAA AAAA AAAA`. | | `getIban()` | `string` | Full IBAN string: `PL` + 26 digits, e.g. `PL61102010260000000000000000`. | | `getFormattedIban()` | `string` | Grouped IBAN: `PL61 1020 1026 0000 0000 0000 0000`. | ### Structure [Section titled “Structure”](#structure) | Method | Return type | Description | | -------------------- | ----------- | ------------------------------------------------------------------- | | `getCheckDigits()` | `string` | First 2 digits — the MOD-97 check value. | | `getSortCode()` | `string` | 8-digit bank sort code (bank identifier + branch + internal check). | | `getBankCode()` | `string` | First 3 digits of the sort code — identifies the bank. | | `getAccountNumber()` | `string` | Last 16 digits — the customer account number. | ## Examples [Section titled “Examples”](#examples) ```php $nrb = Numerik::nrb()->parse('61102010260000000000000000'); $nrb->getRaw(); // '61102010260000000000000000' $nrb->getNormalized(); // '61102010260000000000000000' $nrb->getFormatted(); // '61 1020 1026 0000 0000 0000 0000' $nrb->getIban(); // 'PL61102010260000000000000000' $nrb->getFormattedIban(); // 'PL61 1020 1026 0000 0000 0000 0000' $nrb->getCheckDigits(); // '61' $nrb->getSortCode(); // '10201026' $nrb->getBankCode(); // '102' $nrb->getAccountNumber(); // '0000000000000000' // Input formats are all equivalent $a = Numerik::nrb()->parse('61102010260000000000000000'); $b = Numerik::nrb()->parse('61 1020 1026 0000 0000 0000 0000'); $c = Numerik::nrb()->parse('PL61102010260000000000000000'); $a->getNormalized() === $b->getNormalized(); // true $b->getNormalized() === $c->getNormalized(); // true ``` ## Validation algorithm [Section titled “Validation algorithm”](#validation-algorithm) NRB uses the standard IBAN MOD-97 checksum: 1. Strip spaces and hyphens. Strip the optional `PL` country prefix. Reject inputs over 40 characters. 2. Assert exactly 26 digits remain; fail with `InvalidLength`. 3. Assert all characters are digits; fail with `InvalidCharacters`. 4. Verify the MOD-97 checksum: * Rearrange as: digits 3–26 (the BBAN) + `2521` (numeric encoding of `PL`) + digits 1–2 (check digits). * Compute the resulting number modulo 97. * Fail with `InvalidChecksum` if the result is not `1`. ## Failure reasons [Section titled “Failure reasons”](#failure-reasons) | Reason | Value | When | | ------------------- | -------------------- | ------------------------------------------------------------------------------------- | | `InvalidLength` | `invalid_length` | Input is not exactly 26 digits after normalisation, or exceeds 40 characters raw. | | `InvalidCharacters` | `invalid_characters` | Non-digit characters remain after stripping whitespace, hyphens, and the `PL` prefix. | | `InvalidChecksum` | `invalid_checksum` | MOD-97 remainder is not 1. | # Passport > Validate and parse Polish passport numbers with Numerik. The Polish passport number consists of two uppercase letters followed by seven digits, where the final digit is a checksum computed using the ICAO 9303 weighted algorithm. ## Usage [Section titled “Usage”](#usage) ```php use SlashLab\Numerik\Numerik; // Boolean validation Numerik::passport()->isValid('AB1234564'); // true // Lowercase input and spaces are stripped during normalisation Numerik::passport()->isValid('ab 123456 4'); // true // Rich result $result = Numerik::passport()->validate('AB1234564'); $result->isValid; // true // Parse to value object $passport = Numerik::passport()->parse('AB1234564'); // Null on failure instead of exception $passport = Numerik::passport()->tryParse('bad-input'); // null ``` ## Value object API [Section titled “Value object API”](#value-object-api) `parse()` returns a `SlashLab\Numerik\ValueObjects\Passport` instance or throws on failure. `tryParse()` returns a `SlashLab\Numerik\ValueObjects\Passport` instance or `null` if parsing fails. ### Core [Section titled “Core”](#core) | Method | Return type | Description | | ----------------- | ----------- | ------------------------------------------------- | | `getRaw()` | `string` | The original input, untouched. | | `getNormalized()` | `string` | 2 uppercase letters + 7 digits, e.g. `AB1234564`. | | `__toString()` | `string` | Same as `getNormalized()`. | ### Structure [Section titled “Structure”](#structure) | Method | Return type | Description | | ----------------------- | ----------- | --------------------------------------------------------- | | `getSeries()` | `string` | First 2 letters — the document series, e.g. `AB`. | | `getSequentialNumber()` | `string` | 6-digit sequential number (positions 3–8), e.g. `123456`. | | `getCheckDigit()` | `string` | Single check digit at position 9, e.g. `4`. | ## Examples [Section titled “Examples”](#examples) ```php $passport = Numerik::passport()->parse('AB1234564'); $passport->getRaw(); // 'AB1234564' $passport->getNormalized(); // 'AB1234564' $passport->getSeries(); // 'AB' $passport->getSequentialNumber(); // '123456' $passport->getCheckDigit(); // '4' // Lowercase and spaces normalise to the same value $passport = Numerik::passport()->parse('ab 123456 4'); $passport->getRaw(); // 'ab 123456 4' $passport->getNormalized(); // 'AB1234564' ``` ## Validation algorithm [Section titled “Validation algorithm”](#validation-algorithm) 1. Reject inputs over 32 characters; fail with `InvalidLength`. 2. Strip spaces and hyphens. Convert to uppercase. 3. Assert exactly 9 characters remain; fail with `InvalidLength`. 4. Assert the first 2 characters are letters; fail with `InvalidCharacters`. 5. Assert characters 3–9 are digits; fail with `InvalidCharacters`. 6. Apply the ICAO 9303 checksum over positions 1–8; fail with `InvalidChecksum` if the result does not match position 9. See [Algorithms](/guide/algorithms/#icao-9303). ## Failure reasons [Section titled “Failure reasons”](#failure-reasons) | Reason | Value | When | | ------------------- | -------------------- | ------------------------------------------------------------------------------------------- | | `InvalidLength` | `invalid_length` | Number is not exactly 9 characters after normalisation, or raw input exceeds 32 characters. | | `InvalidCharacters` | `invalid_characters` | Non-letter characters in the series, or non-digit characters in the number portion. | | `InvalidChecksum` | `invalid_checksum` | ICAO 9303 check digit does not match. | ## Related [Section titled “Related”](#related) * [ID Card](/identifiers/id-card) — same ICAO 9303 checksum algorithm; identifies the same person within Poland. * [PESEL](/identifiers/pesel) — universal personal identifier linked to the same individual. # PESEL > Validate and parse Polish PESEL numbers with Numerik. Extracts birth date, gender, age, century, and ordinal number. PESEL (*Powszechny Elektroniczny System Ewidencji Ludności*) is Poland’s universal 11-digit citizen identifier. It encodes the holder’s birth date, birth century, gender, and an ordinal serial number. ## Usage [Section titled “Usage”](#usage) ```php use SlashLab\Numerik\Numerik; // Boolean Numerik::pesel()->isValid('92060512186'); // true // Rich result $result = Numerik::pesel()->validate('92060512186'); $result->isValid; // true // Parse to value object $pesel = Numerik::pesel()->parse('92060512186'); // Null on failure instead of exception $pesel = Numerik::pesel()->tryParse('bad-input'); // null ``` ## Value object API [Section titled “Value object API”](#value-object-api) `parse()` and `tryParse()` return a `SlashLab\Numerik\ValueObjects\Pesel` instance. ### Core [Section titled “Core”](#core) | Method | Return type | Description | | ----------------- | ----------- | ------------------------------ | | `getRaw()` | `string` | The original input, untouched. | | `getNormalized()` | `string` | Whitespace-stripped digits. | | `__toString()` | `string` | Same as `getNormalized()`. | ### Extracted data [Section titled “Extracted data”](#extracted-data) | Method | Return type | Description | | -------------------- | ------------------- | -------------------------------------------------------------------------------------------------------- | | `getBirthDate()` | `DateTimeImmutable` | Birth date decoded from the PESEL. | | `getGender()` | `Gender` | `Gender::Male` or `Gender::Female`. | | `getOrdinalNumber()` | `int` | The 4-digit ordinal serial (digits 7–10). The last digit also encodes gender: odd = male, even = female. | | `getCentury()` | `int` | The birth century as a base year, e.g. `1900`, `2000`, `2100`. | ### Utility [Section titled “Utility”](#utility) | Method | Return type | Description | | ------------ | ----------- | -------------------------------------------- | | `isMale()` | `bool` | `true` when gender is `Gender::Male`. | | `isFemale()` | `bool` | `true` when gender is `Gender::Female`. | | `getAge()` | `int` | Full years elapsed from birth date to today. | | `isAdult()` | `bool` | `true` when `getAge() >= 18`. | ## Examples [Section titled “Examples”](#examples) ```php $pesel = Numerik::pesel()->parse('92060512186'); $pesel->getRaw(); // '92060512186' $pesel->getNormalized(); // '92060512186' $pesel->getBirthDate()->format('Y-m-d'); // '1992-06-05' $pesel->getGender(); // Gender::Female $pesel->isFemale(); // true $pesel->getOrdinalNumber(); // 1218 (digits 7–10) $pesel->getCentury(); // 1900 $pesel->getAge(); // calculated from today $pesel->isAdult(); // true ``` ## Century encoding [Section titled “Century encoding”](#century-encoding) The month digits in a PESEL encode both the real month and the birth century: | Stored month range | Real month | Birth century | | ------------------ | ---------- | ------------- | | 01–12 | 01–12 | 1900–1999 | | 21–32 | 01–12 | 2000–2099 | | 41–52 | 01–12 | 2100–2199 | | 61–72 | 01–12 | 2200–2299 | | 81–92 | 01–12 | 1800–1899 | ## Failure reasons [Section titled “Failure reasons”](#failure-reasons) | Reason | Value | When | | ------------------- | -------------------- | ----------------------------------------------------------- | | `InvalidLength` | `invalid_length` | Input is not exactly 11 digits after normalisation. | | `InvalidCharacters` | `invalid_characters` | Non-digit characters remain after stripping whitespace. | | `InvalidMonth` | `invalid_month` | Month encoding does not match any known century range. | | `InvalidDate` | `invalid_date` | The decoded date is not a real calendar date. | | `FutureDate` | `future_date` | The decoded birth date is in the future (strict mode only). | | `InvalidChecksum` | `invalid_checksum` | Checksum digit does not match the computed value. | | `AllSameDigit` | `all_same_digit` | All 11 digits are identical (strict mode only). | ## Validation algorithm [Section titled “Validation algorithm”](#validation-algorithm) Weights: `1, 3, 7, 9, 1, 3, 7, 9, 1, 3` 1. Reject inputs longer than 32 characters. Strip whitespace. Assert exactly 11 digits. 2. Decode birth date using the century encoding table above. Assert the date is a real calendar date. In strict mode, also reject birth dates in the future. 3. Compute checksum: multiply each of the first 10 digits by its weight, sum the products, take `mod 10`, subtract from `10`, take `mod 10` again. The result must equal digit 11. 4. In strict mode, reject inputs where all 11 digits are identical. See [Algorithms](/guide/algorithms/) for the full reference. # REGON > Validate and parse Polish REGON business registry numbers — both 9-digit and 14-digit — with Numerik. REGON (*Rejestr Gospodarki Narodowej*) is Poland’s business registry identifier. It exists in two forms: * **9 digits** — for individual entities (sole traders, natural persons). * **14 digits** — for legal entities with local units (the first 9 digits are the base REGON, the last 5 identify the local unit). ## Usage [Section titled “Usage”](#usage) ```php use SlashLab\Numerik\Numerik; // Boolean Numerik::regon()->isValid('850518457'); // true (9-digit) Numerik::regon()->isValid('85051845749370'); // true (14-digit) // Rich result $result = Numerik::regon()->validate('850518457'); $result->isValid; // true // Parse to value object $regon = Numerik::regon()->parse('850518457'); // Null on failure instead of exception $regon = Numerik::regon()->tryParse('bad-input'); // null ``` ## Value object API [Section titled “Value object API”](#value-object-api) `parse()` and `tryParse()` return a `SlashLab\Numerik\ValueObjects\Regon` instance. ### Core [Section titled “Core”](#core) | Method | Return type | Description | | ----------------- | ----------- | ------------------------------ | | `getRaw()` | `string` | The original input, untouched. | | `getNormalized()` | `string` | Whitespace-stripped digits. | | `__toString()` | `string` | Same as `getNormalized()`. | ### Type & structure [Section titled “Type & structure”](#type--structure) | Method | Return type | Description | | ---------------------- | -------------- | ------------------------------------------------------------------------- | | `getType()` | `RegonType` | `RegonType::Individual` (9-digit) or `RegonType::LegalEntity` (14-digit). | | `getBaseRegon()` | `string` | First 9 digits — always present for both types. | | `getLocalUnitSuffix()` | `string\|null` | Last 5 digits for 14-digit numbers; `null` for 9-digit. | | `isLocalUnit()` | `bool` | `true` for 14-digit (legal entity with local unit). | ## Examples [Section titled “Examples”](#examples) ```php // 9-digit REGON $regon = Numerik::regon()->parse('850518457'); $regon->getType(); // RegonType::Individual $regon->getBaseRegon(); // '850518457' $regon->getLocalUnitSuffix(); // null $regon->isLocalUnit(); // false // 14-digit REGON $regon = Numerik::regon()->parse('85051845749370'); $regon->getType(); // RegonType::LegalEntity $regon->getBaseRegon(); // '850518457' $regon->getLocalUnitSuffix(); // '49370' $regon->isLocalUnit(); // true ``` ## Failure reasons [Section titled “Failure reasons”](#failure-reasons) | Reason | Value | When | | ------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------- | | `InvalidLength` | `invalid_length` | Input is not exactly 9 or 14 digits after normalisation. | | `InvalidCharacters` | `invalid_characters` | Non-digit characters remain after stripping whitespace. | | `InvalidChecksum` | `invalid_checksum` | Checksum digit does not match. For 14-digit, can refer to either the base 9-digit check or the full 14-digit check. | ## Validation algorithm [Section titled “Validation algorithm”](#validation-algorithm) **9-digit weights:** `8, 9, 2, 3, 4, 5, 6, 7` **14-digit weights:** `2, 4, 8, 5, 0, 9, 7, 3, 6, 1, 2, 4, 8` **9-digit validation:** 1. Strip whitespace. Assert exactly 9 digits. 2. Multiply the first 8 digits by the 9-digit weights, sum, take `mod 11`. If the result is `10`, the checksum digit must be `0`. Otherwise the result must equal digit 9. **14-digit validation:** 1. Strip whitespace. Assert exactly 14 digits. 2. Validate the first 9 digits as a standalone 9-digit REGON. 3. Multiply all 13 significant digits by the 14-digit weights, sum, take `mod 11`. If the result is `10`, checksum digit must be `0`. Otherwise the result must equal digit 14. See [Algorithms](/guide/algorithms/) for the full reference. # VAT-EU > Validate and parse Polish VAT-EU numbers (PL-prefixed NIP) with Numerik. VAT-EU (*Numer VAT UE*) is the intra-EU VAT identification number used in cross-border transactions within the European Union. For Polish taxpayers it is the two-letter country prefix `PL` followed by a standard 10-digit NIP. Validation strips the prefix and applies the NIP MOD-11 checksum algorithm. ## Usage [Section titled “Usage”](#usage) ```php use SlashLab\Numerik\Numerik; // Boolean Numerik::vatEu()->isValid('PL5260250274'); // true // Separators and lowercase prefix are accepted Numerik::vatEu()->isValid('PL526-025-02-74'); // true Numerik::vatEu()->isValid('pl5260250274'); // true // Rich result $result = Numerik::vatEu()->validate('PL5260250274'); $result->isValid; // true // Parse to value object $vatEu = Numerik::vatEu()->parse('PL5260250274'); // Null on failure instead of exception $vatEu = Numerik::vatEu()->tryParse('bad-input'); // null ``` ## Value object API [Section titled “Value object API”](#value-object-api) `parse()` and `tryParse()` return a `SlashLab\Numerik\ValueObjects\VatEu` instance. ### Core [Section titled “Core”](#core) | Method | Return type | Description | | ----------------- | ----------- | -------------------------------------- | | `getRaw()` | `string` | The original input, untouched. | | `getNormalized()` | `string` | `PL` + 10 digits, e.g. `PL5260250274`. | | `__toString()` | `string` | Same as `getNormalized()`. | ### Formatting & metadata [Section titled “Formatting & metadata”](#formatting--metadata) | Method | Return type | Description | | ------------------ | ----------- | ----------------------------------------------------------- | | `getCountryCode()` | `string` | Always `PL`. | | `getNip()` | `string` | The 10-digit NIP without the country prefix. | | `getFormatted()` | `string` | `PL` + standard NIP display format, e.g. `PL526-025-02-74`. | ## Examples [Section titled “Examples”](#examples) ```php $vatEu = Numerik::vatEu()->parse('PL5260250274'); $vatEu->getRaw(); // 'PL5260250274' $vatEu->getNormalized(); // 'PL5260250274' $vatEu->getCountryCode(); // 'PL' $vatEu->getNip(); // '5260250274' $vatEu->getFormatted(); // 'PL526-025-02-74' // Separators are stripped; PL prefix is uppercased $vatEu = Numerik::vatEu()->parse('pl526-025-02-74'); $vatEu->getRaw(); // 'pl526-025-02-74' $vatEu->getNormalized(); // 'PL5260250274' ``` ## Validation algorithm [Section titled “Validation algorithm”](#validation-algorithm) 1. Strip spaces and hyphens. Reject inputs over 32 characters. 2. Assert the first two characters are `PL` (case-insensitive); fail with `InvalidFormat`. 3. Strip the prefix. Assert exactly 10 digits remain; fail with `InvalidLength`. 4. Apply the NIP MOD-11 checksum — see the [Algorithms](/guide/algorithms/#nip) reference. ## Failure reasons [Section titled “Failure reasons”](#failure-reasons) | Reason | Value | When | | ------------------- | -------------------- | ------------------------------------------------------------------------- | | `InvalidLength` | `invalid_length` | NIP portion is not exactly 10 digits, or raw input exceeds 32 characters. | | `InvalidFormat` | `invalid_format` | Input does not start with `PL`, or NIP tax office code is `000`. | | `InvalidCharacters` | `invalid_characters` | NIP portion contains non-digit characters. | | `InvalidChecksum` | `invalid_checksum` | NIP MOD-11 check digit does not match. | | `AllSameDigit` | `all_same_digit` | NIP consists of a single repeated digit — strict mode only. | # Laravel Integration > Use Numerik validation rules directly inside Laravel Form Requests and the Validator facade via slashlab/numerik-laravel. [`slashlab/numerik-laravel`](https://github.com/Sqrcz/numerik-laravel) adds Numerik’s validators as Laravel validation rules. It supports both class-based and string-based syntax and is auto-discovered — no manual registration needed. ## Requirements [Section titled “Requirements”](#requirements) * PHP **8.3** or higher * Laravel **11**, **12**, or **13** ## Installation [Section titled “Installation”](#installation) 1. **Require the package** ```bash composer require slashlab/numerik-laravel ``` 2. **No configuration needed** The service provider is auto-discovered. There is no config file to publish or boot step to add. ## Usage [Section titled “Usage”](#usage) * Class-based rules ```php // Personal use SlashLab\NumerikLaravel\Rules\PeselRule; use SlashLab\NumerikLaravel\Rules\IdCardRule; use SlashLab\NumerikLaravel\Rules\PassportRule; // Tax & Business use SlashLab\NumerikLaravel\Rules\NipRule; use SlashLab\NumerikLaravel\Rules\VatEuRule; use SlashLab\NumerikLaravel\Rules\RegonRule; use SlashLab\NumerikLaravel\Rules\KrsRule; // Banking use SlashLab\NumerikLaravel\Rules\NrbRule; use SlashLab\NumerikLaravel\Rules\IbanRule; public function rules(): array { return [ 'pesel' => ['required', new PeselRule()], 'id_card' => ['required', new IdCardRule()], 'passport' => ['required', new PassportRule()], 'nip' => ['required', new NipRule()], 'vat_eu' => ['required', new VatEuRule()], 'regon' => ['required', new RegonRule()], 'krs' => ['required', new KrsRule()], 'nrb' => ['required', new NrbRule()], 'iban' => ['required', new IbanRule()], ]; } ``` * String-based rules Use plain strings when you don’t need constructor options. The rules are registered under the lowercase identifier name. ```php public function rules(): array { return [ 'pesel' => ['required', 'pesel'], 'id_card' => ['required', 'id_card'], 'passport' => ['required', 'passport'], 'nip' => ['required', 'nip'], 'vat_eu' => ['required', 'vat_eu'], 'regon' => ['required', 'regon'], 'krs' => ['required', 'krs'], 'nrb' => ['required', 'nrb'], 'iban' => ['required', 'iban'], ]; } ``` Caution String-based rules always return a generic validation message regardless of the failure reason. Use class-based rules when specific messages matter (e.g. distinguishing a wrong checksum from an invalid length). ## Validation messages [Section titled “Validation messages”](#validation-messages) Class-based rules return a distinct message for each failure reason. For example, a NIP with an incorrect checksum returns a different message than one with an invalid length. Error messages resolve the field label from `validation.attributes` when available, matching the behaviour of Laravel’s built-in rules. If no label is registered, the field name is humanised — underscores replaced with spaces, first letter capitalised. lang/en/validation.php ```php 'attributes' => [ 'nip_number' => 'NIP', ], ``` The package ships with **English** (`en`) and **Polish** (`pl`) translations. To publish and customise them: ```bash php artisan vendor:publish --tag=numerik-lang ``` This copies the translation files to `lang/vendor/numerik/`. Edit the messages there to override any string. ## Strict mode [Section titled “Strict mode”](#strict-mode) All rules accept an optional `strict` parameter (default `true`). In strict mode, additional semantic checks are applied on top of structural and checksum validation. For PESEL, strict mode rejects numbers with a future birth date or all-identical digits: ```php // strict on (default) — rejects future dates and suspicious patterns new PeselRule() // strict off — only structural and checksum checks are applied new PeselRule(strict: false) ``` The same parameter is accepted by `NipRule`, `VatEuRule`, `RegonRule`, `KrsRule`, `NrbRule`, `IbanRule`, `IdCardRule`, and `PassportRule`. Tip Turn strict mode off when you need to accept test or synthetic data — placeholder PESELs like `22222222222` are common in staging environments. ## PESEL constraints [Section titled “PESEL constraints”](#pesel-constraints) `PeselRule` supports additional constraints that are checked after the number passes structural validation. ### Gender [Section titled “Gender”](#gender) Reject numbers that do not match an expected gender: ```php use SlashLab\Numerik\Enums\Gender; new PeselRule(gender: Gender::Male) new PeselRule(gender: Gender::Female) ``` ### Birth date range [Section titled “Birth date range”](#birth-date-range) Reject numbers whose encoded birth date falls outside a given range: ```php new PeselRule(bornBefore: new DateTimeImmutable('2000-01-01')) new PeselRule(bornAfter: new DateTimeImmutable('1980-12-31')) ``` Constraints can be combined: ```php new PeselRule( gender: Gender::Female, bornAfter: new DateTimeImmutable('1990-01-01'), bornBefore: new DateTimeImmutable('2005-01-01'), ) ``` ## Source [Section titled “Source”](#source) * GitHub: [github.com/Sqrcz/numerik-laravel](https://github.com/Sqrcz/numerik-laravel) * Packagist: [packagist.org/packages/slashlab/numerik-laravel](https://packagist.org/packages/slashlab/numerik-laravel)