summaryrefslogtreecommitdiff
path: root/south-african-id-parser.js
blob: 75637bb7fecfc966dfad13f6858c1353647410c0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
(function (global, factory) {
  'use strict';

  /* eslint-disable no-undef */
  if (typeof exports === 'object' && typeof module !== 'undefined') {
    module.exports = factory();
  } else if (typeof define === 'function' && define.amd) {
    define(factory);
  } else {
    global.saIdParser = factory();
  }
  /* eslint-enable */
}(this, function () {
  'use strict';

  /**
   * Parsing result for a valid South African ID number.
   *
   * @typedef {Object} ValidIDParseResult
   * @property {boolean} isValid - true
   * @property {Date} dateOfBirth - The date of birth from the ID number.
   * @property {boolean} isMale - The sex from the ID number - true if male, false if female.
   * @property {boolean} isFemale - The sex from the ID number - true if female, false if male.
   * @property {boolean} isSouthAfricanCitizen - Citizenship status from the ID
   *   number, true if it indicates South African citizenship.
   */

  /**
   * Parsing result for a invalid South African ID number.
   *
   * @typedef {Object} InvalidIDParseResult
   * @property {boolean} isValid - false
   */

  return {
    /**
     * Validates and ID number and parses out all information from it.
     *
     * This is a combination of the other parsing and validation functions, so
     * refer to their documentation for any details.
     *
     * @function
     * @param {string} idNumber - The ID number to be parsed.
     * @return {ValidIDParseResult|InvalidIDParseResult} An object with all of
     *   the parsing results. If the ID is invalid, the result is an object with
     *   just an `isValid` property set to false.
     *
     * @example
     * var saIdParser = require('south-african-id-parser');
     * var validIdNumber = '9001049818080';
     *
     * var info = saIdParser.parse(validIdNumber);
     * // info === {
     * //   isValid: true,
     * //   dateOfBirth: new Date(1990, 0, 4),
     * //   isMale: true,
     * //   isFemale: false,
     * //   isSouthAfricanCitizen: true
     * // }
     *
     * var invalidIdNumber = '1234567';
     * info = saIdParser.parse(invalidIdNumber);
     * // info === {
     * //   isValid: false
     * // }
     */
    parse: function(idNumber) {
      var isValid = validate(idNumber);
      if (!isValid) {
        return {
          isValid: false
        };
      }

      return {
        isValid: isValid,
        dateOfBirth: parseDateOfBirth(idNumber),
        isMale: parseIsMale(idNumber),
        isFemale: parseIsFemale(idNumber),
        isSouthAfricanCitizen: parseIsSouthAfricanCitizen(idNumber)
      };
    },

    /**
     * Validates an ID number.
     *
     * This includes making sure that it follows the expected 13 digit pattern,
     * checking that the control digit is correct, and checking that the date of
     * birth is a valid date.
     *
     * @function
     * @param {string} idNumber - The ID number to be validated.
     * @return {boolean} True if the ID number is a valid South African ID number.
     *
     * @example
     * var saIdParser = require('south-african-id-parser');
     * var validIdNumber = '9001049818080';
     * var isValid = saIdParser.validate(validIdNumber);
     *
     * // valid === true
     */
    validate: function(idNumber) {
      return validate(idNumber);
    },

    /**
     * Parses the date of birth out of an ID number.
     *
     * Minimal validation of the ID number is performed, requiring only that
     * it's 13 digits long.
     *
     * The date of birth included in the ID number has a two digit year. For
     * example, 90 instead of 1990. This is converted to a full date by
     * comparing the date of birth to the current date, and choosing the century
     * that gives the person the lowest age, while still putting their age in
     * the past.
     *
     * For example, assuming that the current date is 10 December 2015. If the
     * date of birth parsed is 10 December 15, it will be interpreted as 10
     * December 2015. If, on the other hand, the date of birth is parsed as 11
     * December 15, that will be interpreted as 10 December 1915.
     *
     * The date will be in the local timezone, with the time portion set to
     * midnight.
     *
     * @function
     * @param {string} idNumber - The ID number to be parsed.
     * @return {?Date} The date of birth from the ID number, or undefined if the
     *   ID number is not formatted correctly or does not have a valid date of
     *   birth.
     *
     * @example
     * var saIdParser = require('south-african-id-parser');
     * var validIdNumber = '9001049818080';
     * var dateOfBirth = saIdParser.parseDateOfBirth(validIdNumber);
     *
     * // dateOfBirth === new Date(1990, 0, 4)
     */
    parseDateOfBirth: function(idNumber) {
      return parseDateOfBirth(idNumber);
    },

    /**
     * Parses the sex out of the ID number and returns true it is male.
     *
     * Minimal validation of the ID number is performed, requiring only that
     * it's 13 digits long.
     *
     * @function
     * @param {string} idNumber - The ID number to be parsed.
     * @return {?boolean} True if male, false if female. Returns undefined if the
     *   ID number is not a 13 digit number.
     *
     * @example
     * var saIdParser = require('south-african-id-parser');
     * var validIdNumber = '9001049818080';
     * var isMale = saIdParser.parseIsMale(validIdNumber);
     *
     * // isMale === true
     */
    parseIsMale: function(idNumber) {
      return parseIsMale(idNumber);
    },

    /**
     * Parses the sex out of the ID number and returns true it is female.
     *
     * Minimal validation of the ID number is performed, requiring only that
     * it's 13 digits long.
     *
     * @function
     * @param {string} idNumber - The ID number to be parsed.
     * @return {?boolean} True if female, false if male. Returns undefined if the
     *   ID number is not a 13 digit number.
     * @example
     * var saIdParser = require('south-african-id-parser');
     * var validIdNumber = '9001049818080';
     * var isFemale = saIdParser.parseIsFemale(validIdNumber);
     *
     * // isFemale === false
     */
    parseIsFemale: function(idNumber) {
      return parseIsFemale(idNumber);
    },

    /**
     * Parses the citizenship status out of an ID number and returns true if it
     * indicates South African citizen.
     *
     * Minimal validation of the ID number is performed, requiring only that
     * it's 13 digits long.
     *
     * @function
     * @param {string} idNumber - The ID number to be parsed.
     * @return {?boolean} True if the ID number belongs to a South African
     *   citizen. Returns undefined if the ID number is not a 13 digit number.
     *
     * @example
     * var saIdParser = require('south-african-id-parser');
     * var validIdNumber = '9001049818080';
     * var isSouthAfricanCitizen = saIdParser.parseIsSouthAfricanCitizen(validIdNumber);
     *
     * // isSouthAfricanCitizen === true
     */
    parseIsSouthAfricanCitizen: function(idNumber) {
      return parseIsSouthAfricanCitizen(idNumber);
    }
  };

  function validate(idNumber) {
    if (!regexpValidate(idNumber) || !datePartValidate(idNumber) || !controlDigitValidate(idNumber)) {
      return false;
    }

    return true;
  }

  function regexpValidate(idNumber) {
    if (typeof(idNumber) !== 'string') {
      return false;
    }
    var regexp = /^[0-9]{13}$/;
    return regexp.test(idNumber);
  }

  function datePartValidate(idNumber) {
    var dateOfBirth = parseDateOfBirth(idNumber);
    return !!dateOfBirth;
  }

  function controlDigitValidate(idNumber) {
    var checkDigit = parseInt(idNumber[12], 10);

    var oddDigitsSum = 0;

    for (var i = 0; i < idNumber.length - 1; i+=2) {
      oddDigitsSum += parseInt(idNumber[i], 10);
    }
    var evenDigits = '';
    for (var j = 1; j < idNumber.length - 1; j+=2) {
      evenDigits += idNumber[j];
    }
    evenDigits = parseInt(evenDigits, 10);
    evenDigits *= 2;
    evenDigits = '' + evenDigits;

    var sumOfEvenDigits = 0;
    for (var k = 0; k < evenDigits.length; k++) {
      sumOfEvenDigits += parseInt(evenDigits[k], 10);
    }
    var total = sumOfEvenDigits + oddDigitsSum;
    var computedCheckDigit = 10 - (total % 10);

    if (computedCheckDigit === 10) {
      computedCheckDigit = 0;
    }
    return computedCheckDigit === checkDigit;
  }

  function parseDateOfBirth(idNumber) {
    if (!regexpValidate(idNumber)) {
      return undefined;
    }

    // get year, and assume century
    var currentYear = new Date().getFullYear();
    var currentCentury = Math.floor(currentYear/100)*100;
    var yearPart = currentCentury + parseInt(idNumber.substring(0,2), 10);
    if (yearPart > currentYear) {
      yearPart -= 100; //must be last century
    }

    // In Javascript, Jan=0. In ID Numbers, Jan=1.
    var monthPart = parseInt(idNumber.substring(2,4), 10)-1;

    var dayPart = parseInt(idNumber.substring(4,6), 10);

    var dateOfBirth = new Date(yearPart, monthPart, dayPart);

    // validate that date is in a valid range by making sure that it wasn't 'corrected' during construction
    if (!dateOfBirth || dateOfBirth.getFullYear() !== yearPart || dateOfBirth.getMonth() !== monthPart || dateOfBirth.getDate() !== dayPart) {
      return undefined;
    }

    return dateOfBirth;
  }

  function parseIsMale(idNumber) {
    return !parseIsFemale(idNumber);
  }

  function parseIsFemale(idNumber) {
    if (!regexpValidate(idNumber)) {
      return undefined;
    }
    return parseInt(idNumber[6], 10) <= 4;
  }

  function parseIsSouthAfricanCitizen(idNumber) {
    if (!regexpValidate(idNumber)) {
      return undefined;
    }
    return parseInt(idNumber[10], 10) === 0;
  }
}));