Skip to content

Instantly share code, notes, and snippets.

@jramb
Created October 27, 2023 06:43
Show Gist options
  • Save jramb/9784012fe58c158267021be6d44452a6 to your computer and use it in GitHub Desktop.
Save jramb/9784012fe58c158267021be6d44452a6 to your computer and use it in GitHub Desktop.
Luhn validation of swedish personal id number
/**
* @NApiVersion 2.1
*
* @Description nic_validate_se_persnr_mod.js
* @Solution Generic module
*
* @Copyright 2023 Noresca IT Consulting AB
* @Author jorg.ramb <jorg.ramb@noresca.se>
*
* # Explanation
* "It should be SSN" - no, the SSN is something used in the USA, it is not a good translation actually and Skatteverket uses a different term
*
* Below validation applies to several things at the same time. In Swedish:
* - Personnummer = Personal Identification Number
* - Organisationsnummer = Company Registration Number
* - Samordningsnummer = Co-ordination number
* - A co-ordination number is an identification for people who are not or have not been registered in Sweden.
* The purpose of co-ordination numbers is so that public agencies
* and other functions in society are able to identify people even if they are not registered in Sweden.
*
* So... f-it, I am writing "checkLuhn", because that guy deserves some credit.
*
* Sources:
* https://www.mira.se/knowledge-base/vad-heter-personnummer-och-samordningsnummer-pa-engelska/#:~:text=%E2%80%9CSocial%20security%20number%E2%80%9D%20%C3%A4r%20snarast,faktiskt%20menar%20ett%20svenskt%20personnummer.&text=A%20co%2Dordination%20number%20is,not%20been%20registered%20in%20Sweden.
* https://www.skatteverket.se/servicelankar/otherlanguages/inenglishengelska/individualsandemployees/livinginsweden/personalidentitynumbers.4.2cf1b5cd163796a5c8b4295.html
*/
define( [],
() => {
/**
* Handles (formats/validates) a Swedish "Personnummer" (aka slightly missleading "SSN"). /jramb
* @typedef {Object} PersnrParsedResult
* @property {boolean} valid - valid both in format and checksum
* @property {string} type - ORG or PERSON (samordningsnumber also are PERSON!)
* @property {boolean} samordningsnummer
* @property {boolean} wellformed - format looks right (not necessarily valid)
* @property {string[]} parts - list of parts of the personnummer
*
* @param {string} persnrStr - personal/coordination/company number as string
* @return {PersnrParsedResult} - the parsed result
*/
function checkLuhn(persnrStr) {
if (typeof persnrStr !== "string") return {};
const nrRE = /^(19|20)?([0-9]{2})([0-9]{2})([0-9]{2})(-|\+)?([0-9]{4})$/;
let parts = persnrStr.match(nrRE);
// parts: 0=all, 1= century/null, 2=year*(2 digit), 3=month*, 4=day*, 5=delimiter/null, 6=lastfour
let wellformed = !!parts;
if (!wellformed) {
return { wellformed, valid: false };
}
parts[1] = parts[1] || ''; //convert null to empty string
parts[5] = parts[5] || '';
const cleaned = persnrStr.replace(/[+-]/g,''); // only light cleaning
const luhnCheck = numStr => {
const dub = [0, 2, 4, 6, 8, 1, 3, 5, 7, 9];
let sum=0;
let f = numStr.length%2===0;
(numStr+'').split('').forEach(c => {
let val = parseInt(c);
sum += f?dub[val]:val;
f = !f;
});
return sum%10 === 0;
}
let century = parts[1]; // guessed century, if empty
const centuryMissing = !century;
if (centuryMissing) {
// completely arbitrary: xx < cutoff => 20xx, xx > cutoff => 19xx
const cutoff = 11;
century = (parseInt(parts[2])<cutoff?'20':'19');
}
const type = parseInt(parts[3])>=20?'ORG':'PERSON';
let extraPerson = {};
if (type==='PERSON') {
const samordningsnummer = parseInt(parts[4])>60;
const dayAdd = 60;
extraPerson = {
samordningsnummer,
birthdate: new Date(century + parts[2], parts[3] - 1, samordningsnummer ? (parts[4] - dayAdd) : parts[4]),
female: type==='PERSON' && parseInt(parts[6].substring(2,3))%2===0,
}
}
return {
parts, cleaned, century, centuryMissing, wellformed, type,
cleaningNecessary: persnrStr !== cleaned,
masked: parts[1]+parts[2]+'****'+(parts[5]?parts[5]:'-')+parts[6],
formatted: century+parts[2]+parts[3]+parts[4]+(parts[5]?parts[5]:'-')+parts[6],
valid : luhnCheck(parts[2]+parts[3]+parts[4]+parts[6]),
...extraPerson
};
}
return { checkLuhn };
}
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment