Skip to content

Commit

Permalink
extracted parsing/translating logic into another class to avoid addin…
Browse files Browse the repository at this point in the history
…g private methods to Date and Time
  • Loading branch information
Jeremy Weiskotten committed Jul 16, 2011
1 parent 27772d2 commit 99d39e0
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 130 deletions.
132 changes: 2 additions & 130 deletions lib/stamp.rb
Original file line number Diff line number Diff line change
@@ -1,43 +1,9 @@
require "stamp/translator"
require "stamp/version"
require "date"
require "time"

module Stamp
MONTHNAMES_REGEXP = /^(#{Date::MONTHNAMES.compact.join('|')})$/i
ABBR_MONTHNAMES_REGEXP = /^(#{Date::ABBR_MONTHNAMES.compact.join('|')})$/i
DAYNAMES_REGEXP = /^(#{Date::DAYNAMES.join('|')})$/i
ABBR_DAYNAMES_REGEXP = /^(#{Date::ABBR_DAYNAMES.join('|')})$/i

ONE_DIGIT_REGEXP = /^\d{1}$/
TWO_DIGIT_REGEXP = /^\d{2}$/
FOUR_DIGIT_REGEXP = /^\d{4}$/

TIME_REGEXP = /(\d{1,2})(:)(\d{2})(\s*)(:)?(\d{2})?(\s*)?([ap]m)?/i

MERIDIAN_LOWER_REGEXP = /^(a|p)m$/
MERIDIAN_UPPER_REGEXP = /^(A|P)M$/

# Disambiguate based on value
OBVIOUS_YEARS = 60..99
OBVIOUS_MONTHS = 12
OBVIOUS_DAYS = 28..31
OBVIOUS_24_HOUR = 13..23

TWO_DIGIT_DATE_SUCCESSION = {
'%m' => '%d',
'%b' => '%d',
'%B' => '%d',
'%d' => '%y',
'%e' => '%y'
}

TWO_DIGIT_TIME_SUCCESSION = {
'%H' => '%M',
'%I' => '%M',
'%l' => '%M',
'%M' => '%S'
}

# Formats a date/time using a human-friendly example as a template.
#
# @param [String] example a human-friendly date/time example
Expand All @@ -60,101 +26,7 @@ def stamp(example)
# @example
# Date.today.strftime_format("Jan 1, 1999") #=> "%b %e, %Y"
def strftime_format(example)
# extract any substrings that look like times, like "23:59" or "8:37 am"
before, time_example, after = example.partition(TIME_REGEXP)

# transform any date tokens to strftime directives
words = strftime_directives(before.split(/\b/)) do |token, previous_directive|
strftime_date_directive(token, previous_directive)
end

# transform the example time string to strftime directives
unless time_example.empty?
time_parts = time_example.scan(TIME_REGEXP).first
words += strftime_directives(time_parts) do |token, previous_directive|
strftime_time_directive(token, previous_directive)
end
end

# recursively process any remaining text
words << strftime_format(after) unless after.empty?
words.join
end

private

# Transforms tokens that look like date/time parts to strftime directives.
def strftime_directives(tokens)
previous_directive = nil
tokens.map do |token|
directive = yield(token, previous_directive)
previous_directive = directive unless directive.nil?
directive || token
end
end

def strftime_time_directive(token, previous_directive)
case token
when MERIDIAN_LOWER_REGEXP
if RUBY_VERSION =~ /^1.8/ && self.is_a?(Time)
# 1.8.7 doesn't implement %P
self.strftime("%p").downcase
else
'%P'
end

when MERIDIAN_UPPER_REGEXP
'%p'

when TWO_DIGIT_REGEXP
TWO_DIGIT_TIME_SUCCESSION[previous_directive] ||
case token.to_i
when OBVIOUS_24_HOUR
'%H' # 24-hour clock
else
'%I' # 12-hour clock with leading zero
end

when ONE_DIGIT_REGEXP
'%l' # hour without leading zero
end
end

def strftime_date_directive(token, previous_directive)
case token
when MONTHNAMES_REGEXP
'%B'

when ABBR_MONTHNAMES_REGEXP
'%b'

when DAYNAMES_REGEXP
'%A'

when ABBR_DAYNAMES_REGEXP
'%a'

when FOUR_DIGIT_REGEXP
'%Y'

when TWO_DIGIT_REGEXP
# try to discern obvious intent based on the example value
case token.to_i
when OBVIOUS_YEARS
'%y'
when OBVIOUS_MONTHS
'%m'
when OBVIOUS_DAYS
'%d'
else
# the intent isn't obvious based on the example value, so try to
# disambiguate based on context
TWO_DIGIT_DATE_SUCCESSION[previous_directive] || '%m'
end

when ONE_DIGIT_REGEXP
'%e' # day without leading zero
end
Stamp::StrftimeTranslator.new(self).translate(example)
end
end

Expand Down
139 changes: 139 additions & 0 deletions lib/stamp/translator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
module Stamp
class StrftimeTranslator
MONTHNAMES_REGEXP = /^(#{Date::MONTHNAMES.compact.join('|')})$/i
ABBR_MONTHNAMES_REGEXP = /^(#{Date::ABBR_MONTHNAMES.compact.join('|')})$/i
DAYNAMES_REGEXP = /^(#{Date::DAYNAMES.join('|')})$/i
ABBR_DAYNAMES_REGEXP = /^(#{Date::ABBR_DAYNAMES.join('|')})$/i

ONE_DIGIT_REGEXP = /^\d{1}$/
TWO_DIGIT_REGEXP = /^\d{2}$/
FOUR_DIGIT_REGEXP = /^\d{4}$/

TIME_REGEXP = /(\d{1,2})(:)(\d{2})(\s*)(:)?(\d{2})?(\s*)?([ap]m)?/i

MERIDIAN_LOWER_REGEXP = /^(a|p)m$/
MERIDIAN_UPPER_REGEXP = /^(A|P)M$/

# Disambiguate based on value
OBVIOUS_YEARS = 60..99
OBVIOUS_MONTHS = 12
OBVIOUS_DAYS = 28..31
OBVIOUS_24_HOUR = 13..23

TWO_DIGIT_DATE_SUCCESSION = {
'%m' => '%d',
'%b' => '%d',
'%B' => '%d',
'%d' => '%y',
'%e' => '%y'
}

TWO_DIGIT_TIME_SUCCESSION = {
'%H' => '%M',
'%I' => '%M',
'%l' => '%M',
'%M' => '%S'
}

def initialize(target_date_or_time)
@target = target_date_or_time
end

def translate(example)
# extract any substrings that look like times, like "23:59" or "8:37 am"
before, time_example, after = example.partition(TIME_REGEXP)

# transform any date tokens to strftime directives
words = strftime_directives(before.split(/\b/)) do |token, previous_directive|
strftime_date_directive(token, previous_directive)
end

# transform the example time string to strftime directives
unless time_example.empty?
time_parts = time_example.scan(TIME_REGEXP).first
words += strftime_directives(time_parts) do |token, previous_directive|
strftime_time_directive(token, previous_directive)
end
end

# recursively process any remaining text
words << translate(after) unless after.empty?
words.join
end

# Transforms tokens that look like date/time parts to strftime directives.
def strftime_directives(tokens)
previous_directive = nil
tokens.map do |token|
directive = yield(token, previous_directive)
previous_directive = directive unless directive.nil?
directive || token
end
end

def strftime_time_directive(token, previous_directive)
case token
when MERIDIAN_LOWER_REGEXP
if RUBY_VERSION =~ /^1.8/ && @target.is_a?(Time)
# 1.8.7 doesn't implement %P
@target.strftime("%p").downcase
else
'%P'
end

when MERIDIAN_UPPER_REGEXP
'%p'

when TWO_DIGIT_REGEXP
TWO_DIGIT_TIME_SUCCESSION[previous_directive] ||
case token.to_i
when OBVIOUS_24_HOUR
'%H' # 24-hour clock
else
'%I' # 12-hour clock with leading zero
end

when ONE_DIGIT_REGEXP
'%l' # hour without leading zero
end
end

def strftime_date_directive(token, previous_directive)
case token
when MONTHNAMES_REGEXP
'%B'

when ABBR_MONTHNAMES_REGEXP
'%b'

when DAYNAMES_REGEXP
'%A'

when ABBR_DAYNAMES_REGEXP
'%a'

when FOUR_DIGIT_REGEXP
'%Y'

when TWO_DIGIT_REGEXP
# try to discern obvious intent based on the example value
case token.to_i
when OBVIOUS_YEARS
'%y'
when OBVIOUS_MONTHS
'%m'
when OBVIOUS_DAYS
'%d'
else
# the intent isn't obvious based on the example value, so try to
# disambiguate based on context
TWO_DIGIT_DATE_SUCCESSION[previous_directive] || '%m'
end

when ONE_DIGIT_REGEXP
'%e' # day without leading zero
end
end

end
end

0 comments on commit 99d39e0

Please sign in to comment.