Module: Ruby::Rego::Builtins::Times::GoLayout

Defined in:
lib/ruby/rego/builtins/times/go_layout.rb,
lib/ruby/rego/builtins/times/go_layout/parser.rb,
lib/ruby/rego/builtins/times/go_layout/scanner.rb,
lib/ruby/rego/builtins/times/go_layout/formatter.rb,
lib/ruby/rego/builtins/times/go_layout/parser/zones.rb,
lib/ruby/rego/builtins/times/go_layout/parser/fields.rb,
lib/ruby/rego/builtins/times/go_layout/parser/compose.rb,
lib/ruby/rego/builtins/times/go_layout/parser/consumers.rb

Overview

The Go reference-time emitter (a port of appendFormat + appendNano from src/time/format.go). Lives apart from the tokenizer so the go_layout file stays under RubyCritic’s complexity budget; reopens the same GoLayout module so bare references to MONTHS/WEEKDAYS and the public surface resolve unchanged.

Defined Under Namespace

Classes: Parser

Constant Summary collapse

NAMED =

OPA’s acceptedTimeFormats — named layouts mapped to their Go reference-time string.

{
  "ANSIC" => "Mon Jan _2 15:04:05 2006",
  "UnixDate" => "Mon Jan _2 15:04:05 MST 2006",
  "RubyDate" => "Mon Jan 02 15:04:05 -0700 2006",
  "RFC822" => "02 Jan 06 15:04 MST",
  "RFC822Z" => "02 Jan 06 15:04 -0700",
  "RFC850" => "Monday, 02-Jan-06 15:04:05 MST",
  "RFC1123" => "Mon, 02 Jan 2006 15:04:05 MST",
  "RFC1123Z" => "Mon, 02 Jan 2006 15:04:05 -0700",
  "RFC3339" => "2006-01-02T15:04:05Z07:00",
  "RFC3339Nano" => "2006-01-02T15:04:05.999999999Z07:00"
}.freeze
DEFAULT =

The default layout when none is given (Go’s time.RFC3339Nano).

NAMED.fetch("RFC3339Nano")
MONTHS =
%w[January February March April May June July August September October November December].freeze
WEEKDAYS =
%w[Sunday Monday Tuesday Wednesday Thursday Friday Saturday].freeze
ZONE_SHAPES =

Per-token digit geometry for the numeric/ISO zone offsets: total source length, the index of the hour/minute/second digit pairs, and the indices that must hold a colon.

{
  num_short_tz: { len: 3, hh: 1 }, iso_short_tz: { len: 3, hh: 1, z: true },
  num_tz: { len: 5, hh: 1, mm: 3 }, iso_tz: { len: 5, hh: 1, mm: 3, z: true },
  num_colon_tz: { len: 6, hh: 1, mm: 4, colons: [3] },
  iso_colon_tz: { len: 6, hh: 1, mm: 4, colons: [3], z: true },
  num_seconds_tz: { len: 7, hh: 1, mm: 3, ss: 5 },
  iso_seconds_tz: { len: 7, hh: 1, mm: 3, ss: 5, z: true },
  num_colon_seconds_tz: { len: 9, hh: 1, mm: 4, ss: 7, colons: [3, 6] },
  iso_colon_seconds_tz: { len: 9, hh: 1, mm: 4, ss: 7, colons: [3, 6], z: true }
}.freeze
ZERO_TOKENS =
{ "1" => :zero_month, "2" => :zero_day, "3" => :zero_twelve_hour,
"4" => :zero_minute, "5" => :zero_second, "6" => :year }.freeze
NUM_TZ =

Numeric (always-signed) zone tokens, longest first.

[["-070000", :num_seconds_tz], ["-07:00:00", :num_colon_seconds_tz],
["-0700", :num_tz], ["-07:00", :num_colon_tz], ["-07", :num_short_tz]].freeze
ISO_TZ =

ISO-8601 zone tokens (a “Z” prefix prints Z for UTC), longest first.

