Skip to content

Instantly share code, notes, and snippets.

@canhlinh
Last active March 8, 2023 10:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save canhlinh/9b01e9f7e27a4b8ac470a78a8722648d to your computer and use it in GitHub Desktop.
Save canhlinh/9b01e9f7e27a4b8ac470a78a8722648d to your computer and use it in GitHub Desktop.
Validate and generate NRIC/FIN in python
import random
import re
class NRIC:
def __init__(self, value=None):
if isinstance(value, NRIC):
self.nric = value.nric
elif isinstance(value, str):
self.nric = value.strip().upper()
def __repr__(self):
return f"NRIC('{self.nric}')"
def __str__(self):
return self.nric
@property
def value(self):
return self.nric
@property
def length(self):
return len(self.nric)
@property
def first_char(self):
return self.nric[:1] if self.is_correct_format else None
@property
def digits(self):
return self.nric[1:-1] if self.is_correct_format else None
@property
def identifier(self):
return self.nric[-4:] if self.is_correct_format else None
@property
def checksum(self):
return self.nric[-1:] if self.is_correct_format else None
@property
def is_correct_format(self):
return bool(re.match(r'^[STFGM]\d{7}[A-Z]$', self.nric))
@property
def is_valid(self):
try:
return self.validate_checksum()
except (Exception,):
return False
def validate_checksum(self):
is_correct_format, first_char, checksum = self.is_correct_format, self.first_char, self.checksum
digits = self.digits
# Perform basic format check first
if not is_correct_format:
return False
# Valid if the checksum matches the calculated checksum
return checksum == self.calculate_checksum(first_char, digits)
@staticmethod
def generate(first_char=None):
if not first_char or not re.match(r'^[STFGM]$', first_char, re.IGNORECASE):
first_char = random.choice('STFGM')
digits = ''.join(str(random.randint(0, 9)) for _ in range(7))
checksum = NRIC.calculate_checksum(first_char, digits)
return NRIC(first_char + digits + checksum)
@staticmethod
def generate_many(amount=1):
amount = max(1, int(amount))
return [NRIC.generate() for _ in range(amount)]
@staticmethod
def validate(value):
if isinstance(value, list):
return all(item.is_valid if isinstance(item, NRIC) else NRIC(item).is_valid for item in value)
else:
return value.is_valid if isinstance(value, NRIC) else NRIC(value).is_valid
@classmethod
def calculate_checksum(cls, first_char, digits):
# Multiply each of the digits by the respective weights
digits = list(map(int, digits))
digits[0] *= 2
digits[1] *= 7
digits[2] *= 6
digits[3] *= 5
digits[4] *= 4
digits[5] *= 3
digits[6] *= 2
# Calculate total, offset based on first character, and modulus 11
weight = sum(digits)
if first_char == 'T' or first_char == 'G':
offset = 4
elif first_char == 'M':
offset = 3
else:
offset = 0
index = (offset + weight) % 11
# If first_char is M, rotate the index
if first_char == 'M':
index = 10 - index
# Get the value of the index in the checksum array based on first_char
table = cls.get_checksum_table(first_char)
# Return the value of the index in the checksum table
return table[index]
@classmethod
def get_checksum_table(cls, first_char):
checksums = {
'ST': ['J', 'Z', 'I', 'H', 'G', 'F', 'E', 'D', 'C', 'B', 'A'],
'FG': ['X', 'W', 'U', 'T', 'R', 'Q', 'P', 'N', 'M', 'L', 'K'],
'M': ['K', 'L', 'J', 'N', 'P', 'Q', 'R', 'T', 'U', 'W', 'X']
}
key = [k for k in checksums.keys() if first_char in k]
if not key:
raise ValueError(f'Unable to find checksum table for "{first_char}"')
return checksums[key[0]]
if __name__ == "__main__":
for i in range(300):
nric = NRIC.generate()
print(f'{nric} is valid NRIC {NRIC(nric).is_valid}')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment