From 3c612a335fd340c69003d165b478a6b4c80ab9d3 Mon Sep 17 00:00:00 2001 From: Antariksh Date: Mon, 14 Jun 2021 16:32:14 +0800 Subject: [PATCH 1/2] refactor: simplify isUenValid logic --- src/shared/util/uen-validation.ts | 230 ++++++++++++------------------ 1 file changed, 95 insertions(+), 135 deletions(-) diff --git a/src/shared/util/uen-validation.ts b/src/shared/util/uen-validation.ts index 1d132f19b8..880c6507a0 100644 --- a/src/shared/util/uen-validation.ts +++ b/src/shared/util/uen-validation.ts @@ -1,164 +1,124 @@ +import validator from 'validator' + /** - * Validates whether a provided string value adheres to the UIN/FIN format - * as provided on the Singapore Government's National Registration Identity Card. - * @param value The value to be validated + * Validate entity-type indicators, as per + * https://www.uen.gov.sg/ueninternet/faces/pages/admin/aboutUEN.jspx */ -export const isUenValid = (value: string): boolean => { - return validateUEN(value) -} +const VALID_ENTITY_TYPE_INDICATORS = new Set([ + 'LP', + 'LL', + 'FC', + 'PF', + 'RF', + 'MQ', + 'MM', + 'NB', + 'CC', + 'CS', + 'MB', + 'FM', + 'GS', + 'GA', + 'GB', + 'DP', + 'CP', + 'NR', + 'CM', + 'CD', + 'MD', + 'HS', + 'VH', + 'CH', + 'MH', + 'CL', + 'XL', + 'CX', + 'RP', + 'TU', + 'TC', + 'FB', + 'FN', + 'PA', + 'PB', + 'SS', + 'MC', + 'SM', +]) /** - * validates UEN of businesses in Singapore - * https://www.uen.gov.sg/ueninternet/faces/pages/admin/aboutUEN.jspx - * https://gist.github.com/mervintankw/90d5660c6ab03a83ddf77fa8199a0e52 - * @param {string} uen - * @returns {boolean} + * Helper to check whether a string is numeric + * @param s String + * @returns True if string is numeric */ -function validateUEN(uen: string) { - const entityTypeIndicator = [ - 'LP', - 'LL', - 'FC', - 'PF', - 'RF', - 'MQ', - 'MM', - 'NB', - 'CC', - 'CS', - 'MB', - 'FM', - 'GS', - 'GA', - 'GB', - 'DP', - 'CP', - 'NR', - 'CM', - 'CD', - 'MD', - 'HS', - 'VH', - 'CH', - 'MH', - 'CL', - 'XL', - 'CX', - 'RP', - 'TU', - 'TC', - 'FB', - 'FN', - 'PA', - 'PB', - 'SS', - 'MC', - 'SM', - ] +const isNumeric = (s: string): boolean => !isNaN(Number(s)) - // check that uen is not empty - if (!uen || String(uen) === '') { - return false - } +/** + * Validates whether a provided string value adheres to the UIN/FIN format + * as provided on the Singapore Government's National Registration Identity Card. + * @param value The value to be validated + */ +export const isUenValid = (uen: string): boolean => { + // allow lowercase strings + uen = uen.toUpperCase() // check if uen is 9 or 10 digits if (uen.length < 9 || uen.length > 10) { return false } - uen = uen.toUpperCase() - const uenStrArray: any[] = uen.split('') - // (A) Businesses registered with ACRA - if (uenStrArray.length === 9) { + if (uen.length === 9) { // check that last character is a letter - if (!isNaN(uenStrArray[uenStrArray.length - 1])) { + const lastChar = uen[uen.length - 1] + if (!validator.isAlpha(lastChar)) { return false } - for (let i = 0; i < uenStrArray.length - 1; i++) { - // check that first 8 letters are all numbers - if (isNaN(uenStrArray[i])) { - return false - } + // check that first 8 letters are all numbers + const first8Chars = uen.slice(0, 8) + if (!isNumeric(first8Chars)) { + return false } // (A) Businesses registered with ACRA (SUCCESS) return true - } else if (uenStrArray.length === 10) { - // check that last character is a letter - if (!isNaN(uenStrArray[uenStrArray.length - 1])) { - return false - } - - // (B) Local companies registered with ACRA - if ( - !isNaN(uenStrArray[0]) && - !isNaN(uenStrArray[1]) && - !isNaN(uenStrArray[2]) && - !isNaN(uenStrArray[3]) - ) { - // check that 5th to 9th letters are all numbers - if ( - !isNaN(uenStrArray[4]) && - !isNaN(uenStrArray[5]) && - !isNaN(uenStrArray[6]) && - !isNaN(uenStrArray[7]) && - !isNaN(uenStrArray[8]) - ) { - // (B) Local companies registered with ACRA (SUCCESS) - return true - } else { - return false - } - } - // (C) All other entities which will be issued new UEN - else { - // check that 1st letter is either T or S or R - if ( - uenStrArray[0] !== 'T' && - uenStrArray[0] !== 'S' && - uenStrArray[0] !== 'R' - ) { - return false - } + } - // check that 2nd and 3rd letters are numbers only - if (isNaN(uenStrArray[1]) || isNaN(uenStrArray[2])) { - return false - } + // Length is 10 + // check that last character is a letter + const lastChar = uen[uen.length - 1] + if (!validator.isAlpha(lastChar)) { + return false + } - // check that 4th letter is an alphabet - if (!isNaN(uenStrArray[3])) { - return false - } + // (B) Local companies registered with ACRA + const first4Chars = uen.slice(0, 4) + if (isNumeric(first4Chars)) { + // if first 4 are digits then next 5 must be digits too + const next5Chars = uen.slice(4, 9) + return isNumeric(next5Chars) + } - // check entity-type indicator - let entityTypeMatch = false - const entityType = String(uenStrArray[3]) + String(uenStrArray[4]) - for (let i = 0; i < entityTypeIndicator.length; i++) { - if (String(entityTypeIndicator[i]) === String(entityType)) { - entityTypeMatch = true - } - } - if (!entityTypeMatch) { - return false - } + // (C) All other entities which will be issued new UEN + // check that 1st letter is either T or S or R + const firstChar = uen[0] + if (!['T', 'S', 'R'].includes(firstChar)) { + return false + } - // check that 6th to 9th letters are numbers only - if ( - isNaN(uenStrArray[5]) || - isNaN(uenStrArray[6]) || - isNaN(uenStrArray[7]) || - isNaN(uenStrArray[8]) - ) { - return false - } + // check that 2nd and 3rd letters are numbers only + const chars2And3 = uen.slice(1, 3) + if (!isNumeric(chars2And3)) { + return false + } - // (C) All other entities which will be issued new UEN (SUCCESS) - return true - } + // check entity-type indicator + const entityTypeIndicator = uen.slice(3, 5) + if (!VALID_ENTITY_TYPE_INDICATORS.has(entityTypeIndicator)) { + return false } - return false + // check that 6th to 9th letters are numbers only + const chars5To8 = uen.slice(5, 9) + return isNumeric(chars5To8) } From 7c66b8b4e8dc20bd0849e37efca9f63c239e20ca Mon Sep 17 00:00:00 2001 From: Antariksh Date: Tue, 15 Jun 2021 08:48:52 +0800 Subject: [PATCH 2/2] fix: use stricter regex matching --- src/shared/util/uen-validation.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/shared/util/uen-validation.ts b/src/shared/util/uen-validation.ts index 880c6507a0..32c866749e 100644 --- a/src/shared/util/uen-validation.ts +++ b/src/shared/util/uen-validation.ts @@ -1,5 +1,3 @@ -import validator from 'validator' - /** * Validate entity-type indicators, as per * https://www.uen.gov.sg/ueninternet/faces/pages/admin/aboutUEN.jspx @@ -50,7 +48,14 @@ const VALID_ENTITY_TYPE_INDICATORS = new Set([ * @param s String * @returns True if string is numeric */ -const isNumeric = (s: string): boolean => !isNaN(Number(s)) +const isNumeric = (s: string): boolean => !!s.match(/^[0-9]+$/) + +/** + * Helper to check whether a string is alphabetic + * @param s string + * @returns True if string is alphabetic + */ +const isAlphabetic = (s: string): boolean => !!s.match(/^[a-zA-Z]+$/) /** * Validates whether a provided string value adheres to the UIN/FIN format @@ -70,7 +75,7 @@ export const isUenValid = (uen: string): boolean => { if (uen.length === 9) { // check that last character is a letter const lastChar = uen[uen.length - 1] - if (!validator.isAlpha(lastChar)) { + if (!isAlphabetic(lastChar)) { return false } @@ -87,7 +92,7 @@ export const isUenValid = (uen: string): boolean => { // Length is 10 // check that last character is a letter const lastChar = uen[uen.length - 1] - if (!validator.isAlpha(lastChar)) { + if (!isAlphabetic(lastChar)) { return false }