Norwegian personal number verification in C#

By | 2016.12.07

Code to verify Norwegian personal number (personnummer / fødselsnummer) written in C#.

Obeys 2013 standards as described in this Wikipedia article. Please let me know how if you find bugs / improvements. :)

public static class NorwegianPersonalNumberValidator
{
    // Implementation of Norwegian personal number verification based on specifications outlined on https://no.wikipedia.org/wiki/F%C3%B8dselsnummer
    // Version 1: 2016-12-07 Tedd Hansen, tedd@konge.net

    public class Result
    {
        public enum GenderType
        {
            Male,
            Female
        }

        [Flags]
        public enum NumberType
        {
            Normal = 0,
            D = 1,
            H = 2,
            DAndF = 3, // Not valid
            FH = 4,
            DAndFG = 5, // Not valid
            HAndFH = 6, // Not valid
            DAndHAndFH = 7 // Not valid
        }

        public bool Success { get; set; }
        public string ErrorMessage { get; set; }
        public GenderType Gender { get; set; }
        public NumberType Type { get; set; }
        public DateTime Birthday { get; set; }

        public override string ToString()
        {
            if (Success)
                return $"Success: {Success}, Birthday: {Birthday.ToString("yyyy'-'MM'-'dd")}, Gender: {Gender}, Type: {Type}";
            else
                return $"Success: {Success}, ErrorMessage: {ErrorMessage ?? "N/A"}";
        }
    }

    private struct IndividualNumberControlRange
    {
        public int From;
        public int To;
        public int FromYear;
        public int ToYear;
    }
    private static readonly IndividualNumberControlRange[] IndividualControlRange = new[] {
        new IndividualNumberControlRange() { From = 0,   To = 499, FromYear = 1900, ToYear = 1999 },
        new IndividualNumberControlRange() { From = 500, To = 749, FromYear = 1854, ToYear = 1899 },
        new IndividualNumberControlRange() { From = 500, To = 999, FromYear = 2000, ToYear = 2039 },
        new IndividualNumberControlRange() { From = 900, To = 999, FromYear = 1940, ToYear = 1999 },
    };

    private static Regex PersonalNumberRegex = new Regex(@"^(?<birthdate>(?<day>\d\d)(?<month>\d\d)(?<year>\d\d))(?<individual>\d\d(?<gender>\d))(?<checksum>\d\d)$");
    public static Result Validate(string number)
    {
        var result = new Result();
        // Is it the correct length?
        if (number == null || number.Length != 11)
            return new Result() { Success = false, ErrorMessage = "Feil lengde. Personnummer er 11 tall." };

        // Extract what we need
        var match = PersonalNumberRegex.Match(number);

        // Regex is only looking for 11 numbers, so we can assume any error is because there are non-numbers
        if (!match.Success)
            return new Result() { Success = false, ErrorMessage = "Personummer skal kun inneholde tall." };

        // Parse the numbers
        var day = int.Parse(match.Groups["day"].Value);
        var month = int.Parse(match.Groups["month"].Value);
        var year = int.Parse(match.Groups["year"].Value);
        var individual = int.Parse(match.Groups["individual"].Value);
        var gender = int.Parse(match.Groups["gender"].Value);
        var checksum = int.Parse(match.Groups["checksum"].Value);

        // Compensate for D-numbers
        if (day > 31)
        {
            result.Type |= Result.NumberType.D;
            day -= 40;
        }
        // Compensate for H-numbers
        if (month > 30)
        {
            result.Type |= Result.NumberType.H;
            month -= 40;
        }
        // Compensate for FH-numbers
        if (day >= 80)
        {
            result.Type |= Result.NumberType.FH;
            day = 1;
        }

        // Determine gender (not that we need it)
        result.Gender = gender % 2 != 0 ? Result.GenderType.Male : Result.GenderType.Female;

        int fullYear = 0;

        // Get the range based on the individual range
        var ranges = IndividualControlRange.Where(r => individual >= r.From && individual <= r.To).ToList();
        if (ranges == null || ranges.Count == 0)
            return new Result() { Success = false, ErrorMessage = "Feil i personnummer. (#3)" };

        foreach (var range in ranges)
        {
            // Get full year based on the range
            // Note that if there is a change to range so it crosses centuries then we need more checking here - or else the next check will fail
            var fYear = int.Parse(range.FromYear.ToString().Substring(0, 2) + year.ToString("D2"));

            // Check that we are within allowed range
            if (fYear >= range.FromYear && fYear <= range.ToYear)
            {
                fullYear = fYear;
                break;
            }
        }
        if (fullYear == 0)
            return new Result() { Success = false, ErrorMessage = "Feil i personnummer. (#4)" };

        // Have .Net verify the date, using ISO 8601 should be universally safe
        var iso8601Birthday = fullYear + "-" + month.ToString("D2") + "-" + day.ToString("D2");
        DateTime dtBirthday;
        if (!DateTime.TryParse(iso8601Birthday, out dtBirthday))
            return new Result() { Success = false, ErrorMessage = "Feil i personnummer. (#5)" };
        result.Birthday = dtBirthday;

        // And finally calculate and verify the two checksum digits at the end of the number
        // Convert personal number string to int array
        var n = number.Select(c => int.Parse(c.ToString())).ToArray();

        // Calculate checksum number 1
        int k1 = 11 - (3 * n[0] + 7 * n[1] + 6 * n[2] + 1 * n[3] + 8 * n[4] + 9 * n[5] + 4 * n[6] + 5 * n[7] + 2 * n[8]) % 11;
        if (k1 == 11) k1 = 0;

        if (k1 == 10 || k1 != n[9])
            return new Result() { Success = false, ErrorMessage = "Feil i personnummer. (#6)" };

        // Calculate checksum number 2
        int k2 = 11 - (5 * n[0] + 4 * n[1] + 3 * n[2] + 2 * n[3] + 7 * n[4] + 6 * n[5] + 5 * n[6] + 4 * n[7] + 3 * n[8] + 2 * k1) % 11;
        if (k2 == 11) k2 = 0;

        if (k2 == 10 || k2 != n[10])
            return new Result() { Success = false, ErrorMessage = "Feil i personnummer. (#7)" };

        result.Success = true;
        return result;
    }
}

