Skip to content

Commit

Permalink
Refactor YAML core time parser
Browse files Browse the repository at this point in the history
  • Loading branch information
straight-shoota committed Dec 15, 2017
1 parent 9f74def commit 92e737f
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 175 deletions.
65 changes: 54 additions & 11 deletions src/time/format/parser.cr
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,14 @@ struct Time::Format
end
end

def time_zone_z_or_offset?(**options)
time_zone_z_or_offset(**options)

return true
rescue Time::Format::Error
false
end

def time_zone_z_or_offset(**options)
case char = current_char
when 'Z', 'z'
Expand All @@ -321,34 +329,55 @@ struct Time::Format
next_char
end

def time_zone_offset(force_colon = false, allow_colon = true, allow_seconds = true)
def time_zone_offset?(**options)
time_zone_offset(**options)

return true
rescue Time::Format::Error
false
end

def time_zone_offset(force_colon = false, allow_colon = true, allow_seconds = true, force_zero_padding = true, force_minutes = true)
sign = current_char == '-' ? -1 : 1

char = next_char
raise "Invalid timezone" unless char.ascii_number?
hours = char.to_i

char = next_char
raise "Invalid timezone" unless char.ascii_number?
hours = 10*hours + char.to_i
if char.ascii_number?
hours = 10*hours + char.to_i

char = next_char
elsif force_zero_padding
raise "Invalid timezone"
end

char = next_char
if char == ':'
raise "Invalid timezone" unless allow_colon
char = next_char
elsif force_colon
raise "Invalid timezone"
end
raise "Invalid timezone" unless char.ascii_number?
minutes = char.to_i

char = next_char
raise "Invalid timezone" unless char.ascii_number?
minutes = 10*minutes + char.to_i
if char.ascii_number?
minutes = char.to_i

char = next_char
if char.ascii_number?
minutes = 10*minutes + char.to_i
char = next_char
elsif force_zero_padding
raise "Invalid timezone"
end
elsif force_minutes
raise "Invalid timezone"
else
minutes = 0
end

@offset_in_minutes = sign * (60*hours + minutes)
@kind = Time::Kind::Utc
char = next_char

if @reader.has_next? && allow_seconds
pos = @reader.pos
Expand Down Expand Up @@ -410,15 +439,23 @@ struct Time::Format
consume_number_i64(max_digits).to_i
end

def consume_number?(max_digits)
consume_number_i64?(max_digits).try(&.to_i)
end

def consume_number_i64(max_digits)
consume_number_i64?(max_digits) || raise "Invalid number"
end

def consume_number_i64?(max_digits)
n = 0_i64
char = current_char

if char.ascii_number?
n = (char - '0').to_i64
char = next_char
else
raise "Expecting number"
return nil
end

max_digits -= 1
Expand Down Expand Up @@ -453,6 +490,12 @@ struct Time::Format
next_char if current_char.ascii_whitespace?
end

def skip_spaces
while current_char.ascii_whitespace?
next_char
end
end

def whitespace
unless current_char.ascii_whitespace?
raise "Unexpected char: #{current_char.inspect} (#{@reader.pos})"
Expand Down
2 changes: 1 addition & 1 deletion src/yaml/schema/core.cr
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,6 @@ module YAML::Schema::Core
# Minimum length is that of YYYY-M-D
return nil if string.size < 8

TimeParser.new(string).parse
TimeParser.parse?(string)
end
end
226 changes: 63 additions & 163 deletions src/yaml/schema/core/time_parser.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# :nodoc:
struct YAML::Schema::Core::TimeParser
module YAML::Schema::Core::TimeParser
# Even though the standard library has Time parsers given a *fixed* format,
# the format in YAML, http://yaml.org/type/timestamp.html,
# can consist of just the date part, and following it any number of spaces,
Expand All @@ -10,190 +10,90 @@ struct YAML::Schema::Core::TimeParser
#
# As an additional note, Ruby's Psych YAML parser also implements a
# custom time parser, probably for this same reason.
def initialize(string)
@reader = Char::Reader.new(string)
end

