zxcvbn-ruby 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -3
  3. data/README.md +5 -5
  4. data/lib/zxcvbn/clock.rb +10 -0
  5. data/lib/zxcvbn/password_strength.rb +3 -3
  6. data/lib/zxcvbn/version.rb +3 -1
  7. metadata +9 -77
  8. data/.gitignore +0 -18
  9. data/.rspec +0 -1
  10. data/.travis.yml +0 -12
  11. data/CODE_OF_CONDUCT.md +0 -130
  12. data/Gemfile +0 -10
  13. data/Guardfile +0 -26
  14. data/Rakefile +0 -22
  15. data/spec/dictionary_ranker_spec.rb +0 -12
  16. data/spec/feedback_giver_spec.rb +0 -212
  17. data/spec/matchers/date_spec.rb +0 -109
  18. data/spec/matchers/dictionary_spec.rb +0 -30
  19. data/spec/matchers/digits_spec.rb +0 -15
  20. data/spec/matchers/l33t_spec.rb +0 -87
  21. data/spec/matchers/repeat_spec.rb +0 -18
  22. data/spec/matchers/sequences_spec.rb +0 -21
  23. data/spec/matchers/spatial_spec.rb +0 -20
  24. data/spec/matchers/year_spec.rb +0 -15
  25. data/spec/omnimatch_spec.rb +0 -24
  26. data/spec/scorer_spec.rb +0 -5
  27. data/spec/scoring/crack_time_spec.rb +0 -106
  28. data/spec/scoring/entropy_spec.rb +0 -216
  29. data/spec/scoring/math_spec.rb +0 -135
  30. data/spec/spec_helper.rb +0 -54
  31. data/spec/support/js_helpers.rb +0 -35
  32. data/spec/support/js_source/adjacency_graphs.js +0 -8
  33. data/spec/support/js_source/compiled.js +0 -1188
  34. data/spec/support/js_source/frequency_lists.js +0 -10
  35. data/spec/support/js_source/init.coffee +0 -63
  36. data/spec/support/js_source/init.js +0 -95
  37. data/spec/support/js_source/matching.coffee +0 -444
  38. data/spec/support/js_source/matching.js +0 -685
  39. data/spec/support/js_source/scoring.coffee +0 -270
  40. data/spec/support/js_source/scoring.js +0 -390
  41. data/spec/support/matcher.rb +0 -35
  42. data/spec/tester_spec.rb +0 -99
  43. data/spec/zxcvbn_spec.rb +0 -24
  44. data/zxcvbn-ruby.gemspec +0 -31
