Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Another localization #281

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
/doc/
/tags/
Gemfile.lock
.idea
8 changes: 7 additions & 1 deletion lib/chronic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
require 'chronic/parser'
require 'chronic/date'
require 'chronic/time'
require 'chronic/locale'
require 'chronic/locales/en'
require 'chronic/locales/ru'

require 'chronic/handler'
require 'chronic/handlers'
Expand Down Expand Up @@ -74,10 +77,12 @@ class << self
#
# Returns The Time class Chronic uses internally.
attr_accessor :time_class
attr_accessor :locale
end

self.debug = false
self.time_class = ::Time
self.locale = :en


# Parses a string containing a natural language date or time.
Expand All @@ -89,7 +94,8 @@ class << self
# text - The String text to parse.
# opts - An optional Hash of configuration options passed to Parser::new.
def self.parse(text, options = {})
Parser.new(options).parse(text)
locale_class = Locale.by_name(options.delete(:locale) || locale)
Parser.new({locale: locale_class}.merge(options)).parse(text)
end

# Construct a new time object determining possible month overflows
Expand Down
8 changes: 7 additions & 1 deletion lib/chronic/handlers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,13 @@ def handle_s_r_p_a(tokens, options)
end

def handle_s_r_a_s_r_p_a(tokens, options)
anchor_span = get_anchor(tokens[4..tokens.size - 1], options)
anchor_tokens = tokens[4..tokens.size - 1]

anchor_span = if anchor_tokens.count > 1
get_anchor(anchor_tokens, options)
else
Span.new(self.now, self.now + 1)
end

span = handle_srp(tokens[0..1]+tokens[4..6], anchor_span, options)
handle_srp(tokens[2..3]+tokens[4..6], span, options)
Expand Down
26 changes: 26 additions & 0 deletions lib/chronic/locale.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require 'active_support/core_ext'

module Chronic
class Locale

AVAILABLE = [:en, :ru]

def [](path)
path.to_s.split('.').reduce(self.class::LOCALE_HASH) do |value, element|
value[element.to_sym] || {}
end
end

class << self
def by_name(name)
raise "Unknown locale #{name}. Available: #{AVAILABLE}" unless AVAILABLE.include?(name)
klass = "Chronic::Locale::#{name.to_s.camelize}".constantize
klass.new
end

def default
by_name(:en)
end
end
end
end
131 changes: 131 additions & 0 deletions lib/chronic/locales/en.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
class Chronic::Locale::En < Chronic::Locale

LOCALE_HASH = {
repeater: {
scan_for_season_names: {
/^springs?$/ => :spring,
/^summers?$/ => :summer,
/^(autumn)|(fall)s?$/ => :autumn,
/^winters?$/ => :winter
},
scan_for_month_names: {
/^jan[:\.]?(uary)?$/ => :january,
/^feb[:\.]?(ruary)?$/ => :february,
/^mar[:\.]?(ch)?$/ => :march,
/^apr[:\.]?(il)?$/ => :april,
/^may$/ => :may,
/^jun[:\.]?e?$/ => :june,
/^jul[:\.]?y?$/ => :july,
/^aug[:\.]?(ust)?$/ => :august,
/^sep[:\.]?(t[:\.]?|tember)?$/ => :september,
/^oct[:\.]?(ober)?$/ => :october,
/^nov[:\.]?(ember)?$/ => :november,
/^dec[:\.]?(ember)?$/ => :december,
},
scan_for_day_names: {
/^m[ou]n(day)?$/ => :monday,
/^t(ue|eu|oo|u)s?(day)?$/ => :tuesday,
/^we(d|dnes|nds|nns)(day)?$/ => :wednesday,
/^th(u|ur|urs|ers)(day)?$/ => :thursday,
/^fr[iy](day)?$/ => :friday,
/^sat(t?[ue]rday)?$/ => :saturday,
/^su[nm](day)?$/ => :sunday
},
scan_for_day_portions: {
/^ams?$/ => :am,
/^pms?$/ => :pm,
/^mornings?$/ => :morning,
/^afternoons?$/ => :afternoon,
/^evenings?$/ => :evening,
/^(night|nite)s?$/ => :night
},
scan_for_times: /^\d{1,2}(:?\d{1,2})?([\.:]?\d{1,2}([\.:]\d{1,6})?)?$/,
scan_for_units: {
/^years?$/ => :year,
/^seasons?$/ => :season,
/^months?$/ => :month,
/^fortnights?$/ => :fortnight,
/^weeks?$/ => :week,
/^weekends?$/ => :weekend,
/^(week|business)days?$/ => :weekday,
/^days?$/ => :day,
/^hrs?$/ => :hour,
/^hours?$/ => :hour,
/^mins?$/ => :minute,
/^minutes?$/ => :minute,
/^secs?$/ => :second,
/^seconds?$/ => :second
}
},
grabber: {
scan_for_all: {
/last/ => :last,
/this/ => :this,
/next/ => :next
}
},
pointer: {
scan_for_all: {
/\bpast\b/ => :past,
/\b(?:future|in)\b/ => :future,
}
},
separator: {
comma: /^,$/,
dot: /^\.$/,
colon: /^:$/,
space: /^ $/,
slash: /^\/$/,
dash: /^-$/,
scan_for_quote: {
/^'$/ => :single_quote,
/^"$/ => :double_quote
},
at: /^(at|@)$/,
in: /^in$/,
on: /^on$/,
and: /^and$/,
t: /^t$/,
w: /^w$/
}
}