def current_char
@reader.current_char
end

def next_char
@reader.next_char
# Parses a string into a `Time`.
def self.parse?(string) : Time?
parser = Time::Format::Parser.new(string)
if parser.yaml_date_time?
parser.time(Time::Kind::Utc) rescue nil
else
nil
end
end
end

def parse
year = parse_number(4)
return nil unless year

return nil unless dash?

month = parse_number_1_or_2
return nil unless month

return nil unless dash?

day = parse_number_1_or_2
return nil unless day

case current_char
when 'T', 't'
next_char
parse_after_date(year, month, day)
when .ascii_whitespace?
skip_space

if @reader.has_next?
parse_after_date(year, month, day)
struct Time::Format
struct Parser
def yaml_date_time?
if (year = consume_number?(4)) && char?('-')
@year = year
else
new_time(year, month, day)
return nil
end
else
if @reader.has_next?
nil

if (month = consume_number?(2)) && char?('-')
@month = month
else
new_time(year, month, day)
return nil
end
end
end

def parse_after_date(year, month, day)
hour = parse_number_1_or_2
return nil unless hour

return nil unless colon?

minute = parse_number(2)
return nil unless minute

return nil unless colon?

second = parse_number(2)
return nil unless second

unless @reader.has_next?
return new_time(year, month, day, hour, minute, second)
end

nanosecond = 0

if current_char == '.'
next_char

nanosecond = parse_nanoseconds
return nil unless nanosecond
end

skip_space

case current_char
when 'Z'
next_char
when '+', '-'
tz_sign = current_char == '+' ? 1 : -1
next_char

tz_hour = parse_number_1_or_2
return nil unless tz_hour

if colon?
tz_minute = parse_number(2)
return nil unless tz_minute
if day = consume_number?(2)
@day = day
else
tz_minute = parse_number(2)
tz_minute = 0 unless tz_minute
return nil
end

tz_offset = tz_sign * (tz_hour * 60 + tz_minute)
end

return nil if @reader.has_next?
case current_char
when 'T', 't'
next_char
return yaml_time?
when .ascii_whitespace?
skip_spaces

time = new_time(year, month, day, hour, minute, second, nanosecond: nanosecond)
if time && tz_offset
time = time - tz_offset.minutes
end
time
end

def parse_nanoseconds
return nil unless current_char.ascii_number?

multiplier = Time::NANOSECONDS_PER_SECOND / 10
number = current_char.to_i

next_char

8.times do
break unless current_char.ascii_number?

number *= 10
number += current_char.to_i
multiplier /= 10

next_char
end
if @reader.has_next?
return yaml_time?
end
else
if @reader.has_next?
return nil
end
end

while current_char.ascii_number?
next_char
true
end

number * multiplier
end

def parse_number(n)
number = 0

n.times do
return nil unless current_char.ascii_number?

number *= 10
number += current_char.to_i
def yaml_time?
if (hour = consume_number?(2)) && char?(':')
@hour = hour
else
return nil
end

next_char
end
if (minute = consume_number?(2)) && char?(':')
@minute = minute
else
return nil
end

number
end
if second = consume_number?(2)
@second = second
else
return nil
end

def parse_number_1_or_2
return nil unless current_char.ascii_number?
second_fraction?

number = current_char.to_i
next_char
skip_spaces

if current_char.ascii_number?
number *= 10
number += current_char.to_i
next_char
end
if @reader.has_next?
unless time_zone_z_or_offset?(force_zero_padding: false, force_minutes: false)
return nil
end

number
end
return nil if @reader.has_next?
end

def skip_space
while current_char.ascii_whitespace?
next_char
true
end
end

def dash?
return false unless current_char == '-'

next_char
true
end

def colon?
return false unless current_char == ':'

next_char
true
end

def new_time(*args, **named_args)
Time.utc(*args, **named_args)
rescue
nil
end
end

0 comments on commit 92e737f

Please sign in to comment.