[["Z070000", :iso_seconds_tz], ["Z07:00:00", :iso_colon_seconds_tz],
["Z0700", :iso_tz], ["Z07:00", :iso_colon_tz], ["Z07", :iso_short_tz]].freeze
SCANNERS =
{
  "J" => method(:scan_letter_j), "M" => method(:scan_letter_m),
  "0" => method(:scan_zero), "1" => method(:scan_one), "2" => method(:scan_two),
  "_" => method(:scan_underscore), "3" => ->(_l, _i) { [:twelve_hour, 1] },
  "4" => ->(_l, _i) { [:minute, 1] }, "5" => ->(_l, _i) { [:second, 1] },
  "P" => method(:scan_pm_upper), "p" => method(:scan_pm_lower),
  "-" => method(:scan_dash), "Z" => method(:scan_zed),
  "." => method(:scan_fraction), "," => method(:scan_fraction)
}.freeze
ISO_ZONE =

ISO-8601 zone tokens print “Z” for a zero (UTC) offset; numeric tokens never do.

%i[iso_tz iso_colon_tz iso_seconds_tz iso_short_tz iso_colon_seconds_tz].freeze
COLON_ZONE =
%i[iso_colon_tz num_colon_tz iso_colon_seconds_tz num_colon_seconds_tz].freeze
SHORT_ZONE =
%i[num_short_tz iso_short_tz].freeze
SECONDS_ZONE =
%i[iso_seconds_tz num_seconds_tz num_colon_seconds_tz iso_colon_seconds_tz].freeze
COLON_SECONDS_ZONE =
%i[num_colon_seconds_tz iso_colon_seconds_tz].freeze
ZONE_OFFSET =
(ISO_ZONE + %i[num_tz num_colon_tz num_seconds_tz num_short_tz num_colon_seconds_tz]).freeze

Class Method Summary collapse

Class Method Details

.format(time, layout) ⇒ String

Parameters:

  • time (Time)

    the instant already localised in the target zone

  • layout (String)

    a Go reference-time layout (resolved if a named constant)

Returns:

  • (String)


45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/ruby/rego/builtins/times/go_layout.rb', line 45

def self.format(time, layout)
  out = +""
  remaining = resolve(layout)
  until remaining.empty?
    prefix, token, remaining = next_chunk(remaining)
    out << prefix
    break if token.nil?

    out << emit(time, token)
  end
  out
end

.next_chunk(layout) ⇒ Object

A port of Go’s nextStdChunk: the next [literal prefix, token, suffix]. token is a Symbol for a fixed field, a [:frac, :zero|:nine, digits, separator] tuple for fractional seconds, or nil when no more layout tokens remain (prefix is then the trailing literal). :reek:TooManyStatements



17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/ruby/rego/builtins/times/go_layout/scanner.rb', line 17

def self.next_chunk(layout)
  index = 0
  while index < layout.length
    token, length, literal = token_at(layout, index)
    if token
      prefix = layout[0, index + (literal || 0)].to_s
      return [prefix, token, layout[(index + length)..].to_s]
    end

    index += 1
  end
  [layout, nil, ""]
end

.parse(layout, value) ⇒ Object

Parse value against layout, returning epoch nanoseconds, or nil when the value does not match the layout, a field is out of range, input is left over, or the instant falls outside the representable range. A named layout (NAMED) resolves to its Go string; unlike format, an empty layout is NOT defaulted (it stays the empty layout, which matches only the empty value), matching OPA’s time.Parse.



26
27
28
# File 'lib/ruby/rego/builtins/times/go_layout/parser.rb', line 26

def self.parse(layout, value)
  Parser.new(NAMED.fetch(layout, layout), value).run
end

.resolve(layout) ⇒ Object

The Go layout string for a (possibly named) layout, or the literal layout if not named.



36
37
38
39
40
# File 'lib/ruby/rego/builtins/times/go_layout.rb', line 36

def self.resolve(layout)
  return DEFAULT if layout.empty?

  NAMED.fetch(layout, layout)
end

.scan_dash(layout, index) ⇒ Object



125
126
127
# File 'lib/ruby/rego/builtins/times/go_layout/scanner.rb', line 125

def self.scan_dash(layout, index)
  scan_table(layout, index, NUM_TZ)
end

.scan_fraction(layout, index) ⇒ Object

“.000”/”.999”/”,000”/”,999” — a separator then a run of all-0 or all-9 digits. :reek:TooManyStatements



