Class: Zxcvbn::Matchers::Date Private
- Inherits:
-
Object
- Object
- Zxcvbn::Matchers::Date
- Defined in:
- lib/zxcvbn/matchers/date.rb
Overview
This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.
Matches date patterns in passwords, both with and without separators. Ported from the zxcvbn v4 JavaScript implementation’s date_match function.
Constant Summary collapse
- MAYBE_DATE_WITH_SEP =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Matches a separator-based date substring (e.g. “02/12/1997”, “97-12-02”). The first and last groups each allow 1–4 digits so the year may appear in either position; #map_ints_to_dmy resolves which group is the year.
%r{\A(\d{1,4})([\s/\\_.-])(\d{1,2})\2(\d{1,4})\z}- MAYBE_DATE_WITHOUT_SEP =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Matches a run of digits that could be a date without separators.
/\A\d+\z/- DATE_SPLITS =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Maps token length to split-point pairs for separator-free date parsing. Each pair [a, b] divides the token into three parts:
[0...a],[a...b],[b..]. MirrorsDATE_SPLITSin the JS v4 source. { 4 => [[1, 2], [2, 3]], 5 => [[1, 3], [2, 3]], 6 => [[1, 2], [2, 4], [4, 5]], 7 => [[1, 3], [2, 3], [4, 5], [4, 6]], 8 => [[2, 4], [4, 6]] }.freeze
- DATE_MIN_YEAR =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Earliest year accepted as a valid date year.
1000- DATE_MAX_YEAR =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Latest year accepted as a valid date year.
2050
Instance Method Summary collapse
-
#expand_year(year) ⇒ Integer
private
Expands a 2-digit year to 4 digits.
-
#map_ints_to_dm(day_val, month_val) ⇒ Hash?
private
Tries to assign two integers to day and month.
-
#map_ints_to_dmy(int1, int2, int3) ⇒ Hash?
private
Resolves three integers into a {year:, month:, day:} hash, or
nilif no valid assignment exists. -
#match_with_separator(password) ⇒ Array<MatchBuilder>
private
Finds date matches that use a separator character (space, slash, hyphen, etc.).
-
#match_without_separator(password, reference_year: Time.now.year) ⇒ Array<MatchBuilder>
private
Finds date matches in runs of digits that contain no separator character.
-
#matches(password, reference_year: Time.now.year) ⇒ Array<MatchBuilder>
private
Returns all date matches found in
password, deduplicating any match whose character span is fully contained within another match’s span.
Instance Method Details
#expand_year(year) ⇒ Integer
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Expands a 2-digit year to 4 digits. Values above 99 are returned unchanged. Mirrors two_to_four_digit_year in the JS v4 source.
Threshold is strictly > 50, matching JS: 50 → 2050, 51 → 1951. Negative values are treated as 1900s (e.g. -5 → 1995) — this is an edge case inherited from the JS implementation.
198 199 200 201 202 |
# File 'lib/zxcvbn/matchers/date.rb', line 198 def (year) return year if year > 99 year > 50 ? year + 1900 : year + 2000 end |
#map_ints_to_dm(day_val, month_val) ⇒ Hash?
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Tries to assign two integers to day and month. Attempts both orderings and returns the first that satisfies 1 ≤ day ≤ 31 and 1 ≤ month ≤ 12. Mirrors map_ints_to_dm in the JS v4 source.
182 183 184 185 186 187 |
# File 'lib/zxcvbn/matchers/date.rb', line 182 def map_ints_to_dm(day_val, month_val) [[day_val, month_val], [month_val, day_val]].each do |day, month| return { day:, month: } if day.between?(1, 31) && month >= 1 && month <= 12 end nil end |
#map_ints_to_dmy(int1, int2, int3) ⇒ Hash?
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Resolves three integers into a {year:, month:, day:} hash, or nil if no valid assignment exists. Mirrors map_ints_to_dmy in the JS v4 source.
The middle value (int2) is always treated as the non-year component (it comes from the d{1,2} capture group in the separator regex, or the middle split in the no-separator path). The outer two values are tried as the year: first int3, then int1. A value in [DATE_MIN_YEAR, DATE_MAX_YEAR] is treated as a 4-digit year (takes priority); otherwise both are tried as 2-digit years via #expand_year.
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 |
# File 'lib/zxcvbn/matchers/date.rb', line 140 def map_ints_to_dmy(int1, int2, int3) return nil if int2 > 31 || int2 <= 0 [int1, int2, int3].each do |n| return nil if n > 99 && n < DATE_MIN_YEAR return nil if n > DATE_MAX_YEAR end num_over_thirty_one = [int1, int2, int3].count { |n| n > 31 } num_over_twelve = [int1, int2, int3].count { |n| n > 12 } num_under_one = [int1, int2, int3].count { |n| n <= 0 } return nil if num_over_thirty_one >= 2 || num_over_twelve == 3 || num_under_one >= 2 # Try int3 then int1 as the year; 4-digit range takes priority over 2-digit. # If a 4-digit candidate is found but day/month are invalid, return nil immediately # rather than falling through to the 2-digit pass. pairs = [[int3, int1, int2], [int1, int2, int3]] four_digit = pairs.find { |yc, _dm1, _dm2| yc.between?(DATE_MIN_YEAR, DATE_MAX_YEAR) } if four_digit year_candidate, dm1, dm2 = four_digit dm = map_ints_to_dm(dm1, dm2) return dm ? { year: year_candidate, month: dm[:month], day: dm[:day] } : nil end # Fall back to 2-digit year pairs.each do |year_candidate, dm1, dm2| dm = map_ints_to_dm(dm1, dm2) next unless dm return { year: (year_candidate), month: dm[:month], day: dm[:day] } end nil end |
#match_with_separator(password) ⇒ Array<MatchBuilder>
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Finds date matches that use a separator character (space, slash, hyphen, etc.). Iterates over all substrings of length 6–10 and tests each against MAYBE_DATE_WITH_SEP, then resolves day/month/year via #map_ints_to_dmy.
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 |
# File 'lib/zxcvbn/matchers/date.rb', line 55 def match_with_separator(password) result = [] return result if password.length < 6 (0..(password.length - 6)).each do |i| ((i + 5)..[i + 9, password.length - 1].min).each do |j| token = password[i..j] m = MAYBE_DATE_WITH_SEP.match(token) next unless m date = map_ints_to_dmy(m[1].to_i, m[3].to_i, m[4].to_i) next unless date result << MatchBuilder.new( i:, j:, token:, pattern: 'date', separator: m[2], year: date[:year], month: date[:month], day: date[:day] ) end end result end |
#match_without_separator(password, reference_year: Time.now.year) ⇒ Array<MatchBuilder>
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Finds date matches in runs of digits that contain no separator character. Iterates over all digit-only substrings of length 4–8, applies DATE_SPLITS to generate day/month/year candidates via #map_ints_to_dmy, and picks the candidate whose year is closest to the current year.
4-digit tokens that look like standalone years (matched by Year::YEAR_REGEX) are skipped to avoid treating a year token as a date.
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 |
# File 'lib/zxcvbn/matchers/date.rb', line 93 def match_without_separator(password, reference_year: Time.now.year) result = [] return result if password.length < 4 (0..(password.length - 4)).each do |i| ((i + 3)..[i + 7, password.length - 1].min).each do |j| token = password[i..j] next unless MAYBE_DATE_WITHOUT_SEP.match?(token) splits = DATE_SPLITS[token.length] next unless splits next if token.length == 4 && Year::YEAR_REGEX.match?(token) candidates = splits.filter_map do |a, b| map_ints_to_dmy(token[0...a].to_i, token[a...b].to_i, token[b..].to_i) end next if candidates.empty? best = candidates.min_by { |c| (c[:year] - reference_year).abs } result << MatchBuilder.new( i:, j:, token:, pattern: 'date', separator: '', year: best[:year], month: best[:month], day: best[:day] ) end end result end |
#matches(password, reference_year: Time.now.year) ⇒ Array<MatchBuilder>
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns all date matches found in password, deduplicating any match whose character span is fully contained within another match’s span.
42 43 44 45 46 47 |
# File 'lib/zxcvbn/matchers/date.rb', line 42 def matches(password, reference_year: Time.now.year) all = match_with_separator(password) + match_without_separator(password, reference_year:) all.reject do |match| all.any? { |other| !other.equal?(match) && other.i <= match.i && other.j >= match.j } end end |