Class: Zxcvbn::Matchers::Date Private

Inherits:
Object
  • Object
show all
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..]. Mirrors DATE_SPLITS in 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

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.

Parameters:

  • year (Integer)

    the year value to expand

Returns:

  • (Integer)

    4-digit year



198
199
200
201
202
# File 'lib/zxcvbn/matchers/date.rb', line 198

def expand_year(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.

Parameters:

  • day_val (Integer)

    candidate day value

  • month_val (Integer)

    candidate month value

Returns:

  • (Hash, nil)

    {day:, month:} or nil if neither ordering is valid



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.

Parameters:

  • int1 (Integer)

    first integer (leading digits)

  • int2 (Integer)

    middle integer (always the non-year component)

  • int3 (Integer)

    last integer (trailing digits)

Returns:

  • (Hash, nil)

    {year:, month:, day:} or nil if no valid date



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: expand_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.

Parameters:

  • password (String)

    the password to search

Returns:



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.

Parameters:

  • password (String)

    the password to search

  • reference_year (Integer) (defaults to: Time.now.year)

    year used to pick the closest candidate; defaults to the current year

Returns:



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.

Parameters:

  • password (String)

    the password to search

  • reference_year (Integer) (defaults to: Time.now.year)

    year used to pick the closest candidate for separator-free dates; defaults to the current year

Returns:

  • (Array<MatchBuilder>)

    matches with pattern ‘date’, each containing year, month, day, and separator



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