@@ -1,63 +0,0 @@
1
-
2
- ranked_user_inputs_dict = {}
3
-
4
- # initialize matcher lists
5
- DICTIONARY_MATCHERS = [
6
- build_dict_matcher('passwords', build_ranked_dict(passwords)),
7
- build_dict_matcher('english', build_ranked_dict(english)),
8
- build_dict_matcher('male_names', build_ranked_dict(male_names)),
9
- build_dict_matcher('female_names', build_ranked_dict(female_names)),
10
- build_dict_matcher('surnames', build_ranked_dict(surnames)),
11
- build_dict_matcher('user_inputs', ranked_user_inputs_dict),
12
- ]
13
-
14
- MATCHERS = DICTIONARY_MATCHERS.concat [
15
- l33t_match,
16
- digits_match, year_match, date_match,
17
- repeat_match, sequence_match,
18
- spatial_match
19
- ]
20
-
21
- GRAPHS =
22
- 'qwerty': qwerty
23
- 'dvorak': dvorak
24
- 'keypad': keypad
25
- 'mac_keypad': mac_keypad
26
-
27
- # on qwerty, 'g' has degree 6, being adjacent to 'ftyhbv'. '\' has degree 1.
28
- # this calculates the average over all keys.
29
- calc_average_degree = (graph) ->
30
- average = 0
31
- for key, neighbors of graph
32
- average += (n for n in neighbors when n).length
33
- average /= (k for k,v of graph).length
34
- average
35
-
36
- KEYBOARD_AVERAGE_DEGREE = calc_average_degree(qwerty)
37
- KEYPAD_AVERAGE_DEGREE = calc_average_degree(keypad) # slightly different for keypad/mac keypad, but close enough
38
-
39
- KEYBOARD_STARTING_POSITIONS = (k for k,v of qwerty).length
40
- KEYPAD_STARTING_POSITIONS = (k for k,v of keypad).length
41
-
42
- time = -> (new Date()).getTime()
43
-
44
- # now that frequency lists are loaded, replace zxcvbn stub function.
45
- zxcvbn = (password, user_inputs) ->
46
- start = time()
47
- if user_inputs?
48
- for i in [0...user_inputs.length]
49
- # update ranked_user_inputs_dict.
50
- # i+1 instead of i b/c rank starts at 1.
51
- ranked_user_inputs_dict[user_inputs[i]] = i + 1
52
- matches = omnimatch password
53
- result = minimum_entropy_match_sequence password, matches
54
- result.calc_time = time() - start
55
- result
56
-
57
- # make zxcvbn function globally available
58
- # via window or exports object, depending on the environment
59
- if window?
60
- window.zxcvbn = zxcvbn
61
- window.zxcvbn_load_hook?() # run load hook from user, if defined
62
- else if exports?
63
- exports.zxcvbn = zxcvbn
@@ -1,95 +0,0 @@
1
- // Generated by CoffeeScript 1.3.3
2
- var DICTIONARY_MATCHERS, GRAPHS, KEYBOARD_AVERAGE_DEGREE, KEYBOARD_STARTING_POSITIONS, KEYPAD_AVERAGE_DEGREE, KEYPAD_STARTING_POSITIONS, MATCHERS, calc_average_degree, k, ranked_user_inputs_dict, time, v, zxcvbn;
3
-
4
- ranked_user_inputs_dict = {};
5
-
6
- DICTIONARY_MATCHERS = [build_dict_matcher('passwords', build_ranked_dict(passwords)), build_dict_matcher('english', build_ranked_dict(english)), build_dict_matcher('male_names', build_ranked_dict(male_names)), build_dict_matcher('female_names', build_ranked_dict(female_names)), build_dict_matcher('surnames', build_ranked_dict(surnames)), build_dict_matcher('user_inputs', ranked_user_inputs_dict)];
7
-
8
- MATCHERS = DICTIONARY_MATCHERS.concat([l33t_match, digits_match, year_match, date_match, repeat_match, sequence_match, spatial_match]);
9
-
10
- GRAPHS = {
11
- 'qwerty': qwerty,
12
- 'dvorak': dvorak,
13
- 'keypad': keypad,
14
- 'mac_keypad': mac_keypad
15
- };
16
-
17
- calc_average_degree = function(graph) {
18
- var average, k, key, n, neighbors, v;
19
- average = 0;
20
- for (key in graph) {
21
- neighbors = graph[key];
22
- average += ((function() {
23
- var _i, _len, _results;
24
- _results = [];
25
- for (_i = 0, _len = neighbors.length; _i < _len; _i++) {
26
- n = neighbors[_i];
27
- if (n) {
28
- _results.push(n);
29
- }
30
- }
31
- return _results;
32
- })()).length;
33
- }
34
- average /= ((function() {
35
- var _results;
36
- _results = [];
37
- for (k in graph) {
38
- v = graph[k];
39
- _results.push(k);
40
- }
41
- return _results;
42
- })()).length;
43
- return average;
44
- };
45
-
46
- KEYBOARD_AVERAGE_DEGREE = calc_average_degree(qwerty);
47
-
48
- KEYPAD_AVERAGE_DEGREE = calc_average_degree(keypad);
49
-
50
- KEYBOARD_STARTING_POSITIONS = ((function() {
51
- var _results;
52
- _results = [];
53
- for (k in qwerty) {
54
- v = qwerty[k];
55
- _results.push(k);
56
- }
57
- return _results;
58
- })()).length;
59
-
60
- KEYPAD_STARTING_POSITIONS = ((function() {
61
- var _results;
62
- _results = [];
63
- for (k in keypad) {
64
- v = keypad[k];
65
- _results.push(k);
66
- }
67
- return _results;
68
- })()).length;
69
-
70
- time = function() {
71
- return (new Date()).getTime();
72
- };
73
-
74
- zxcvbn = function(password, user_inputs) {
75
- var i, matches, result, start, _i, _ref;
76
- start = time();
77
- if (user_inputs != null) {
78
- for (i = _i = 0, _ref = user_inputs.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
79
- ranked_user_inputs_dict[user_inputs[i]] = i + 1;
80
- }
81
- }
82
- matches = omnimatch(password);
83
- result = minimum_entropy_match_sequence(password, matches);
84
- result.calc_time = time() - start;
85
- return result;
86
- };
87
-
88
- if (typeof window !== "undefined" && window !== null) {
89
- window.zxcvbn = zxcvbn;
90
- if (typeof window.zxcvbn_load_hook === "function") {
91
- window.zxcvbn_load_hook();
92
- }
93
- } else if (typeof exports !== "undefined" && exports !== null) {
94
- exports.zxcvbn = zxcvbn;
95
- }
@@ -1,444 +0,0 @@
1
-
2
- empty = (obj) -> (k for k of obj).length == 0
3
- extend = (lst, lst2) -> lst.push.apply lst, lst2
4
- translate = (string, chr_map) -> (chr_map[chr] or chr for chr in string.split('')).join('')
5
-
6
- # ------------------------------------------------------------------------------
7
- # omnimatch -- combine everything ----------------------------------------------
8
- # ------------------------------------------------------------------------------
9
-
10
- omnimatch = (password) ->
11
- matches = []
12
- for matcher in MATCHERS
13
- extend matches, matcher(password)
14
- matches.sort (match1, match2) ->
15
- (match1.i - match2.i) or (match1.j - match2.j)
16
-
17
- #-------------------------------------------------------------------------------
18
- # dictionary match (common passwords, english, last names, etc) ----------------
19
- #-------------------------------------------------------------------------------
20
-
21
- dictionary_match = (password, ranked_dict) ->
22
- result = []
23
- len = password.length
24
- password_lower = password.toLowerCase()
25
- for i in [0...len]
26
- for j in [i...len]
27
- if password_lower[i..j] of ranked_dict
28
- word = password_lower[i..j]
29
- rank = ranked_dict[word]
30
- result.push(
31
- pattern: 'dictionary'
32
- i: i
33
- j: j
34
- token: password[i..j]
35
- matched_word: word
36
- rank: rank
37
- )
38
- result
39
-
40
- build_ranked_dict = (unranked_list) ->
41
- result = {}
42
- i = 1 # rank starts at 1, not 0
43
- for word in unranked_list
44
- result[word] = i
45
- i += 1
46
- result
47
-
48
- build_dict_matcher = (dict_name, ranked_dict) ->
49
- (password) ->
50
- matches = dictionary_match(password, ranked_dict)
51
- match.dictionary_name = dict_name for match in matches
52
- matches
53
-
54
- #-------------------------------------------------------------------------------
55
- # dictionary match with common l33t substitutions ------------------------------
56
- #-------------------------------------------------------------------------------
57
-
58
- l33t_table =
59
- a: ['4', '@']
60
- b: ['8']
61
- c: ['(', '{', '[', '<']
62
- e: ['3']
63
- g: ['6', '9']
64
- i: ['1', '!', '|']
65
- l: ['1', '|', '7']
66
- o: ['0']
67
- s: ['$', '5']
68
- t: ['+', '7']
69
- x: ['%']
70
- z: ['2']
71
-
72
- # makes a pruned copy of l33t_table that only includes password's possible substitutions
73
- relevent_l33t_subtable = (password) ->
74
- password_chars = {}
75
- for chr in password.split('')
76
- password_chars[chr] = true
77
- filtered = {}
78
- for letter, subs of l33t_table
79
- relevent_subs = (sub for sub in subs when sub of password_chars)
80
- if relevent_subs.length > 0
81
- filtered[letter] = relevent_subs
82
- filtered
83
-
84
- # returns the list of possible 1337 replacement dictionaries for a given password
85
- enumerate_l33t_subs = (table) ->
86
- keys = (k for k of table)
87
- subs = [[]]
88
-
89
- dedup = (subs) ->
90
- deduped = []
91
- members = {}
92
- for sub in subs
93
- assoc = ([k,v] for k,v in sub)
94
- assoc.sort()
95
- label = (k+','+v for k,v in assoc).join('-')
96
- unless label of members
97
- members[label] = true
98
- deduped.push sub
99
- deduped
100
-
101
- helper = (keys) ->
102
- return if not keys.length
103
- first_key = keys[0]
104
- rest_keys = keys[1..]
105
- next_subs = []
106
- for l33t_chr in table[first_key]
107
- for sub in subs
108
- dup_l33t_index = -1
109
- for i in [0...sub.length]
110
- if sub[i][0] == l33t_chr
111
- dup_l33t_index = i
112
- break
113
- if dup_l33t_index == -1
114
- sub_extension = sub.concat [[l33t_chr, first_key]]
115
- next_subs.push sub_extension
116
- else
117
- sub_alternative = sub.slice(0)
118
- sub_alternative.splice(dup_l33t_index, 1)
119
- sub_alternative.push [l33t_chr, first_key]
120
- next_subs.push sub
121
- next_subs.push sub_alternative
122
- subs = dedup next_subs
123
- helper(rest_keys)
124
-
125
- helper(keys)
126
- sub_dicts = [] # convert from assoc lists to dicts
127
- for sub in subs
128
- sub_dict = {}
129
- for [l33t_chr, chr] in sub
130
- sub_dict[l33t_chr] = chr
131
- sub_dicts.push sub_dict
132
- sub_dicts
133
-
134
- l33t_match = (password) ->
135
- matches = []
136
- for sub in enumerate_l33t_subs relevent_l33t_subtable password
137
- break if empty sub # corner case: password has no relevent subs.
138
- for matcher in DICTIONARY_MATCHERS
139
- subbed_password = translate password, sub
140
- for match in matcher(subbed_password)
141
- token = password[match.i..match.j]
142
- if token.toLowerCase() == match.matched_word
143
- continue # only return the matches that contain an actual substitution
144
- match_sub = {} # subset of mappings in sub that are in use for this match
145
- for subbed_chr, chr of sub when token.indexOf(subbed_chr) != -1
146
- match_sub[subbed_chr] = chr
147
- match.l33t = true
148
- match.token = token
149
- match.sub = match_sub
150
- match.sub_display = ("#{k} -> #{v}" for k,v of match_sub).join(', ')
151
- matches.push match
152
- matches
153
-
154
- # ------------------------------------------------------------------------------
155
- # spatial match (qwerty/dvorak/keypad) -----------------------------------------
156
- # ------------------------------------------------------------------------------
157
-
158
- spatial_match = (password) ->
159
- matches = []
160
- for graph_name, graph of GRAPHS
161
- extend matches, spatial_match_helper(password, graph, graph_name)
162
- matches
163
-
164
- spatial_match_helper = (password, graph, graph_name) ->
165
- result = []
166
- i = 0
167
- while i < password.length - 1
168
- j = i + 1
169
- last_direction = null
170
- turns = 0
171
- shifted_count = 0
172
- loop
173
- prev_char = password.charAt(j-1)
174
- found = false
175
- found_direction = -1
176
- cur_direction = -1
177
- adjacents = graph[prev_char] or []
178
- # consider growing pattern by one character if j hasn't gone over the edge.
179
- if j < password.length
180
- cur_char = password.charAt(j)
181
- for adj in adjacents
182
- cur_direction += 1
183
- if adj and adj.indexOf(cur_char) != -1
184
- found = true
185
- found_direction = cur_direction
186
- if adj.indexOf(cur_char) == 1
187
- # index 1 in the adjacency means the key is shifted, 0 means unshifted: A vs a, % vs 5, etc.
188
- # for example, 'q' is adjacent to the entry '2@'. @ is shifted w/ index 1, 2 is unshifted.
189
- shifted_count += 1
190
- if last_direction != found_direction
191
- # adding a turn is correct even in the initial case when last_direction is null:
192
- # every spatial pattern starts with a turn.
193
- turns += 1
194
- last_direction = found_direction
195
- break
196
- # if the current pattern continued, extend j and try to grow again
197
- if found
198
- j += 1
199
- # otherwise push the pattern discovered so far, if any...
200
- else
201
- if j - i > 2 # don't consider length 1 or 2 chains.
202
- result.push
203
- pattern: 'spatial'
204
- i: i
205
- j: j-1
206
- token: password[i...j]
207
- graph: graph_name
208
- turns: turns
209
- shifted_count: shifted_count
210
- # ...and then start a new search for the rest of the password.
211
- i = j
212
- break
213
- result
214
-
215
- #-------------------------------------------------------------------------------
216
- # repeats (aaa) and sequences (abcdef) -----------------------------------------
217
- #-------------------------------------------------------------------------------
218
-
219
- repeat_match = (password) ->
220
- result = []
221
- i = 0
222
- while i < password.length
223
- j = i + 1
224
- loop
225
- [prev_char, cur_char] = password[j-1..j]
226
- if password.charAt(j-1) == password.charAt(j)
227
- j += 1
228
- else
229
- if j - i > 2 # don't consider length 1 or 2 chains.
230
- result.push
231
- pattern: 'repeat'
232
- i: i
233
- j: j-1
234
- token: password[i...j]
235
- repeated_char: password.charAt(i)
236
- break
237
- i = j
238
- result
239
-
240
- SEQUENCES =
241
- lower: 'abcdefghijklmnopqrstuvwxyz'
242
- upper: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
243
- digits: '01234567890'
244
-
245
- sequence_match = (password) ->
246
- result = []
247
- i = 0
248
- while i < password.length
249
- j = i + 1
250
- seq = null # either lower, upper, or digits
251
- seq_name = null
252
- seq_direction = null # 1 for ascending seq abcd, -1 for dcba
253
- for seq_candidate_name, seq_candidate of SEQUENCES
254
- [i_n, j_n] = (seq_candidate.indexOf(chr) for chr in [password.charAt(i), password.charAt(j)])
255
- if i_n > -1 and j_n > -1
256
- direction = j_n - i_n
257
- if direction in [1, -1]
258
- seq = seq_candidate
259
- seq_name = seq_candidate_name
260
- seq_direction = direction
261
- break
262
- if seq
263
- loop
264
- [prev_char, cur_char] = password[j-1..j]
265
- [prev_n, cur_n] = (seq_candidate.indexOf(chr) for chr in [prev_char, cur_char])
266
- # Bug fix. 'ba+' was falsly being reported as a sequence due to 1 - null working in JS.
267
- # TODO: Submit PR to zxcvbn.js
268
- if !!cur_n && (cur_n - prev_n == seq_direction)
269
- j += 1
270
- else
271
- if j - i > 2 # don't consider length 1 or 2 chains.
272
- result.push
273
- pattern: 'sequence'
274
- i: i
275
- j: j-1
276
- token: password[i...j]
277
- sequence_name: seq_name
278
- sequence_space: seq.length
279
- ascending: seq_direction == 1
280
- break
281
- i = j
282
- result
283
-
284
- #-------------------------------------------------------------------------------
285
- # digits, years, dates ---------------------------------------------------------
286
- #-------------------------------------------------------------------------------
287
-
288
- repeat = (chr, n) -> (chr for i in [1..n]).join('')
289
-
290
- findall = (password, rx) ->
291
- matches = []
292
- loop
293
- match = password.match rx
294
- break if not match
295
- match.i = match.index
296
- match.j = match.index + match[0].length - 1
297
- matches.push match
298
- password = password.replace match[0], repeat(' ', match[0].length)
299
- matches
300
-
301
- digits_rx = /\d{3,}/
302
- digits_match = (password) ->
303
- for match in findall password, digits_rx
304
- [i, j] = [match.i, match.j]
305
- pattern: 'digits'
306
- i: i
307
- j: j
308
- token: password[i..j]
309
-
310
- # 4-digit years only. 2-digit years have the same entropy as 2-digit brute force.
311
- year_rx = /19\d\d|200\d|201\d/
312
- year_match = (password) ->
313
- for match in findall password, year_rx
314
- [i, j] = [match.i, match.j]
315
- pattern: 'year'
316
- i: i
317
- j: j
318
- token: password[i..j]
319
-
320
- date_match = (password) ->
321
- # match dates with separators 1/1/1911 and dates without 111997
322
- date_without_sep_match(password).concat date_sep_match(password)
323
-
324
- date_without_sep_match = (password) ->
325
- date_matches = []
326
- for digit_match in findall password, /\d{4,8}/ # 1197 is length-4, 01011997 is length 8
327
- [i, j] = [digit_match.i, digit_match.j]
328
- token = password[i..j]
329
- end = token.length
330
- candidates_round_1 = [] # parse year alternatives
331
- if token.length <= 6
332
- candidates_round_1.push # 2-digit year prefix
333
- daymonth: token[2..]
334
- year: token[0..1]
335
- i: i
336
- j: j
337
- candidates_round_1.push # 2-digit year suffix
338
- daymonth: token[0...end-2]
339
- year: token[end-2..]
340
- i: i
341
- j: j
342
- if token.length >= 6
343
- candidates_round_1.push # 4-digit year prefix
344
- daymonth: token[4..]
345
- year: token[0..3]
346
- i: i
347
- j: j
348
- candidates_round_1.push # 4-digit year suffix
349
- daymonth: token[0...end-4]
350
- year: token[end-4..]
351
- i: i
352
- j: j
353
- candidates_round_2 = [] # parse day/month alternatives
354
- for candidate in candidates_round_1
355
- switch candidate.daymonth.length
356
- when 2 # ex. 1 1 97
357
- candidates_round_2.push
358
- day: candidate.daymonth[0]
359
- month: candidate.daymonth[1]
360
- year: candidate.year
361
- i: candidate.i
362
- j: candidate.j
363
- when 3 # ex. 11 1 97 or 1 11 97
364
- candidates_round_2.push
365
- day: candidate.daymonth[0..1]
366
- month: candidate.daymonth[2]
367
- year: candidate.year
368
- i: candidate.i
369
- j: candidate.j
370
- candidates_round_2.push
371
- day: candidate.daymonth[0]
372
- month: candidate.daymonth[1..2]
373
- year: candidate.year
374
- i: candidate.i
375
- j: candidate.j
376
- when 4 # ex. 11 11 97
377
- candidates_round_2.push
378
- day: candidate.daymonth[0..1]
379
- month: candidate.daymonth[2..3]
380
- year: candidate.year
381
- i: candidate.i
382
- j: candidate.j
383
- # final loop: reject invalid dates
384
- for candidate in candidates_round_2
385
- day = parseInt(candidate.day)
386
- month = parseInt(candidate.month)
387
- year = parseInt(candidate.year)
388
- [valid, [day, month, year]] = check_date(day, month, year)
389
- continue unless valid
390
- date_matches.push
391
- pattern: 'date'
392
- i: candidate.i
393
- j: candidate.j
394
- token: password[i..j]
395
- separator: ''
396
- day: day
397
- month: month
398
- year: year
399
- date_matches
400
-
401
- date_rx_year_suffix = ///
402
- ( \d{1,2} ) # day or month
403
- ( \s | - | / | \\ | _ | \. ) # separator
404
- ( \d{1,2} ) # month or day
405
- \2 # same separator
406
- ( 19\d{2} | 200\d | 201\d | \d{2} ) # year
407
- ///
408
- date_rx_year_prefix = ///
409
- ( 19\d{2} | 200\d | 201\d | \d{2} ) # year
410
- ( \s | - | / | \\ | _ | \. ) # separator
411
- ( \d{1,2} ) # day or month
412
- \2 # same separator
413
- ( \d{1,2} ) # month or day
414
- ///
415
- date_sep_match = (password) ->
416
- matches = []
417
- for match in findall password, date_rx_year_suffix
418
- [match.day, match.month, match.year] = (parseInt(match[k]) for k in [1,3,4])
419
- match.sep = match[2]
420
- matches.push match
421
- for match in findall password, date_rx_year_prefix
422
- [match.day, match.month, match.year] = (parseInt(match[k]) for k in [4,3,1])
423
- match.sep = match[2]
424
- matches.push match
425
- for match in matches
426
- [valid, [day, month, year]] = check_date(match.day, match.month, match.year)
427
- continue unless valid
428
- pattern: 'date'
429
- i: match.i
430
- j: match.j
431
- token: password[match.i..match.j]
432
- separator: match.sep
433
- day: day
434
- month: month
435
- year: year
436
-
437
- check_date = (day, month, year) ->
438
- if 12 <= month <= 31 and day <= 12 # tolerate both day-month and month-day order
439
- [day, month] = [month, day]
440
- if day > 31 or month > 12
441
- return [false, []]
442
- unless 1900 <= year <= 2019
443
- return [false, []]
444
- [true, [day, month, year]]