From 384d38cfcd5531f5d20d3f793ef8acc4a2a0f732 Mon Sep 17 00:00:00 2001 From: dizer Date: Tue, 2 Sep 2014 18:38:06 +0800 Subject: [PATCH 1/2] Localization --- .gitignore | 1 + lib/chronic.rb | 8 +- lib/chronic/handlers.rb | 8 +- lib/chronic/locale.rb | 26 ++++++ lib/chronic/locales/en.rb | 131 ++++++++++++++++++++++++++++++ lib/chronic/locales/ru.rb | 96 ++++++++++++++++++++++ lib/chronic/parser.rb | 42 ++-------- lib/chronic/tags/grabber.rb | 13 +-- lib/chronic/tags/pointer.rb | 10 +-- lib/chronic/tags/repeater.rb | 64 ++------------- lib/chronic/tags/separator.rb | 82 +++++++++---------- test/test_parsing.rb | 3 + test/test_parsing_localized_ru.rb | 79 ++++++++++++++++++ test/test_repeater_day_name.rb | 4 +- 14 files changed, 410 insertions(+), 157 deletions(-) create mode 100644 lib/chronic/locale.rb create mode 100644 lib/chronic/locales/en.rb create mode 100644 lib/chronic/locales/ru.rb create mode 100644 test/test_parsing_localized_ru.rb diff --git a/.gitignore b/.gitignore index c956b379..ac1db785 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ /doc/ /tags/ Gemfile.lock +.idea diff --git a/lib/chronic.rb b/lib/chronic.rb index 1659a929..573f0b12 100644 --- a/lib/chronic.rb +++ b/lib/chronic.rb @@ -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' @@ -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. @@ -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 diff --git a/lib/chronic/handlers.rb b/lib/chronic/handlers.rb index 8d4b7a46..15526d05 100644 --- a/lib/chronic/handlers.rb +++ b/lib/chronic/handlers.rb @@ -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) diff --git a/lib/chronic/locale.rb b/lib/chronic/locale.rb new file mode 100644 index 00000000..211aeede --- /dev/null +++ b/lib/chronic/locale.rb @@ -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 diff --git a/lib/chronic/locales/en.rb b/lib/chronic/locales/en.rb new file mode 100644 index 00000000..fe3921fb --- /dev/null +++ b/lib/chronic/locales/en.rb @@ -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 diff --git a/lib/chronic/locales/ru.rb b/lib/chronic/locales/ru.rb new file mode 100644 index 00000000..266806e4 --- /dev/null +++ b/lib/chronic/locales/ru.rb @@ -0,0 +1,96 @@ +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, + /^сезон(ов)?$/ => :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.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 diff --git a/lib/chronic/parser.rb b/lib/chronic/parser.rb index 3d4c95dd..37780d7b 100644 --- a/lib/chronic/parser.rb +++ b/lib/chronic/parser.rb @@ -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. diff --git a/lib/chronic/tags/grabber.rb b/lib/chronic/tags/grabber.rb index 409b66a1..b4843c6f 100644 --- a/lib/chronic/tags/grabber.rb +++ b/lib/chronic/tags/grabber.rb @@ -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 \ No newline at end of file +end diff --git a/lib/chronic/tags/pointer.rb b/lib/chronic/tags/pointer.rb index 50f4abf4..087daafc 100644 --- a/lib/chronic/tags/pointer.rb +++ b/lib/chronic/tags/pointer.rb @@ -10,19 +10,15 @@ class Pointer < Tag # Returns an Array of tokens. def self.scan(tokens, options) tokens.each do |token| - if t = scan_for_all(token) then token.tag(t) end + if t = scan_for_all(token, options) then token.tag(t) end end end # token - The Token object we want to scan. # # Returns a new Pointer object. - def self.scan_for_all(token) - scan_for token, self, - { - /\bpast\b/ => :past, - /\b(?:future|in)\b/ => :future, - } + def self.scan_for_all(token, options={}) + scan_for token, self, options[:locale]['pointer.scan_for_all'] end def to_s diff --git a/lib/chronic/tags/repeater.rb b/lib/chronic/tags/repeater.rb index 9a7ea117..8a8e40aa 100644 --- a/lib/chronic/tags/repeater.rb +++ b/lib/chronic/tags/repeater.rb @@ -23,94 +23,42 @@ def self.scan(tokens, options) # # Returns a new Repeater object. def self.scan_for_season_names(token, options = {}) - scan_for token, RepeaterSeasonName, - { - /^springs?$/ => :spring, - /^summers?$/ => :summer, - /^(autumn)|(fall)s?$/ => :autumn, - /^winters?$/ => :winter - }, options + scan_for token, RepeaterSeasonName, options[:locale]['repeater.scan_for_season_names'], options end # token - The Token object we want to scan. # # Returns a new Repeater object. def self.scan_for_month_names(token, options = {}) - scan_for token, RepeaterMonthName, - { - /^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 - }, options + scan_for token, RepeaterMonthName, options[:locale]['repeater.scan_for_month_names'], options end # token - The Token object we want to scan. # # Returns a new Repeater object. def self.scan_for_day_names(token, options = {}) - scan_for token, RepeaterDayName, - { - /^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 - }, options + scan_for token, RepeaterDayName, options[:locale]['repeater.scan_for_day_names'], options end # token - The Token object we want to scan. # # Returns a new Repeater object. def self.scan_for_day_portions(token, options = {}) - scan_for token, RepeaterDayPortion, - { - /^ams?$/ => :am, - /^pms?$/ => :pm, - /^mornings?$/ => :morning, - /^afternoons?$/ => :afternoon, - /^evenings?$/ => :evening, - /^(night|nite)s?$/ => :night - }, options + scan_for token, RepeaterDayPortion, options[:locale]['repeater.scan_for_day_portions'], options end # token - The Token object we want to scan. # # Returns a new Repeater object. def self.scan_for_times(token, options = {}) - scan_for token, RepeaterTime, /^\d{1,2}(:?\d{1,2})?([\.:]?\d{1,2}([\.:]\d{1,6})?)?$/, options + scan_for token, RepeaterTime, options[:locale]['repeater.scan_for_times'], options end # token - The Token object we want to scan. # # Returns a new Repeater object. def self.scan_for_units(token, options = {}) - { - /^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 - }.each do |item, symbol| + options[:locale]['repeater.scan_for_units'].each do |item, symbol| if item =~ token.word klass_name = 'Repeater' + symbol.to_s.capitalize klass = Chronic.const_get(klass_name) diff --git a/lib/chronic/tags/separator.rb b/lib/chronic/tags/separator.rb index a15f3705..9136a331 100644 --- a/lib/chronic/tags/separator.rb +++ b/lib/chronic/tags/separator.rb @@ -10,115 +10,111 @@ class Separator < Tag # Returns an Array of tokens. def self.scan(tokens, options) tokens.each do |token| - token.tag scan_for_commas(token) - token.tag scan_for_dots(token) - token.tag scan_for_colon(token) - token.tag scan_for_space(token) - token.tag scan_for_slash(token) - token.tag scan_for_dash(token) - token.tag scan_for_quote(token) - token.tag scan_for_at(token) - token.tag scan_for_in(token) - token.tag scan_for_on(token) - token.tag scan_for_and(token) - token.tag scan_for_t(token) - token.tag scan_for_w(token) + token.tag scan_for_commas(token, options) + token.tag scan_for_dots(token, options) + token.tag scan_for_colon(token, options) + token.tag scan_for_space(token, options) + token.tag scan_for_slash(token, options) + token.tag scan_for_dash(token, options) + token.tag scan_for_quote(token, options) + token.tag scan_for_at(token, options) + token.tag scan_for_in(token, options) + token.tag scan_for_on(token, options) + token.tag scan_for_and(token, options) + token.tag scan_for_t(token, options) + token.tag scan_for_w(token, options) end end # token - The Token object we want to scan. # # Returns a new SeparatorComma object. - def self.scan_for_commas(token) - scan_for token, SeparatorComma, { /^,$/ => :comma } + def self.scan_for_commas(token, options={}) + scan_for token, SeparatorComma, { options[:locale]['separator.comma'] => :comma } end # token - The Token object we want to scan. # # Returns a new SeparatorDot object. - def self.scan_for_dots(token) - scan_for token, SeparatorDot, { /^\.$/ => :dot } + def self.scan_for_dots(token, options={}) + scan_for token, SeparatorDot, { options[:locale]['separator.dot'] => :dot } end # token - The Token object we want to scan. # # Returns a new SeparatorColon object. - def self.scan_for_colon(token) - scan_for token, SeparatorColon, { /^:$/ => :colon } + def self.scan_for_colon(token, options={}) + scan_for token, SeparatorColon, { options[:locale]['separator.colon'] => :colon } end # token - The Token object we want to scan. # # Returns a new SeparatorSpace object. - def self.scan_for_space(token) - scan_for token, SeparatorSpace, { /^ $/ => :space } + def self.scan_for_space(token, options={}) + scan_for token, SeparatorSpace, { options[:locale]['separator.space'] => :space } end # token - The Token object we want to scan. # # Returns a new SeparatorSlash object. - def self.scan_for_slash(token) - scan_for token, SeparatorSlash, { /^\/$/ => :slash } + def self.scan_for_slash(token, options={}) + scan_for token, SeparatorSlash, { options[:locale]['separator.slash'] => :slash } end # token - The Token object we want to scan. # # Returns a new SeparatorDash object. - def self.scan_for_dash(token) - scan_for token, SeparatorDash, { /^-$/ => :dash } + def self.scan_for_dash(token, options={}) + scan_for token, SeparatorDash, { options[:locale]['separator.dash'] => :dash } end # token - The Token object we want to scan. # # Returns a new SeparatorQuote object. - def self.scan_for_quote(token) - scan_for token, SeparatorQuote, - { - /^'$/ => :single_quote, - /^"$/ => :double_quote - } + def self.scan_for_quote(token, options={}) + scan_for token, SeparatorQuote, options[:locale]['separator.scan_for_quote'] end # token - The Token object we want to scan. # # Returns a new SeparatorAt object. - def self.scan_for_at(token) - scan_for token, SeparatorAt, { /^(at|@)$/ => :at } + def self.scan_for_at(token, options={}) + scan_for token, SeparatorAt, { options[:locale]['separator.at'] => :at } end # token - The Token object we want to scan. # # Returns a new SeparatorIn object. - def self.scan_for_in(token) - scan_for token, SeparatorIn, { /^in$/ => :in } + def self.scan_for_in(token, options={}) + scan_for token, SeparatorIn, { options[:locale]['separator.in'] => :in } end # token - The Token object we want to scan. # # Returns a new SeparatorOn object. - def self.scan_for_on(token) - scan_for token, SeparatorOn, { /^on$/ => :on } + def self.scan_for_on(token, options={}) + scan_for token, SeparatorOn, { options[:locale]['separator.on'] => :on } end # token - The Token object we want to scan. # # Returns a new SeperatorAnd Object object. - def self.scan_for_and(token) - scan_for token, SeparatorAnd, { /^and$/ => :and } + def self.scan_for_and(token, options={}) + scan_for token, SeparatorAnd, { options[:locale]['separator.and'] => :and } end # token - The Token object we want to scan. # # Returns a new SeperatorT Object object. - def self.scan_for_t(token) - scan_for token, SeparatorT, { /^t$/ => :T } + def self.scan_for_t(token, options={}) + scan_for token, SeparatorT, { options[:locale]['separator.t'] => :T } end # token - The Token object we want to scan. # # Returns a new SeperatorW Object object. - def self.scan_for_w(token) - scan_for token, SeparatorW, { /^w$/ => :W } + def self.scan_for_w(token, options={}) + scan_for token, SeparatorW, { options[:locale]['separator.w'] => :W } end def to_s diff --git a/test/test_parsing.rb b/test/test_parsing.rb index 577d00c4..458a4c04 100644 --- a/test/test_parsing.rb +++ b/test/test_parsing.rb @@ -969,6 +969,9 @@ def test_parse_guess_s_r_p time = parse_now("24 hours 20 minutes from now") assert_equal Time.local(2006, 8, 17, 14, 20, 0), time + + time = parse_now("24 hours 20 minutes ago") + assert_equal Time.local(2006, 8, 15, 13, 40, 0), time end def test_parse_guess_p_s_r diff --git a/test/test_parsing_localized_ru.rb b/test/test_parsing_localized_ru.rb new file mode 100644 index 00000000..8558958e --- /dev/null +++ b/test/test_parsing_localized_ru.rb @@ -0,0 +1,79 @@ +require 'helper' + +class TestParsingLocalizedRu < TestCase + # Wed Aug 16 14:00:00 UTC 2006 + TIME_2006_08_16_14_00_00 = Time.local(2006, 8, 16, 14, 0, 0, 0) + + def setup + @time_2006_08_16_14_00_00 = TIME_2006_08_16_14_00_00 + end + + def test_handle_rmn_sd + time = parse_now("авг 3") + assert_equal Time.local(2007, 8, 3, 12), time + + time = parse_now("авг. 3") + assert_equal Time.local(2007, 8, 3, 12), time + + time = parse_now("авг 20") + assert_equal Time.local(2006, 8, 20, 12), time + + time = parse_now("авг 20", :context => :future) + assert_equal Time.local(2006, 8, 20, 12), time + + time = parse_now("мая 27") + assert_equal Time.local(2007, 5, 27, 12), time + + time = parse_now("мая 28", :context => :past) + assert_equal Time.local(2006, 5, 28, 12), time + + time = parse_now("мая 28 5 вечера", :context => :past) + assert_equal Time.local(2006, 5, 28, 17), time + + time = parse_now("мая 28 в 5 вечера", :context => :past) + assert_equal Time.local(2006, 5, 28, 17), time + + time = parse_now("мая 28 в 5:32.19 вечера", :context => :past) + assert_equal Time.local(2006, 5, 28, 17, 32, 19), time + + time = parse_now("мая 28 в 5:32:19.764") + assert_in_delta Time.local(2007, 5, 28, 17, 32, 19, 764000), time, 0.001 + + time = parse_now("1 апр 2014", now: nil) + assert_equal Time.local(2014, 4, 1, 12), time + + time = parse_now("1 апр. 2014", now: nil) + assert_equal Time.local(2014, 4, 1, 12), time + + time = parse_now("1 апреля 2014", now: nil) + assert_equal Time.local(2014, 4, 1, 12), time + end + + def test_handle_rmn_sd_on + time = parse_now("2 дня назад") + assert_equal Time.local(2006, 8, 14, 14), time + + time = parse_now("6 месяцев назад") + assert_equal Time.local(2006, 2, 16, 14), time + + time = parse_now("3 года назад") + assert_equal Time.local(2003, 8, 16, 14), time + + time = parse_now("1 месяц назад") + assert_equal Time.local(2006, 7, 16, 14), time + + time = parse_now("3 года, 1 месяц назад от сейчас") + assert_equal Time.local(2003, 7, 16, 14), time + + time = parse_now("3 года, 1 месяц назад") + assert_equal Time.local(2003, 7, 16, 14), time + end + + private + def parse_now(string, options={}) + Chronic.parse(string, {:now => TIME_2006_08_16_14_00_00, :locale => :ru }.merge(options)) + end + def pre_normalize(s) + Chronic::Parser.new.pre_normalize s + end +end diff --git a/test/test_repeater_day_name.rb b/test/test_repeater_day_name.rb index 7fa7f0da..82524acc 100644 --- a/test/test_repeater_day_name.rb +++ b/test/test_repeater_day_name.rb @@ -8,12 +8,12 @@ def setup def test_match token = Chronic::Token.new('saturday') - repeater = Chronic::Repeater.scan_for_day_names(token) + repeater = Chronic::Repeater.scan_for_day_names(token, locale: Chronic::Locale.by_name(:en)) assert_equal Chronic::RepeaterDayName, repeater.class assert_equal :saturday, repeater.type token = Chronic::Token.new('sunday') - repeater = Chronic::Repeater.scan_for_day_names(token) + repeater = Chronic::Repeater.scan_for_day_names(token, locale: Chronic::Locale.by_name(:en)) assert_equal Chronic::RepeaterDayName, repeater.class assert_equal :sunday, repeater.type end From 8d28e5ed2fa946352fbc0565c8ec4746b8ae0e5f Mon Sep 17 00:00:00 2001 From: dizer Date: Tue, 2 Sep 2014 19:09:09 +0800 Subject: [PATCH 2/2] Localization --- lib/chronic/locales/ru.rb | 3 ++- test/helper.rb | 4 ++-- test/test_parsing_localized_ru.rb | 36 ++++++++++++++++--------------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/lib/chronic/locales/ru.rb b/lib/chronic/locales/ru.rb index 266806e4..09e7081e 100644 --- a/lib/chronic/locales/ru.rb +++ b/lib/chronic/locales/ru.rb @@ -34,6 +34,7 @@ class Chronic::Locale::Ru < Chronic::Locale scan_for_units: { /^лет?$/ => :year, /^год(а)?$/ => :year, + /^г\.$/ => :year, /^сезон(ов)?$/ => :season, /^месяц(ев)?$/ => :month, /^недел(я|ь)$/ => :week, @@ -79,7 +80,7 @@ class Chronic::Locale::Ru < Chronic::Locale def pre_normalize(text) - text = text.to_s.downcase + 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!(/\./, ':') diff --git a/test/helper.rb b/test/helper.rb index cb961435..e042add6 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -1,7 +1,7 @@ -unless defined? Chronic +# unless defined? Chronic $:.unshift File.expand_path('../../lib', __FILE__) require 'chronic' -end +# end require 'minitest/autorun' diff --git a/test/test_parsing_localized_ru.rb b/test/test_parsing_localized_ru.rb index 8558958e..c63ada6c 100644 --- a/test/test_parsing_localized_ru.rb +++ b/test/test_parsing_localized_ru.rb @@ -1,6 +1,8 @@ +$:.unshift '.' require 'helper' -class TestParsingLocalizedRu < TestCase +# class TestParsing < TestCase +class TestParsingLocalizedRu < Minitest::Test # Wed Aug 16 14:00:00 UTC 2006 TIME_2006_08_16_14_00_00 = Time.local(2006, 8, 16, 14, 0, 0, 0) @@ -10,22 +12,22 @@ def setup def test_handle_rmn_sd time = parse_now("авг 3") - assert_equal Time.local(2007, 8, 3, 12), time + assert_equal Time.local(2007, 8, 3, 0), time time = parse_now("авг. 3") - assert_equal Time.local(2007, 8, 3, 12), time + assert_equal Time.local(2007, 8, 3, 0), time time = parse_now("авг 20") - assert_equal Time.local(2006, 8, 20, 12), time + assert_equal Time.local(2006, 8, 20, 0), time time = parse_now("авг 20", :context => :future) - assert_equal Time.local(2006, 8, 20, 12), time + assert_equal Time.local(2006, 8, 20, 0), time time = parse_now("мая 27") - assert_equal Time.local(2007, 5, 27, 12), time + assert_equal Time.local(2007, 5, 27, 0), time time = parse_now("мая 28", :context => :past) - assert_equal Time.local(2006, 5, 28, 12), time + assert_equal Time.local(2006, 5, 28, 0), time time = parse_now("мая 28 5 вечера", :context => :past) assert_equal Time.local(2006, 5, 28, 17), time @@ -39,14 +41,17 @@ def test_handle_rmn_sd time = parse_now("мая 28 в 5:32:19.764") assert_in_delta Time.local(2007, 5, 28, 17, 32, 19, 764000), time, 0.001 - time = parse_now("1 апр 2014", now: nil) - assert_equal Time.local(2014, 4, 1, 12), time + time = parse_now("1 апр 2014") + assert_equal Time.local(2014, 4, 1, 0), time - time = parse_now("1 апр. 2014", now: nil) - assert_equal Time.local(2014, 4, 1, 12), time + time = parse_now("1 апр. 2014") + assert_equal Time.local(2014, 4, 1, 0), time - time = parse_now("1 апреля 2014", now: nil) - assert_equal Time.local(2014, 4, 1, 12), time + time = parse_now("1 апреля 2014") + assert_equal Time.local(2014, 4, 1, 0), time + + time = parse_now("16 Июн 2014 г.") + assert_equal Time.local(2014, 6, 16, 0), time end def test_handle_rmn_sd_on @@ -71,9 +76,6 @@ def test_handle_rmn_sd_on private def parse_now(string, options={}) - Chronic.parse(string, {:now => TIME_2006_08_16_14_00_00, :locale => :ru }.merge(options)) - end - def pre_normalize(s) - Chronic::Parser.new.pre_normalize s + Chronic.parse(string, {now: TIME_2006_08_16_14_00_00, locale: :ru, guess: :begin}.merge(options)) end end