From 9c36b2614a56de11bb3ad3e46c2915a5a14941df Mon Sep 17 00:00:00 2001 From: Rocco Nicholls Date: Tue, 29 Oct 2024 22:26:36 -0600 Subject: [PATCH] Refactored Jarvis time parsing for some reason --- app/javascript/jil/statement.js | 2 ++ app/service/jarvis/schedule_parser.rb | 1 + app/service/jarvis/times.rb | 30 ++++++++++++++++++--------- app/service/jil/parser.rb | 12 ++++------- spec/services/jarvis_spec.rb | 1 + 5 files changed, 28 insertions(+), 18 deletions(-) diff --git a/app/javascript/jil/statement.js b/app/javascript/jil/statement.js index 261d6d94..dfabe5af 100644 --- a/app/javascript/jil/statement.js +++ b/app/javascript/jil/statement.js @@ -315,6 +315,8 @@ export default class Statement { throw new Error("Name must match [_a-z0-9]") } else if (!newname.match(/^[_a-z]/)) { throw new Error("Name must begin with a lowercase letter!") + } else if (!newname.match(/^(nil|null|true|false)$/)) { + throw new Error("Name cannot be a reserved word!") } else if (Statement.nameTaken(newname, this)) { throw new Error("Name has already been taken!") } diff --git a/app/service/jarvis/schedule_parser.rb b/app/service/jarvis/schedule_parser.rb index f1c59da4..fc72f9cd 100644 --- a/app/service/jarvis/schedule_parser.rb +++ b/app/service/jarvis/schedule_parser.rb @@ -28,6 +28,7 @@ def relative_time end def valid_words? + # context can still be overridden with `x time ago` time_str, @scheduled_time = Jarvis::Times.extract_time(@msg.downcase.squish, context: :future) return false unless @scheduled_time diff --git a/app/service/jarvis/times.rb b/app/service/jarvis/times.rb index 5c69a151..3d7bc5e3 100644 --- a/app/service/jarvis/times.rb +++ b/app/service/jarvis/times.rb @@ -13,10 +13,12 @@ def extract_time(words, chronic_opts={}) day_words = (Date::DAYNAMES + Date::ABBR_DAYNAMES + [:today, :tomorrow, :yesterday, :morning, :night, :afternoon, :evening, :tonight]).map { |w| w.to_s.downcase.to_sym } day_words_regex = rx.words(day_words) time_words = [:second, :minute, :hour, :day, :week, :month, :year] - time_words_regex = rx.words(time_words, suffix: "s?") - iso8601_regex = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}-07:00/ - time_str = words[/\b(in) (#{drx}|an?) #{time_words_regex}(( and)?( a)?( half)?)?( (and )?#{drx}( #{time_words_regex})?)?/] - time_str ||= words[/\b(in) (#{drx}|an?)( (and )?(a )?half( #{time_words_regex})?)?/] + rel_words_regex = rx.words(time_words, suffix: "s?") + iso8601_regex = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}-\d{2}:\d{2}/ + and_some = /(?: (?:and )?(?:an? )?(#{drx})?(?: ?\bhalf\b ?)?(?: (#{rel_words_regex}))?)?/ + time_words_regex = /(#{rel_words_regex})#{and_some}/ + time_str = words[/\bin (#{drx}|an?) #{time_words_regex}(?: ?(#{drx}))?/] + time_str ||= words[/\bin (#{drx}|an?)#{and_some}(#{rel_words_regex})/] time_str ||= words[/(\b(?:on|next|last) )?(#{month_words_regex} \d{1,2}(\w{2})?(,? '?\d{2,4})? )?((in the )?(#{day_words_regex} ?)+ )?\b(at) \d+:?\d*( ?(am|pm))?( (#{day_words_regex} ?)+)?/] time_str ||= words[/(\b(?:on|next|last) )?#{month_words_regex} \d{1,2}(\w{2})?(,? '?\d{2,4})?/] time_str ||= words[/(\b(?:on|next|last))(?:^| )?\d{1,2}\/\d{1,2}(\/(\d{2}|\d{4})\b)?/] @@ -42,11 +44,12 @@ def extract_time(words, chronic_opts={}) parsed_time += 12.hours end else - m = time_str.match(/in (#{drx}) (#{time_words_regex}) ?(?:and )?(\d*) ?(#{time_words_regex})?/) + m = time_str.match(/(#{drx})\s+(#{rel_words_regex})#{and_some}/) parsed_time = m&.to_a&.then { |_, n1, t1, n2, t2| - t = Time.current - t += (n1 || 1).to_f.send(t1 || :hours) - t += n2.to_i.send(t2 || :minutes) + interval = (n1 || 1).to_f.send(t1 || :hours) + interval += n2.to_i.send(t2 || :minutes) + interval = chronic_opts[:context] == :past ? -interval : interval + Time.current + interval } end @@ -55,14 +58,21 @@ def extract_time(words, chronic_opts={}) end def safe_date_parse(timestamp, chronic_opts={}) + # Force override the context if using `in x time` or `x time ago` + chronic_opts[:context] = :future if timestamp.match?(/^\s*in\b/) + chronic_opts[:context] = :past if timestamp.match?(/\b(from now|ago)\s*$/) opts = chronic_opts.reverse_merge(ambiguous_time_range: 8) ::Chronic.time_class = ::ActiveSupport::TimeZone.new("Mountain Time (US & Canada)") - ::Chronic.parse(timestamp, opts).then { |time| - next if time.nil? + ::Chronic.parse(timestamp, opts)&.then { |time| skip = timestamp.match?(/(a|p)m/) ? 24.hours : 12.hours time += skip while chronic_opts[:context] == :future && time < Time.current time -= skip while chronic_opts[:context] == :past && time > Time.current time } + rescue => e + ### Rescue various Chronic errors, specifically + # -- https://github.com/mojombo/chronic/issues/415 + # ::Chronic.parse("3 hours and 30 minutes before") + # → NoMethodError: undefined method `start=' for nil:NilClass end end diff --git a/app/service/jil/parser.rb b/app/service/jil/parser.rb index 9a040735..5891c4e7 100644 --- a/app/service/jil/parser.rb +++ b/app/service/jil/parser.rb @@ -94,14 +94,10 @@ def self.syntax_highlighting(code, tk=nil) "\e[#{COLORS[:string]}m", ].join } - when /^\d+$/ - col[:numeric, arg] - when /^(nil|null|true|false)$/ - col[:constant, arg] - when /^\w+$/ - col[:variable, arg] - else - col[:err, "[Invalid String?]#{arg.inspect}"] + when /^\d+$/ then col[:numeric, arg] + when /^(nil|null|true|false)$/ then col[:constant, arg] # reserved words + when /^\w+$/ then col[:variable, arg] + else col[:err, "[Invalid String?]#{arg.inspect}"] end # else col[:err, "[#{arg.class}]#{arg.inspect}"] end diff --git a/spec/services/jarvis_spec.rb b/spec/services/jarvis_spec.rb index bec509f6..3e005754 100644 --- a/spec/services/jarvis_spec.rb +++ b/spec/services/jarvis_spec.rb @@ -514,6 +514,7 @@ def jarvis(msg, user=@user) "in an hour and a half" => [Time.local(2022, 6, 24, 7, 15), "today at 7:15am"], "in 3 and a half hours" => [Time.local(2022, 6, 24, 9, 15), "today at 9:15am"], "in 3 hours and 30 minutes" => [Time.local(2022, 6, 24, 9, 15), "today at 9:15am"], + "3 hours and 30 minutes ago" => [Time.local(2022, 6, 24, 2, 15), "today at 2:15am"], "in 3.5 hours" => [Time.local(2022, 6, 24, 9, 15), "today at 9:15am"], "tonight" => [Time.local(2022, 6, 24, 22, 0), "today at 10pm"], "at 11:15 tomorrow" => [Time.local(2022, 6, 25, 11, 15), "tomorrow at 11:15am"],