Skip to content

Commit

Permalink
Merge pull request #2 from IonBazan/bugfix/series-m
Browse files Browse the repository at this point in the history
Fix series M support
  • Loading branch information
IonBazan authored Feb 17, 2022
2 parents a00ee60 + 8c6bf98 commit 4cb22e7
Show file tree
Hide file tree
Showing 5 changed files with 52 additions and 15 deletions.
7 changes: 4 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ jobs:
php-versions:
- '7.4'
- '8.0'
- '8.1'
include:
- php-versions: '8.1'
- php-versions: '8.2'
composer-flags: '--ignore-platform-reqs'
steps:
- name: Checkout
Expand All @@ -29,14 +30,14 @@ jobs:
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
- name: Run mutation tests
if: ${{ matrix.php-versions == 8.0 }}
if: ${{ matrix.php-versions == 8.1 }}
env:
STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
run: |
composer req infection/infection
vendor/bin/infection --ignore-msi-with-no-mutations --min-covered-msi=100 --min-msi=100 -s -j4
- name: Run phpstan
if: ${{ matrix.php-versions == 8.0 }}
if: ${{ matrix.php-versions == 8.1 }}
run: |
composer req phpstan/phpstan
vendor/bin/phpstan analyse src -l 6
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
[![Downloads](https://img.shields.io/packagist/dt/ion-bazan/nric.svg)](https://packagist.org/packages/ion-bazan/nric)
[![License](https://img.shields.io/packagist/l/ion-bazan/nric.svg)](https://packagist.org/packages/ion-bazan/nric)

**It now supports new 2022 M-series FIN numbers!**

This package provides a self-validating value object for storing, generating and validating Singapore NRIC and FIN numbers in PHP.

## Usage
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ion-bazan/nric",
"type": "library",
"description": "Provides a value object to store, generate and validate Singapore NRIC/FIN numbers",
"description": "Provides a value object to store, generate and validate Singapore NRIC/FIN numbers, including new M-series FIN numbers",
"keywords": ["Singapore", "NRIC", "FIN", "ID", "value object", "validator", "generator"],
"license": "MIT",
"authors": [
Expand Down
39 changes: 35 additions & 4 deletions src/NRIC.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ final class NRIC implements Stringable
private const LENGTH = 9;
private const CHECKSUM_CITIZEN = 'JZIHGFEDCBA';
private const CHECKSUM_FOREIGNER = 'XWUTRQPNMLK';
private const CHECKSUM_FOREIGNER_2022 = 'XWUTROPNJLK';
private const PREFIX_CITIZEN_1900 = 'S';
private const PREFIX_CITIZEN_2000 = 'T';
private const PREFIX_FOREIGNER_1900 = 'F';
Expand Down Expand Up @@ -76,6 +77,11 @@ public function is2000(): bool
return in_array($this->id[0], [self::PREFIX_CITIZEN_2000, self::PREFIX_FOREIGNER_2000], true);
}

public function isSeriesM(): bool
{
return self::PREFIX_FOREIGNER_2022 === $this->id[0];
}

public function __toString(): string
{
return $this->id;
Expand All @@ -87,14 +93,15 @@ public function __toString(): string
*/
private function validate(): void
{
$regex = vsprintf('/^([%s%s][\d]{7}[%s])|([%s%s%s][\d]{7}[%s])$/', [
$regex = vsprintf('/^([%s%s][\d]{7}[%s])|([%s%s%s][\d]{7}[%s%s])$/', [
self::PREFIX_CITIZEN_1900,
self::PREFIX_CITIZEN_2000,
self::CHECKSUM_CITIZEN,
self::PREFIX_FOREIGNER_1900,
self::PREFIX_FOREIGNER_2000,
self::PREFIX_FOREIGNER_2022,
self::CHECKSUM_FOREIGNER,
self::CHECKSUM_FOREIGNER_2022
]);

if (preg_match($regex, $this->id) === 0) {
Expand All @@ -113,15 +120,39 @@ private function addChecksum(): void

private function generateChecksum(): string
{
$checksum = $this->is2000() ? 4 : 0;
$checksum = $this->getOffset();

foreach (self::CHECKSUM_WEIGHTS as $key => $weight) {
$checksum += $this->id[$key+1] * $weight; // @phpstan-ignore-line
}

$checksumArr = $this->isForeigner() ? self::CHECKSUM_FOREIGNER : self::CHECKSUM_CITIZEN;
return $this->getChecksumChars()[$checksum % 11];
}

private function getOffset(): int
{
if ($this->isSeriesM()) {
return 3;
}

if ($this->is2000()) {
return 4;
}

return 0;
}

private function getChecksumChars(): string
{
if ($this->isSeriesM()) {
return self::CHECKSUM_FOREIGNER_2022;
}

if ($this->isForeigner()) {
return self::CHECKSUM_FOREIGNER;
}

return $checksumArr[$checksum % 11];
return self::CHECKSUM_CITIZEN;
}

private static function getRandomDate(): DateTimeInterface
Expand Down
17 changes: 10 additions & 7 deletions tests/NRICTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace IonBazan\NRIC\Tests;

use DateTime;
use IonBazan\NRIC\Exception\InvalidChecksumException;
use IonBazan\NRIC\Exception\InvalidFormatException;
use IonBazan\NRIC\NRIC;
Expand Down Expand Up @@ -31,7 +32,7 @@ public function testGenerateFin(): void
*/
public function testGenerateNricWithDate(string $date, string $regex): void
{
$nric = NRIC::generateNric(new \DateTime($date));
$nric = NRIC::generateNric(new DateTime($date));
self::assertInstanceOf(NRIC::class, $nric);
NRIC::fromString($nric->__toString());
self::assertFalse($nric->isForeigner());
Expand All @@ -43,7 +44,7 @@ public function testGenerateNricWithDate(string $date, string $regex): void
*/
public function testGenerateFinWithDate(string $date, string $regex): void
{
$fin = NRIC::generateFin(new \DateTime($date));
$fin = NRIC::generateFin(new DateTime($date));
self::assertInstanceOf(NRIC::class, $fin);
NRIC::fromString($fin->__toString());
self::assertTrue($fin->isForeigner());
Expand All @@ -53,8 +54,8 @@ public function testGenerateFinWithDate(string $date, string $regex): void
public function testFinRandomness(): void
{
$results = '';
for ($i = 0; $i < 10; ++$i) {
$results .= NRIC::generateFin(new \DateTime('1993-12-16'))->__toString();
for ($i = 0; $i < 100; ++$i) {
$results .= NRIC::generateFin(new DateTime('1993-12-16'))->__toString();
}

for ($i = 0; $i < 10; ++$i) {
Expand All @@ -66,7 +67,7 @@ public function testNricRandomness(): void
{
$results = [];
for ($i = 0; $i < 10; ++$i) {
$id = NRIC::generateNric(new \DateTime('1967-12-16'));
$id = NRIC::generateNric(new DateTime('1967-12-16'));
self::assertMatchesRegularExpression('/S0[01][0-9]{5}[A-Z]/', $id->__toString());
$results[] = $id->__toString();
}
Expand All @@ -82,11 +83,12 @@ public function testNricRandomness(): void
/**
* @dataProvider validIdsProvider
*/
public function testValidIds(string $id, bool $is2000, bool $foreigner): void
public function testValidIds(string $id, bool $is2000, bool $foreigner, bool $seriesM = false): void
{
$nric = NRIC::fromString($id);
self::assertSame($is2000, $nric->is2000());
self::assertSame($foreigner, $nric->isForeigner());
self::assertSame($seriesM, $nric->isSeriesM());
}

/**
Expand Down Expand Up @@ -150,6 +152,7 @@ public function validIdsProvider(): iterable
yield ['T5717279C', true, false]; // post-2000 NRIC
yield ['F6470401W', false, true]; // pre-2000 FIN
yield ['G8877699U', true, true]; // post-2000 FIN
yield ['M8877699L', false, true]; // post-2022 FIN
yield ['M5043078W', false, true, true]; // post-2022 FIN
yield ['M2424771J', false, true, true]; // post-2022 FIN with new J checksum letter
}
}

0 comments on commit 4cb22e7

Please sign in to comment.