143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/ruby/rego/builtins/times/go_layout/scanner.rb', line 143

def self.scan_fraction(layout, index)
  separator = layout[index]
  digit = layout[index + 1]
  return [nil, 0] unless %w[0 9].include?(digit)

  stop = index + 1
  stop += 1 while layout[stop] == digit
  return [nil, 0] if digit_at?(layout, stop) # the run must end here

  kind = digit == "0" ? :zero : :nine
  [[:frac, kind, stop - (index + 1), separator], stop - index]
end

.scan_letter_j(layout, index) ⇒ Object

Each scanner returns [token, source_length] for a match at index (or a third literal_prefix_length element when a leading char is literal, as in scan_underscore’s _2006), else [nil, 0]. The fixed-field scanners mirror nextStdChunk’s per-leading-byte cases. :reek:TooManyStatements



56
57
58
59
60
61
# File 'lib/ruby/rego/builtins/times/go_layout/scanner.rb', line 56

def self.scan_letter_j(layout, index)
  return [:long_month, 7] if layout[index, 7] == "January"
  return [:month, 3] if layout[index, 3] == "Jan" && !starts_lower?(layout[(index + 3)..])

  [nil, 0]
end

.scan_letter_m(layout, index) ⇒ Object

:reek:TooManyStatements



64
65
66
67
68
69
70
71
72
# File 'lib/ruby/rego/builtins/times/go_layout/scanner.rb', line 64

def self.scan_letter_m(layout, index)
  if layout[index, 3] == "Mon"
    return [:long_weekday, 6] if layout[index, 6] == "Monday"
    return [:weekday, 3] unless starts_lower?(layout[(index + 3)..])
  end
  return [:tz, 3] if layout[index, 3] == "MST"

  [nil, 0]
end

.scan_one(layout, index) ⇒ Object



85
86
87
88
89
# File 'lib/ruby/rego/builtins/times/go_layout/scanner.rb', line 85

def self.scan_one(layout, index)
  return [:hour, 2] if layout[index + 1] == "5"

  [:num_month, 1]
end

.scan_pm_lower(layout, index) ⇒ Object



114
115
116
# File 'lib/ruby/rego/builtins/times/go_layout/scanner.rb', line 114

def self.scan_pm_lower(layout, index)
  layout[index + 1] == "m" ? [:pm_lower, 2] : [nil, 0]
end

.scan_pm_upper(layout, index) ⇒ Object



110
111
112
# File 'lib/ruby/rego/builtins/times/go_layout/scanner.rb', line 110

def self.scan_pm_upper(layout, index)
  layout[index + 1] == "M" ? [:pm_upper, 2] : [nil, 0]
end

.scan_two(layout, index) ⇒ Object



91
92
93
94
95
# File 'lib/ruby/rego/builtins/times/go_layout/scanner.rb', line 91

def self.scan_two(layout, index)
  return [:long_year, 4] if layout[index, 4] == "2006"

  [:day, 1]
end

.scan_underscore(layout, index) ⇒ Object

:reek:TooManyStatements



98
99
100
101
102
103
104
105
106
107
108
# File 'lib/ruby/rego/builtins/times/go_layout/scanner.rb', line 98

def self.scan_underscore(layout, index)
  if layout[index + 1] == "2"
    # "_2006" is a literal underscore followed by the long year (the "_" stays literal).
    return [:long_year, 5, 1] if layout[index, 5] == "_2006"

    return [:under_day, 2]
  end
  return [:under_year_day, 3] if layout[index, 3] == "__2"

  [nil, 0]
end

.scan_zed(layout, index) ⇒ Object



129
130
131
# File 'lib/ruby/rego/builtins/times/go_layout/scanner.rb', line 129

def self.scan_zed(layout, index)
  scan_table(layout, index, ISO_TZ)
end

.scan_zero(layout, index) ⇒ Object



77
78
79
80
81
82
83
# File 'lib/ruby/rego/builtins/times/go_layout/scanner.rb', line 77

def self.scan_zero(layout, index)
  following = layout[index + 1]
  return [ZERO_TOKENS.fetch(following), 2] if following && ZERO_TOKENS.key?(following)
  return [:zero_year_day, 3] if layout[index, 3] == "002"

  [nil, 0]
end