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

Changes to Time.zone are not reflected in Chronic parsing #182

Open
ghiculescu opened this issue Apr 4, 2013 · 23 comments · May be fixed by #214
Open

Changes to Time.zone are not reflected in Chronic parsing #182

ghiculescu opened this issue Apr 4, 2013 · 23 comments · May be fixed by #214
Labels

Comments

@ghiculescu
Copy link

1.9.3p286 :036 > Time.zone
 => (GMT+10:00) Brisbane 
1.9.3p286 :037 > Time.zone.parse '9am'
 => Thu, 04 Apr 2013 09:00:00 EST +10:00 
1.9.3p286 :038 > Chronic.parse '9am'
 => Thu, 04 Apr 2013 09:00:00 EST +10:00 
1.9.3p286 :039 > Time.zone = 'Greenwich'
 => "Greenwich" 
1.9.3p286 :040 > Time.zone.parse '9am'
 => Thu, 04 Apr 2013 09:00:00 GMT +00:00 
1.9.3p286 :041 > Chronic.parse '9am'
 => Thu, 04 Apr 2013 09:00:00 EST +10:00 
1.9.3p286 :042 > Chronic.time_class
 => (GMT+10:00) Brisbane 

My rails app sets Time.zone on a user level, using an around_filter based on the current user. This causes subtle problems in Chronic parsing, which is why I've (reluctantly) stuck to Time.zone.parse so far.

The only solution I can think of is to Chronic.time_class on each request, after changing the Time.zone. But that makes things harder to test and doesn't feel right. I wonder if there is a better way of doing it?

@ghiculescu
Copy link
Author

I tried the suggestion at #158 (comment) but it didn't seem to make a difference (though it's not clear to me if any changes were actually made or if @injekt was just suggesting possible changes)

1.9.3p286 :045 > Chronic.parse('9am', time_class: Time.zone)
 => Thu, 04 Apr 2013 09:00:00 EST +10:00 

@leejarvis
Copy link
Collaborator

Yeah I was suggesting that as a change, it's not yet possible. I'll mark this as a feature.

@mmlac mmlac linked a pull request Sep 19, 2013 that will close this issue
@dnagir
Copy link
Contributor

dnagir commented Jun 25, 2014

Is there a usable workaround currently?