def pre_normalize(text)
text = text.to_s.downcase
text.gsub!(/\b(\d{2})\.(\d{2})\.(\d{4})\b/, '\3 / \2 / \1')
text.gsub!(/\b([ap])\.m\.?/, '\1m')
text.gsub!(/(\s+|:\d{2}|:\d{2}\.\d{3})\-(\d{2}:?\d{2})\b/, '\1tzminus\2')
text.gsub!(/\./, ':')
text.gsub!(/([ap]):m:?/, '\1m')
text.gsub!(/['"]/, '')
text.gsub!(/,/, ' ')
text.gsub!(/^second /, '2nd ')
text.gsub!(/\bsecond (of|day|month|hour|minute|second)\b/, '2nd \1')
text = Numerizer.numerize(text)
text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
text.gsub!(/(?:^|\s)0(\d+:\d+\s*pm?\b)/, ' \1')
text.gsub!(/\btoday\b/, 'this day')
text.gsub!(/\btomm?orr?ow\b/, 'next day')
text.gsub!(/\byesterday\b/, 'last day')
text.gsub!(/\bnoon\b/, '12:00pm')
text.gsub!(/\bmidnight\b/, '24:00')
text.gsub!(/\bnow\b/, 'this second')
text.gsub!('quarter', '15')
text.gsub!('half', '30')
text.gsub!(/(\d{1,2}) (to|till|prior to|before)\b/, '\1 minutes past')
text.gsub!(/(\d{1,2}) (after|past)\b/, '\1 minutes future')
text.gsub!(/\b(?:ago|before(?: now)?)\b/, 'past')
text.gsub!(/\bthis (?:last|past)\b/, 'last')
text.gsub!(/\b(?:in|during) the (morning)\b/, '\1')
text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
text.gsub!(/\btonight\b/, 'this night')
text.gsub!(/\b\d+:?\d*[ap]\b/, '\0m')
text.gsub!(/\b(\d{2})(\d{2})(am|pm)\b/, '\1:\2\3')
text.gsub!(/(\d)([ap]m|oclock)\b/, '\1 \2')
text.gsub!(/\b(hence|after|from)\b/, 'future')
text.gsub!(/^\s?an? /i, '1 ')
text.gsub!(/\b(\d{4}):(\d{2}):(\d{2})\b/, '\1 / \2 / \3') # DTOriginal
text.gsub!(/\b0(\d+):(\d{2}):(\d{2}) ([ap]m)\b/, '\1:\2:\3 \4')
text
end
end
97 changes: 97 additions & 0 deletions lib/chronic/locales/ru.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
class Chronic::Locale::Ru < Chronic::Locale

LOCALE_HASH = {
repeater: {
scan_for_season_names: {},
scan_for_month_names: {
/^янв[:\.]?(ар)?(ь|я)?$/ => :january,
/^фев[:\.]?(рал)?(ь|я)?$/ => :february,
/^мар[:\.]?(т)?(а)?$/ => :march,
/^апр[:\.]?(ел)?(ь|я)?$/ => :april,
/^ма(й|я)$/ => :may,
/^июн[:\.]?(ь|я)?$/ => :june,
/^июл[:\.]?(ь|я)?$/ => :july,
/^авг[:\.]?(уст)?(а)?$/ => :august,
/^сен[:\.]?(тябр)?(ь|я)?$/ => :september,
/^окт[:\.]?(ябр)?(ь|я)?$/ => :october,
/^ноя[:\.]?(бр)?(ь|я)?$/ => :november,
/^дек[:\.]?(абр)?(ь|я)?$/ => :december,
},
scan_for_day_names: {
/^понедельник$/ => :monday,
/^вторник$/ => :tuesday,
/^среда$/ => :wednesday,
/^четверг$/ => :thursday,
/^пятница$/ => :friday,
/^суббота$/ => :saturday,
/^воскресение$/ => :sunday
},
scan_for_day_portions: {
/^утра$/ => :am,
/^вечера$/ => :pm
},
scan_for_times: /^\d{1,2}(:?\d{1,2})?([\.:]?\d{1,2}([\.:]\d{1,6})?)?$/,
scan_for_units: {
/^лет?$/ => :year,
/^год(а)?$/ => :year,
/^г\.$/ => :year,
/^сезон(ов)?$/ => :season,
/^месяц(ев)?$/ => :month,
/^недел(я|ь)$/ => :week,
/^выходн(ой|ых)$/ => :weekend,
/^дн(я|ей)$/ => :day,
/^час(ов)?(а)?$/ => :hour,
/^мин(ут)?(а)?$/ => :minute,
/^сек(унд)?(а)?$/ => :second,
}
},
grabber: {
scan_for_all: {
/прошлый/ => :last,
/этот/ => :this,
/следующий/ => :next
}
},
pointer: {
scan_for_all: {
/\bназад\b/ => :past,
/\b(?:через)\b/ => :future,
}
},
separator: {
comma: /^,$/,
dot: /^\.$/,
colon: /^:$/,
space: /^ $/,
slash: /^\/$/,
dash: /^-$/,
scan_for_quote: {
/^'$/ => :single_quote,
/^"$/ => :double_quote
},
at: /^в$/,
in: /^в$/,
on: /^в$/,
and: /^и$/,
t: /^t$/,
w: /^w$/
}
}


def pre_normalize(text)
text = text.to_s.mb_chars.downcase
text.gsub!(/\b(\d{2})\.(\d{2})\.(\d{4})\b/, '\3 / \2 / \1')
text.gsub!(/(\s+|:\d{2}|:\d{2}\.\d{3})\-(\d{2}:?\d{2})\b/, '\1tzminus\2')
text.gsub!(/\./, ':')
text.gsub!(/['"]/, '')
text.gsub!(/,/, ' ')
text = Numerizer.numerize(text)
text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
text.gsub!(/(?:^|\s)0(\d+:\d+\s*pm?\b)/, ' \1')
text.gsub!(/\b(\d{4}):(\d{2}):(\d{2})\b/, '\1 / \2 / \3') # DTOriginal
text.gsub!(/\b0(\d+):(\d{2}):(\d{2}) ([ap]m)\b/, '\1:\2:\3 \4')
text.gsub!(/\bсейчас\b/, 'эта секунда')
text
end
end
42 changes: 6 additions & 36 deletions lib/chronic/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,43 +91,13 @@ def parse(text)
# #=> "136 days future this second"
#
# Returns a new String ready for Chronic to parse.

def pre_normalize(text)
text = text.to_s.downcase
text.gsub!(/\b(\d{2})\.(\d{2})\.(\d{4})\b/, '\3 / \2 / \1')
text.gsub!(/\b([ap])\.m\.?/, '\1m')
text.gsub!(/(\s+|:\d{2}|:\d{2}\.\d{3})\-(\d{2}:?\d{2})\b/, '\1tzminus\2')
text.gsub!(/\./, ':')
text.gsub!(/([ap]):m:?/, '\1m')
text.gsub!(/['"]/, '')
text.gsub!(/,/, ' ')
text.gsub!(/^second /, '2nd ')
text.gsub!(/\bsecond (of|day|month|hour|minute|second)\b/, '2nd \1')
text = Numerizer.numerize(text)
text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
text.gsub!(/(?:^|\s)0(\d+:\d+\s*pm?\b)/, ' \1')
text.gsub!(/\btoday\b/, 'this day')
text.gsub!(/\btomm?orr?ow\b/, 'next day')
text.gsub!(/\byesterday\b/, 'last day')
text.gsub!(/\bnoon\b/, '12:00pm')
text.gsub!(/\bmidnight\b/, '24:00')
text.gsub!(/\bnow\b/, 'this second')
text.gsub!('quarter', '15')
text.gsub!('half', '30')
text.gsub!(/(\d{1,2}) (to|till|prior to|before)\b/, '\1 minutes past')
text.gsub!(/(\d{1,2}) (after|past)\b/, '\1 minutes future')
text.gsub!(/\b(?:ago|before(?: now)?)\b/, 'past')
text.gsub!(/\bthis (?:last|past)\b/, 'last')
text.gsub!(/\b(?:in|during) the (morning)\b/, '\1')
text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
text.gsub!(/\btonight\b/, 'this night')
text.gsub!(/\b\d+:?\d*[ap]\b/,'\0m')
text.gsub!(/\b(\d{2})(\d{2})(am|pm)\b/, '\1:\2\3')
text.gsub!(/(\d)([ap]m|oclock)\b/, '\1 \2')
text.gsub!(/\b(hence|after|from)\b/, 'future')
text.gsub!(/^\s?an? /i, '1 ')
text.gsub!(/\b(\d{4}):(\d{2}):(\d{2})\b/, '\1 / \2 / \3') # DTOriginal
text.gsub!(/\b0(\d+):(\d{2}):(\d{2}) ([ap]m)\b/, '\1:\2:\3 \4')
text
locale.pre_normalize(text)
end

def locale
options[:locale] || Chronic::Locale.default
end

# Guess a specific time within the given span.
Expand Down
13 changes: 4 additions & 9 deletions lib/chronic/tags/grabber.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,19 @@ class Grabber < Tag
# Returns an Array of Token objects.
def self.scan(tokens, options)
tokens.each do |token|
token.tag scan_for_all(token)
token.tag scan_for_all(token, options)
end
end

# token - The Token object to scan.
#
# Returns a new Grabber object.
def self.scan_for_all(token)
scan_for token, self,
{
/last/ => :last,
/this/ => :this,
/next/ => :next
}
def self.scan_for_all(token, options={})
scan_for token, self, options[:locale]['grabber.scan_for_all']
end

def to_s
'grabber-' << @type.to_s
end
end
end
end
Loading