module Flexitime::Parser
Constants
- HOUR_MINUTE_REGEX
time regex to match hour/min eg. “09:15”, “09:15 AM” and “09:15AM” with 1 or 2 digit hour; 2 digit minute; optional case-insensitive meridiem; and colon or period separator as in rails-i18n there a few locales (such as Danish) that use a period with hours & minutes
- HOUR_MINUTE_SECOND_REGEX
time regex to match hour/min/sec eg. “09:15:30”, “09:15:30 AM” and “09:15:30AM” with 1 or 2 digit hour; 2 digit minute & second; optional case-insensitive meridiem; and colon separator but not a period separator as there are no rails-i18n locales using that separator with hours, minutes & seconds and it would clash with the local date pattern
- ISO8601_DATE_REGEX
date regex to match year/month/day eg. “2021-10-27” with 4 digit year; 1 or 2 digit month & day; and slash, hyphen or period separator
- ISO8601_TIME_REGEX
time regex to match a zulu time “11:14:30.000Z” with 2 digit hour, minute & second; 1 to 6 digit milliseconds; and Z character
- LOCAL_DATE_REGEX
date regex to match day/month/year and month/day/year eg. “27/10/2021” and “10/27/2021” with 1 or 2 digit day & month; 2 or 4 digit year; and slash, hyphen or period separator
Public Instance Methods
Parse a String argument and create an ActiveSupport::TimeWithZone object using Time.zone
The parse uses either the argument or configuration first_date_part (:day or :month) and either the argument or configuration precision (:day, :hour, :min, :sec or :usec).
When the String contains a date or date/time or time that matches the regular expressions the Time.zone local method is used to create a TimeWithZone object otherwise the Time.zone parse method is used to create a TimeWithZone object and for an invalid date, date/time or time nil is returned.
# File lib/flexitime/parser.rb, line 34 def parse(str, first_date_part: nil, precision: nil) validate_options(first_date_part: first_date_part, precision: precision) str = str.is_a?(String) ? str : str.try(:to_str) return nil if str.blank? parts = extract_parts(str, first_date_part: first_date_part) if parts.present? create_time_from_parts(parts, precision: precision) if valid_date_parts?(parts) else time_zone_parse(str, precision: precision) end end
Private Instance Methods
Create a TimeWithZone object object using only those parts required for the configuration precision
# File lib/flexitime/parser.rb, line 166 def create_time_from_parts(parts, precision: nil) time = time_zone.local(*local_args_for_precision(parts, precision: precision)) parts[:utc] ? (time + time.utc_offset) : time rescue end
# File lib/flexitime/parser.rb, line 93 def extract_date_parts(str, first_date_part: nil) extract_iso_date_parts(str) || extract_local_date_parts(str, first_date_part: first_date_part) end
# File lib/flexitime/parser.rb, line 135 def extract_hour_minute_parts(str) # match array returns ["12:35 AM", "12", "35", "AM"] parts = str.match(HOUR_MINUTE_REGEX).to_a.pop(3) if parts.present? {hour: make_hour(parts.first.to_i, parts.third), min: parts.second.to_i} end end
# File lib/flexitime/parser.rb, line 127 def extract_hour_minute_second_parts(str) # match array returns ["12:35:20 AM", "12", "35", "20", "AM"] parts = str.match(HOUR_MINUTE_SECOND_REGEX).to_a.pop(4) if parts.present? {hour: make_hour(parts.first.to_i, parts.fourth), min: parts.second.to_i, sec: parts.third.to_i} end end
# File lib/flexitime/parser.rb, line 97 def extract_iso_date_parts(str) # match array returns ["1973-08-23", "1973", "08", "23"] parts = str.match(ISO8601_DATE_REGEX).to_a.pop(3).map(&:to_i) if parts.present? {year: make_year(parts.first), month: parts.second, day: parts.third} end end
# File lib/flexitime/parser.rb, line 119 def extract_iso_time_parts(str) # match array returns ["11:14:30.999Z", "11", "14", "30", "999", "Z"] parts = str.match(ISO8601_TIME_REGEX).to_a.pop(5).map(&:to_i) if parts.present? {hour: parts.first, min: parts.second, sec: parts.third, usec: parts.fourth, utc: true} end end
# File lib/flexitime/parser.rb, line 105 def extract_local_date_parts(str, first_date_part: nil) # match array returns ["23-08-1973", "23", "08", "1973"] parts = str.match(LOCAL_DATE_REGEX).to_a.pop(3).map(&:to_i) if parts.present? first_date_part ||= Flexitime.configuration.first_date_part day, month = first_date_part == :day ? [parts.first, parts.second] : [parts.second, parts.first] {year: make_year(parts.third), month: month, day: day} end end
Extract date and time parts and return a Hash containing the parts or nil if either the date or time string does not match a regex
# File lib/flexitime/parser.rb, line 68 def extract_parts(str, first_date_part: nil) date_str, time_str = separate_date_and_time(str) date_parts = extract_date_parts(date_str, first_date_part: first_date_part) if date_parts.blank? now = time_zone.now date_parts = {year: now.year, month: now.month, day: now.day} time_str = str.strip end if time_str.present? time_parts = extract_time_parts(time_str) return nil if time_parts.blank? date_parts.merge(time_parts) else date_parts end end
# File lib/flexitime/parser.rb, line 115 def extract_time_parts(str) extract_iso_time_parts(str) || extract_hour_minute_second_parts(str) || extract_hour_minute_parts(str) end
Returns the date/time parts required for the configuration precision
# File lib/flexitime/parser.rb, line 173 def local_args_for_precision(parts, precision: nil) keys = [:year, :month, :day, :hour, :min, :sec, :usec] precision ||= Flexitime.configuration.precision index = keys.index(precision) keys[0..index].map { |key| parts[key] } end
Convert hour depending on presence of am/pm
# File lib/flexitime/parser.rb, line 154 def make_hour(hour, meridiem) meridiem.to_s.downcase == "pm" ? hour + 12 : hour end
Convert 2 digit years into 4
# File lib/flexitime/parser.rb, line 144 def make_year(year) return year if year.to_s.size > 2 start_year = time_zone.now.year - Flexitime.configuration.ambiguous_year_future_bias century = (start_year / 100) * 100 full_year = century + year full_year < start_year ? full_year + 100 : full_year end
# File lib/flexitime/parser.rb, line 88 def separate_date_and_time(str) parts = str.index(" ").present? ? str.split(" ") : str.split("T") [parts.shift, parts.join(" ")] end
Set the precision, first checking if this is necessary to avoid the overhead of calling the change method
# File lib/flexitime/parser.rb, line 189 def set_precision(time, precision: nil) precision ||= Flexitime.configuration.precision index = PRECISIONS.index(precision) dismiss_part = PRECISIONS[index + 1] excess = PRECISIONS[(index + 1)..-1].sum { |key| time.send(key) } excess > 0 ? time.change(dismiss_part => 0) : time end
Returns an ActiveSupport::TimeZone
# File lib/flexitime/parser.rb, line 52 def time_zone Time.zone ||= "UTC" Time.zone end
Parse the string using Time.zone and set the configuration precision
# File lib/flexitime/parser.rb, line 181 def time_zone_parse(str, precision: nil) time = time_zone.parse(str) set_precision(time, precision: precision) rescue end
Validate the day & month parts as Time.zone#local accepts some invalid values such as Time.zone.local(2021,2,30) returning “2021-03-02”
# File lib/flexitime/parser.rb, line 160 def valid_date_parts?(parts) parts[:month] >= 1 && parts[:month] <= 12 && parts[:day] >= 1 && parts[:day] <= Time.days_in_month(parts[:month], parts[:year]) end
Ensure the parse method options are valid reusing the configuration class validation which raises an exception
# File lib/flexitime/parser.rb, line 59 def validate_options(first_date_part: nil, precision: nil) return if first_date_part.blank? && precision.blank? config = Configuration.new config.first_date_part = first_date_part if first_date_part.present? config.precision = precision if precision.present? end