All I can see to work this around is the following:
(assuming time_class as an option for parse isn't supported yet)

def parse_with_tz(value)
  previous = Chronic.time_class
  begin
    Chronic.time_class = Time.zone
    return Chronic.parse(value)
  ensure
    Chronic.time_class = previous
  end
end

I was expecting this NOT to be thread-safe.
However, I could not confirm it on MRI and Rubinius:
https://gist.github.com/dnagir/5329defec514596fdc6e

So I'm a little puzzled as to whether there's a need in syncronisation (Mutex) or not.

@dnagir
Copy link
Contributor

dnagir commented Jun 25, 2014

UPDATE: I've tested it on JRuby and it does indeed break there. So I can confirm that synchronisation is indeed required for the parse_with_tz. For example:

SYNC = Mutex.new

def parse_with_tz(value)
  SYNC.synchronize do
    previous = Chronic.time_class
    begin
      Chronic.time_class = Time.zone
      return Chronic.parse(value)
    ensure
      Chronic.time_class = previous
    end
  end
end

@dnagir
Copy link
Contributor

dnagir commented Jun 25, 2014

UPDATE 2: All implementations will require synchronisation. The fact that MRI and Rubinius didn't in my has no conclusion yet.
This is because the Ruby class vars are generally unsafe as proven by this, simplified test - https://gist.github.com/dnagir/80df45c96b49776dd174

@zxiest
Copy link

zxiest commented Jun 25, 2014

Maybe this will help someone:
http://stackoverflow.com/a/15786663/226255

Chronic.parse("next 5:00 pm", 
:time_class => ActiveSupport::TimeZone.new(User.first.time_zone)).utc

@dnagir
Copy link
Contributor

dnagir commented Jun 25, 2014

I don't know how that SO answer can help because time_class option is Not
supported at all as far as I can see.
On 25 Jun 2014 18:25, "Abdo" [email protected] wrote:

Maybe this will help someone:
http://stackoverflow.com/a/15786663/226255

Chronic.parse("next 5:00 pm",
:time_class => ActiveSupport::TimeZone.new(User.first.time_zone)).utc


Reply to this email directly or view it on GitHub
#182 (comment).

@leejarvis
Copy link
Collaborator

It's correct that this is not thread-safe. There is currently no workaround until we build something into Chronic (see the other issues). This shouldn't be so hard now that parse creates a new Chronic::Parser instance. We an feed the time_class option into the Parser class and let it default back to Chronic.time_class. Probably once this option is added Chronic.time_class should be removed to avoid questioning thread safety (it'll never be thread-safe).

To clarify and reiterate what @dnagir has said; that SO answer is incorrect. I've commented on it.

@dnagir
Copy link
Contributor

dnagir commented Jun 25, 2014

@leejarvis roger that.

I'll see if I can send a PR within a few days or so with all that sorted.

Unless you have it sorted already of course?

@leejarvis
Copy link
Collaborator

@dnagir Honestly, I don't. I haven't managed to find time for Chronic for quite a while. As frustrating as it is, I do plan to find some time to work on it (though I am working on a replacement, too). Work is very busy. Please feel free to submit a PR for it. @davispuh is currently managing a lot of the code also so one of us can review. Happy to expand collaborators. Still hoping to move the repo or sort something out so I don't have to bother @mojombo every time.

@davispuh
Copy link
Collaborator

I'm still waiting for PRs to be reviewed by @leejarvis some could be merged, but this isn't my lib so I'm waiting ;)

@dnagir
Copy link
Contributor

dnagir commented Jul 2, 2014

Do you guys develop Chronic on Ruby 2?

I'm given error even when I try to bundle it:

$ bundle
There was a LoadError while loading chronic.gemspec: 
cannot load such file -- numerizer from
  /Users/dnagir/proj/chronic/chronic.gemspec:2:in `<main>'

Does it try to require a relative path? That's been removed in Ruby 1.9.

@davispuh
Copy link
Collaborator

davispuh commented Jul 2, 2014

it's because gemspec includes whole chronic and thus you get this exception because you don't have numerizer gem, it's fixed in #262 but currently you can just gem install numerizer

@dnagir
Copy link
Contributor

dnagir commented Jul 2, 2014

Well, I just had a closer look at the parser.rb file.

This whole drama can be avoided easily by using now option.

This makes me wonder what's the point of the time_class option at all then?
What should happen if both now and time_class options are given?

The solution to the whole issue is as simple as: Chronic.parse '9am', now: Time.zone.now

@dnagir
Copy link
Contributor

dnagir commented Jul 2, 2014

In the meanwhile, this PR would make it clear about the time_class support: #269

@dnagir
Copy link
Contributor

dnagir commented Jul 2, 2014

Well, I see why now now options doesn't work (which it should) to fix this issue.

The time_class is being used all over the place:

lib/chronic/date.rb
66:      start_year = Chronic.time_class.now.year - bias

lib/chronic/handlers.rb
10:      day_start = Chronic.time_class.local(year, month, day)
78:        day_start = Chronic.time_class.local(year, month, day)
126:        end_time = Chronic.time_class.local(next_month_year, next_month_month)
127:        Span.new(Chronic.time_class.local(year, month), end_time)
135:      t = Chronic.time_class.parse(options[:text])
151:        day_start = Chronic.time_class.local(year, month, day)
168:        day_start = Chronic.time_class.local(year, month, day)
185:        day_start = Chronic.time_class.local(year, month, day)
209:        day_start = Chronic.time_class.local(year, month, day)
240:        day_start = Chronic.time_class.local(year, month, day)
241:        day_start = Chronic.time_class.local(year + 1, month, day) if options[:context] == :future && day_start < now
265:        end_time = Chronic.time_class.local(next_month_year, next_month_month)
266:        Span.new(Chronic.time_class.local(year, month), end_time)
297:          start_time = Chronic.time_class.local(year, month.index, day)
301:          day_start = Chronic.time_class.local(year, month.index, day)
318:        start_time = Chronic.time_class.local(year, month.index, day)
340:          start_time = Chronic.time_class.local(year, month, day)
344:          day_start = Chronic.time_class.local(year, month, day)
363:          start_time = Chronic.time_class.local(year, month.index, day)
367:          day_start = Chronic.time_class.local(year, month.index, day)
384:        start_time = Chronic.time_class.local(year, month.index, day)
399:        time = Chronic.time_class.local(year, month, day, h, m, s)
400:        end_time = Chronic.time_class.local(year, month, day + 1, h, m, s)
402:        time = Chronic.time_class.local(year, month, day)
404:        end_time = Chronic.time_class.local(year, month, day)
583:      Chronic.time_class.local(*date_parts)

lib/chronic/parser.rb
54:      @now = options.delete(:now) || Chronic.time_class.now

lib/chronic/repeaters/repeater_day.rb
14:        @current_day_start = Chronic.time_class.local(@now.year, @now.month, @now.day)

lib/chronic/repeaters/repeater_time.rb
79:        midnight = Chronic.time_class.local(@now.year, @now.month, @now.day)

lib/chronic/repeaters/repeater_week.rb
40:        this_week_start = Chronic.time_class.local(@now.year, @now.month, @now.day, @now.hour) + RepeaterHour::HOUR_SECONDS
47:        this_week_end = Chronic.time_class.local(@now.year, @now.month, @now.day, @now.hour)

lib/chronic/time.rb
31:      offset = Chronic.time_class.now.to_time.utc_offset unless offset # get current system's UTC offset if offset is nil

lib/chronic.rb
69:    #   Chronic.time_class = Time.zone
74:    attr_accessor :time_class
78:  self.time_class = ::Time
140:    if Chronic.time_class.name == "Date"
141:      Chronic.time_class.new(year, month, day)
142:    elsif not Chronic.time_class.respond_to?(:new) or (RUBY_VERSION.to_f < 1.9 and Chronic.time_class.name == "Time")
143:      Chronic.time_class.local(year, month, day, hour, minute, second)
145:      offset = Time::normalize_offset(offset) if Chronic.time_class.name == "DateTime"
146:      Chronic.time_class.new(year, month, day, hour, minute, second, offset)

README.md
152:>> Chronic.time_class = Time.zone

test/test_chronic.rb
133:    org = Chronic.time_class
135:      Chronic.time_class = ::Time
139:      Chronic.time_class = org
144:    org = Chronic.time_class
146:      Chronic.time_class = ::Date
149:      Chronic.time_class = org
154:    org = Chronic.time_class
156:      Chronic.time_class = ::DateTime
160:      Chronic.time_class = org
168:    org = Chronic.time_class
172:      Chronic.time_class = ::Time.zone
175:      Chronic.time_class = ::Time.zone
178:      Chronic.time_class = org

So whether we add it not it as an option, or whether we use now option or not... is irrelevant because the global Chronic.time_class is still being used everywhere.

I'm giving up on this :(

@davispuh
Copy link
Collaborator

davispuh commented Jul 2, 2014

now and time_class are different options. You specify now to tell Chronic at which time it should treat things like "Today", "Tomorrow" etc., but with time_class you specify which Time class you want output to be in, eg. Time or Date class

EDIT: So I think correct behavior would be that when parsing 'Today at 5pm' it would use same Timezone as used for now option for parsing/input, but output would be in Timezone which is for time_class

@bjfish
Copy link

bjfish commented Aug 23, 2015

Would there be any issue with doing the following in rails?

             Time.use_zone('Pacific Time (US & Canada)') do
                 Chronic.time_class = Time.zone
                 Chronic.parse("Sunday")
             end

Thank you

@chewi
Copy link

chewi commented Jun 20, 2016

As @kbaum pointed out in the other issue, Time.zone= is thread local. What @bjfish proposed above is still not safe as Chronic.time_class may get called by other threads during that block, however there is one easy workaround. Stick this in an initializer.

module Chronic
  def self.time_class
    ::Time.zone
  end
end

Then you can safely do this.

Time.use_zone('Pacific Time (US & Canada)') do
  Chronic.parse("Sunday")
end

@espen
Copy link

espen commented Nov 10, 2016

That looks like a good workaround @chewi ,have you been using this in production?

@chewi
Copy link

chewi commented Nov 10, 2016

Unfortunately I've completely forgotten why I looked into this. I don't believe we're using that workaround in production at the moment.

@mphalliday
Copy link

@espen we've been using @chewi's workaround for a few weeks in production now. Working well for us.

@espen
Copy link

espen commented Nov 11, 2016

great @mphalliday, thanks for the feedback. My tests so far looks good. Thanks for sharing @chewi

BFF76o added a commit to BFF76o/chronic that referenced this issue Sep 4, 2024
Theis will be usefull to eliminate all
the possible confusion going on what options
are and are not supported by chronic.

One such example is:
mojombo/chronic#182 (comment)

It is a good idea to tell people know about it in advance
if they provide invalid or unsupported options.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants