Module: RemLint::DateLiteral

Defined in:
lib/remlint/date_literal.rb

Overview

Reads a date out of a run of tokens, the way GetFullDate does.

The classification is FindNumericToken's (src/token.c): a number from 1990 to 5990 is a year, a number from 1 to 31 is a day, and YYYY-MM-DD is a date all by itself. There is no overlap to resolve -- years start above the largest day -- so the components can arrive in any order.

Returns nil unless all three components turn up, because a partial date is a different question from a wrong one, and the rules that care about partial dates want to ask it separately.

Constant Summary collapse

EPOCH_YEAR =
1990
LAST_YEAR =

BASE + YR_RANGE from src/custom.h.in.

EPOCH_YEAR + 4000
MAX_DAY =
31
SEPARATORS =
%w[- /].freeze
MONTHS =
%w[
  JANUARY FEBRUARY MARCH APRIL MAY JUNE
  JULY AUGUST SEPTEMBER OCTOBER NOVEMBER DECEMBER
].freeze

Class Method Summary collapse

Class Method Details

.absorb(tokens, cursor, state) ⇒ Object



81
82
83
84
85
86
87
88
89
90
# File 'lib/remlint/date_literal.rb', line 81

def absorb(tokens, cursor, state)
  iso = iso_date(tokens, cursor)

  if iso
    merge(state, iso)
    5
  else
    component(tokens[cursor], state)
  end
end

.at(tokens, index) ⇒ Object

The date at index, or nil.



77
78
79
# File 'lib/remlint/date_literal.rb', line 77

def at(tokens, index)
  scan(tokens, index).first
end

.classify(token) ⇒ Object



119
120
121
122
123
124
# File 'lib/remlint/date_literal.rb', line 119

def classify(token)
  case token.type
  when :number then number(token.value)
  when :name   then month(token.value)
  end
end

.complete(state) ⇒ Object



159
160
161
162
163
# File 'lib/remlint/date_literal.rb', line 159

def complete(state)
  if state.values.none?(&:nil?)
    CalendarDate.new(state[:year], state[:month], state[:day])
  end
end

.component(token, state) ⇒ Object



108
109
110
111
112
113
114
115
116
117
# File 'lib/remlint/date_literal.rb', line 108

def component(token, state)
  value = classify(token)

  if value.nil? || !state[value.first].nil?
    0
  else
    state[value.first] = value.last
    1
  end
end

.iso_date(tokens, cursor) ⇒ Object

2027-01-01 reaches the lexer as five tokens, because a hyphen is an operator everywhere else.



94
95
96
97
98
99
100
# File 'lib/remlint/date_literal.rb', line 94

def iso_date(tokens, cursor)
  window = tokens[cursor, 5]

  if window&.length == 5 && iso_shape?(window)
    { year: window[0].value.to_i, month: window[2].value.to_i, day: window[4].value.to_i }
  end
end

.iso_shape?(window) ⇒ Boolean

Returns:

  • (Boolean)


102
103
104
105
106
# File 'lib/remlint/date_literal.rb', line 102

def iso_shape?(window)
  window[0].type == :number && window[2].type == :number && window[4].type == :number &&
    SEPARATORS.include?(window[1].value) && window[1].value == window[3].value &&
    window[0].value.length == 4
end

.merge(state, components) ⇒ Object



153
154
155
156
157
# File 'lib/remlint/date_literal.rb', line 153

def merge(state, components)
  components.each do |key, value|
    state[key] ||= value
  end
end

.month(word) ⇒ Object

Month names abbreviate like every other keyword: Jan is JANUARY because its minimum length is 3.



140
141
142
143
144
145
146
# File 'lib/remlint/date_literal.rb', line 140

def month(word)
  keyword = Vocabulary.keyword(word)

  if keyword&.type == "T_Month"
    [:month, MONTHS.index(keyword.name) + 1]
  end
end

.number(text) ⇒ Object



126
127
128
129
130
131
132
133
134
135
136
# File 'lib/remlint/date_literal.rb', line 126

def number(text)
  value = Integer(text, exception: false)

  if value.nil?
    nil
  elsif value >= EPOCH_YEAR && value <= LAST_YEAR
    [:year, value]
  elsif value >= 1 && value <= MAX_DAY
    [:day, value]
  end
end

.scan(tokens, index) ⇒ Object

Scans forward from index and returns [date, tokens consumed]. Consumed is zero when nothing date-shaped was there at all.



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/remlint/date_literal.rb', line 59

def scan(tokens, index)
  state = { year: nil, month: nil, day: nil }
  cursor = index

  while cursor < tokens.length
    taken = absorb(tokens, cursor, state)

    if taken.zero?
      break
    end

    cursor += taken
  end

  [complete(state), cursor - index]
end