void Main()
{
    // Test a whole lot of numbers...
    foreach (var pn in new[] { "11056027043", "01073703642", "11123434945", "19073120317", "02012526582", "01052304454", "28023727473", "07052739084", "03126320255", "13106215582", "07032425814", "26082416446", "15060238585", "30121408178", "27096212181", "23021347574", "20064234219", "15081024143", "26045230189", "10082000734", "02012048281", "20046647611", "29096114654", "10026402781", "06122615721", "17093725573", "04121422528", "04024403756", "20101229085", "19092040383", "23053739865", "24101534665", "12021509637", "29011203424", "18100810960", "22040112832", "18101938626", "16021315420", "18060835965", "26050311189", "27094110284", "21112531448", "28015703069", "21122141931", "30062121293", "10095324709", "03129788495", "19123924293", "24020317082", "26085821228", "26093737317", "29123535985", "29034704358", "05079764185", "11022315392", "01070615963", "19123122669", "21072401511", "22054406766", "09104338078", "18062248188", "30030827720", "30112309574", "13052429840", "18071235706", "21031838069", "27124620754", "06024248559", "03033029178", "23123301676", "02034006841", "21072007651", "02125218752", "09036030736", "17021721999", "08023716837", "30031710606", "08116344379", "27116322930", "15071437741", "17093343433", "13030820546", "13044940323", "07052826904", "04072204420", "19116213748", "23109866350", "05012118130", "21094016352", "25095110668", "11123630884", "01114934301", "10022530181", "19056102216", "06025612378", "24074641810", "01040326363", "03034908183", "13032149086", "02114412077", "18080406424", "17072630378", "04092334681", "29080604899", "25123841185", "10113841724", "12065136587", "06030310878", "22020526134", "13102022887", "11115532164", "02072342685", "28114002692", "29104548448", "28052326658", "18116435868", "27044618252", "22024417623", "23022628828", "26034940583", "06051220313", "11042224601", "07083106608", "09039858808", "20022534093", "13035932571", "07081439073", "08053604146", "07073128735", "26096220609", "31075807301", "03022413723", "07063723511", "26066222992", "06084708613", "23013117187", "29073019808", "24062118284", "16032844042", "15123629776", "14123902867", "16035907215", "07095638052", "11034806901", "22126334676", "05031049115", "30092903458", "20034734152", "11063819206", "25055243754", "27035620058", "21051146074", "20094511330", "05059959494", "16053718144", "04124348973", "19112526393", "15065744985", "04106439600", "10121512268", "11020023335", "08081519855", "05075030136", "07056207850", "11115902573", "02042847839", "16100324044", "06075042491", "21079891638", "18070849859", "01015033106", "19072248530", "12023702809", "27080408124", "18044930346", "22100127346", "16119888232", "27033837831", "04042622287", "14034120723", "15014131539", "20035421794", "18115548537", "23064702998", "02024939521", "24030549194", "28094723177", "12030246126", "28063144225", "08052621187", "01110836399", "19011233360", "20101138829", "11053336886", "12114423309", "10070343334", "14093948247", "07043538963", "28014503937", "19070428914", "01110540457", "27061246180", "27056341256", "13105013773", "27036345107", "31082817118", "06115110088", "15105730222", "20052542872", "16096010048", "25122123025", "10094641611", "11034548759", "16103839568", "17100930382", "11094129240", "02022029110", "17106242046", "12106207591", "23074421733", "06123642862", "23126244819", "13124902694", "08081101885", "22015211354", "21013130903", "02122305093", "23010019192", "29044201982", "24100324632", "24013846509", "21024927722", "25106340725", "16083327239", "31034007376", "15121411005", "20015216191", "25113637032", "26075821742", "18060828616", "29051214674", "19126131146", "26043134476", "20124131643", "29075545310", "29094040734", "01055524900", "29076437376", "27040714753", "10096633292", "04099754678", "06081102277", "08069899936", "08021834301", "18059884715", "18045613433", "25096416248", "25120211954", "26040910149", "05086131120", "22033447179", "12045314845", "02122312332", "25096647819", "21030620507", "26101618802", "14073911886", "04115003416", "25111041191", "11069684981", "01033241638", "07114207610", "14090225597", "10011130785", "21084015145", "18065138693", "21070305647", "25100337862", "30060807694", "08091021456", "19052803538", "16126241708", "19029750915", "19052047725", "15065819209", "02013245013", "28082011120", "27086345590", "12080322230", "29115414354", "30091342681", "23104817351", "18050542919", "22063729772", "10075347206", "16084800833", "09024430097", "01024032112", "29041435516", "08115302893", "05074909924", "19099758808", "03062045476", "10094712861", "11053907778", "20062606097", "19102611627", "27106512758", "22059952793", "19060604080", "11111513424", "24030202607", "14072424973", "11110422220", "12010841346", "13126129011", "10123039667", "31053648854", "29101736693", "30093149412", "09060608617", "07111913875", "18030536645", "19123804832", "15022305376", "13031915419", "15106031603", "15012828958", "26040746632", "31125107454", "28021640524", "31034442404", "09101119162", "08072518595", "15122923778", "02052538788", "21104313105", "21095508070", "02114908889", "16102948053", "02013914853", "13041911578", "23101716481", "09076347295", "03084733708", "14029668915", "17014122842", "08121512794", "22010539092", "11013406253", "09115212289", "04059650756", "24094335554", "17084615870", "25055616100", "11023346283", "14036438143", "24096131476", "22033837937", "23124303443", "22094532470", "23075543980", "23030123395", "18109671580", "10081235622", "20012049200", "30011228476", "30093913872", "29091211910", "01061730466", "12095347930", "10010933094", "22091122883", "03056626505", "18072343111", "14084414019", "17062318252", "21042831856", "03062007698", "16021847171", "25031247198", "03055615054", "16061512628", "31059997591", "10090645116", "15075346317", "29115839673", "10031546643", "21053445451", "05125424823", "23044029804", "10066417358", "26041503873", "30015003363", "15090710157", "12011248851", "14041024063", "04081813839", "31036116946", "07114921801", "05074332090", "02073642070", "02123322404", "19102233509", "10039651837", "24122702606", "31082308469", "11113122333", "04109679920", "22020342378", "20114343235", "19083010404", "09020424386", "20084610225", "08014748289", "14125332715", "10066343737", "05026324388", "28079678793", "11086630941", "01121319434", "17092608131", "14112442643", "23112748318", "15064031225", "04045736933", "05031643602", "30084722614", "11012503603", "19124843947", "20070510635", "08024502334", "29112218511", "01080036686", "08066217095", "03103928582", "14054946709", "01040135258", "19080024161", "14034440035", "01056204950", "19021710949", "01014138419", "01085116874", "18034724292", "04079883218", "16084343351", "06112416593", "13102701318", "04100422277", "21075330003", "08026249539", "01060044496", "11103922633", "31030741075", "05062220564", "16085700174", "13044233996", "03064148671", "04104513102", "07055033352", "17112946667", "20113429144", "09106040696", "09113322559", "07079879538", "01064723444", "13044342086", "15101644283", "29035205509", "29083306236", "06105524984", "27080734072", "27011202581", "05119689084", "25055430252", "11113026334", "24043924482", "14020945185", "15122248159", "10065644555", "06109770173", "26031828677", "28113924669", "28021433677", "28112044436", "14064322827", "28090226136", "24123309487", "10122611766", "07036327736", "13033525707", "24114832339", "21074331898", "13120716705", "27112512654", "23083844418", "21094311257", "10036115233", "04129951651" })
        Console.WriteLine(pn + " " + NorwegianPersonalNumberValidator.Validate(pn));

    // Some of these fail because individual-number falls within range specifications modified since 2013
    foreach (var pn in new[] { "10083707696", "13093347166", "25082347197", "26109815046", "22022912694", "18031445155", "16030389615", "07092846657", "31059624876", "04077129877", "26112803822", "08121510198", "23042348880", "27063413890", "09030766986", "06108721853", "14127632058", "12086022409", "28084133568", "24049651844", "28065002622", "19024308416", "07025722390", "29020842316", "06087743258", "20100924359", "29119751495", "25059992409", "07056334208", "09128248274", "01109432780", "02121093742", "05115905424", "28120468229", "29051098151", "10072619469", "29044808119", "07023610501", "11034542645", "05055048314", "02107131240", "20050086985", "16053043643", "03121733217", "12018422660", "17064503634", "27090297444", "03048643964", "05041210674", "05097902315", "07114118537", "17030967038", "22077028521", "08128646702", "17076614105", "06120435254", "19110324122", "17027840000", "10010954903", "04013210964", "05097238490", "16099016802", "06048324272", "05040033272", "24051623408", "23027743323", "25024516515", "25025001357", "30111022298", "25064808071", "26019430072", "24080899544", "07070344453", "13111347470", "18077042063", "05087022635", "24021541008", "08014120557", "21050083443", "03010150123", "12117433941", "31075014360", "18026418417", "14129766973", "24057348689", "09088833322", "22095346603", "04046229113", "06097808951", "17050690660", "03076022568", "21127402415", "12088323473", "25077642032", "02063144269", "02119105563", "02036006202", "01119724933", "06111534096", "18052302559", "13028929743", "05079530214", "20053746420", "26055526353", "09051845177", "04124148184", "28129844541", "30129870172", "15113328697", "25107024606", "09122633672", "05095300180", "08117622828", "23060726175", "17096009437", "19094811778", "07125437154", "23061077731", "14106225746", "20028632305", "10017731896", "08051616759", "30078506187", "15124612672", "30086844466", "01020765271", "06090201628", "30060409702", "15029030755", "19109331425", "09115512231", "07099808351", "11011458817", "26073736543", "28086016372", "14082849697", "18056009597", "28092209042", "31019831259", "01078941584", "26045905194", "01037408367", "22103232094", "15010940661", "25068944891", "21061606855", "30048711215", "10020879985", "28065614792", "18074300746", "18126104185", "27085129002", "15098749907", "19097622577", "30100422081", "02032406192", "16030761387", "10121061746", "26046935372", "06044544812", "07052131469", "19092748777", "30058415517", "22124447589", "03080235630", "18017605891", "18065504304", "24108217045", "06100542887", "10049103900", "20057238306", "20090032314", "22055913432", "16038836483", "20092608078", "14048424105", "12107944298", "04113432638", "09011416605", "29071641145", "06125519383", "02033905079", "13028938599", "02101285076", "05116220212", "08122038867", "25034718558", "05020678226", "13014030241", "17095234623", "20010965579", "19093536447", "27010983466", "19110648394", "11123927267", "22024324951", "03067614983", "21046341837", "04117204702", "01093732827", "28041172276", "25032128125", "24095542197", "13025443424", "28017207638", "18080161642", "23030671635", "08047331561", "01111428634", "30109998849", "01084922087", "26034431158", "11098942590", "17031822874", "07082123649", "16090761750", "08051363400", "20117126642", "17021703117", "11063234178", "09098723339", "15022341739", "06120822541", "24112342946", "27129520855", "19117808524", "06082525856", "11111346013", "08042703002", "04027611996", "17121254816", "27112533988", "16120606577", "17070633388", "05124029475", "23120874934", "08072129297", "07110840777", "08053328998", "16080955913", "28044226865", "29038936725", "19113649243", "06081838729", "22035212713", "11058304818", "07023640095", "08090987667", "20110123184", "18079407514", "16081931228", "15036018264", "28127416615", "22112437731", "20064120132", "28030568100", "03129134282", "31076710131", "08124247005", "10105937852", "25120006331", "23085711822", "14019638856", "25080605040", "21045441234", "25030831694", "22102645220", "21034242078", "28088406925", "03062302916", "05121706425", "14120267133", "26123546724", "06113319486", "07057332578", "22121348458", "21063847345", "30092814455", "31080685591", "22089511511", "22082246456", "23108110864", "24110674194", "23111463719", "31084643028", "07095645997", "13022543460", "29121626428", "01056941033", "07063711467", "20010016895", "25113640955", "11083912148", "07066618832", "06082743640", "14069776281", "09120022854", "31032647467", "04031837907", "24115907529", "02067448599", "23053924190", "19010792439", "09071169925", "26019205796", "07024548702", "15031053983", "13126733814", "24012027482", "01090168424", "01066847973", "05031938165", "02028403286", "15126503943", "15010472919", "23072128702", "28040770676", "21024930545", "13039639771", "23013331464", "19010360970", "07100739873", "02016040846", "04107244854", "30010112199", "12073523212", "27031397443", "29092725837", "12043915779", "17051630602", "17036036923", "20017312228", "10018507302", "01011149614", "03116638648", "30011588596", "05036133696", "20043143469", "16103517579", "22078812501", "30090943290", "16076024557", "13032314214", "01127722451", "13067742286", "13103543295", "09013932902", "25128524013", "06060193299", "24011289807", "20067305524", "15122835054", "24029669356", "23084822086", "14078315823", "06112612422", "09041080970", "29119612398", "14025200404", "10072522856", "08089116164", "25021053611", "08100996279", "01021566809", "30063706783", "10010720023", "29109604184", "03080912322", "30095226589", "16129639737", "18100691481", "14023908175", "17029866368", "03050795127", "19021230394", "10030642371", "18079435844", "25043631112", "06051498257", "20010811784", "12055743760", "01066249707", "06058943590", "01046330877", "21100345363", "20070807875", "04091134026", "17090665367", "24115231568", "14103114560", "08118020597", "28077431847", "21063719770", "14016015793", "23022546740", "30034806329", "14098543903", "17060131201", "20018235812", "08041304813", "17070290324", "17069049745", "01067816605", "06035435660", "17054933878", "22015237574", "26026620642", "22124143857", "13039703429", "22068723942", "02109438661", "20103007144", "07087331320", "17082321217", "02067144352", "29106604442", "26069772985", "05032814044", "30099030913", "22090036061", "15022327140", "23083929723", "05033424223", "14040991329", "13112921103", "06107343030", "25020297789", "16125021208", "15126600140", "03034247667", "17110482302", "24082812268", "16124516874", "13036349292", "04049980979", "23115107623", "17107147616", "06049629069", "25107322577", "21064541983", "07108343340", "07080668958", "29071543992", "18110254675", "17105516770", "25050881507", "13107600978", "06112223981", "25030192780", "21071644542", "10066326344", "10108731791", "21065328922", "05086413088", "20128028227", "11085021658", "24054403859", "15017008169", "25020744874", "19020233403", "11090881630", "25037546316", "07079135377", "22021218662", "22010683347", "25086327093", "27099647286", "25010443876", "28040663306", "13012532533", "17097020663", "27117522143", "08103642265", "24056001588", "03068818612", "12066541817", "06049824457", "12021915813", "24030895300", "29108145682", "07016737369", "30086437577", "29017338127", "03011362329", "27100131795", "20111010758", "08079335771", "11076010082", "18028944272", "12017224601", "03094619784", "17086849011", "05088745258", "19022403403", "29039536941", "11024645470", "27061366984", "14064536177" })
        Console.WriteLine(pn + " " + NorwegianPersonalNumberValidator.Validate(pn));
}

(PS: This code is easy to test in LINQPad. Select C# Program, paste it in and hit run.)

2 thoughts on “Norwegian personal number verification in C#

  1. Jonny Strickley

    Hi Tedd,

    I am trying to apply similar logic to a Teradata SQL query. What I am trying to find out is how the 2nd checksum is calculated if the 1st produces an “X” i.e. the first checksum was 10 which is replaced with an “X” (according to several sources in Wiki, Google etc).

    e.g.

    181251490 becomes 122 when multiplying each digit by the weight and adding the result.

    122/11 = 11 remainder 1

    11-1 = 10

    Check digit is therefore X

    181251490 becomes 181251490X

    So I need to understand how to use mod11 on 181251490X

    (one source: http://www.pgrocer.net/Cis51/mod11.html)

    Thanks,
    Jonny

    Reply
    1. tedd Post author

      Hi

      For Norwegian birth numbers there is no X, only digits. If the mod11 result is 11 its changed to 0, if result is 10 then number is invalid. Second control digit follows the same rule with multipliers vs indexes changed.

      Br,
      Tedd

      Reply

Leave a Reply