Why does Ruby think there are 366 days in 100 AD? - ruby

The following expression returns '366' in Ruby, implying 100 AD is a leap year (which it is not):
(Date.ordinal(101) - Date.ordinal(100)).to_i
Same with DateTime.
However, Date.leap?(100) correctly returns false.
Same results for version 1.9.1. and 2.0.0.
What gives? Should I file a bug report?
Update
Also, apparenly 1582 AD is 10 days short!
(Date.ordinal(1583) - Date.ordinal(1582)).to_i
=> 355

According to Wikipedia, 100 was indeed a leap year and 1582 did indeed skip 10 days.

Apparently, prior to 1582-10-15, Ruby interprets dates as Julian calendar dates, instead of Gregorian calendar dates. More details here:
http://teleologi.blogspot.com/2013/05/ruby-times-dates-good-bad-and-so-on.html
Apparently not a bug, but definitely violates the principle of least surprise (at least in the eyes of this coder).
How confusing!
Edit
Debate about "reasonable defaults" aside, Ruby seems to quite flexible on these touchy issues of calendar-choice, compared to other languages. I've learned that the Date and DateTime constructors can receive a "reform date" constant, which determines when the transition from Julian to Gregorian calendar occurs. The default is ITALY (1582-10-15), but ENGLAND is also an option (the jump occurs at 1752-09-14).
To avoid surprises from transitioning between calendars, I should have used the Gregorian calendar for all dates:
(Date.ordinal(i+1,1,Date::GREGORIAN) - Date.ordinal(i,1,Date::GREGORIAN)).to_i
=> 365
Also, Date.leap?(100) returned "false" because it is an alias of Date.gregorian_leap?. Meanwhile, Date.julian_leap(100) returns true. To avoid surprises, probably best to use method version of leap?, which uses whichever reform date you've picked.
Date.new(100, 1, 1, Date::JULIAN).leap?
=> true
Date.new(100, 1, 1, Date::GREGORIAN).leap?
=> false

Related

Validate dates before 1970-01-01

How can I validate date before 1970-01-01 (like 1900-01-01)?
When I try to use CDateVaildator it ends with CTimestamp::getTimestamp() (I found it with help of debugger)
return #mktime($hr,$min,$sec,$mon,$day,$year)
where $hr=0, $min=0, $sec=0, $mon=1, $dat=1, $year=1900 which obviously returns false and this fails whole validation.
CTimestamp::getTimestamp() will generate a timestamp for any dates from 1901 (not 1900!) up to 2038. For dates between 1901 and 1970 it generates a negative number. Just tested it, and the earliest date I can get to work is 14th December 1901. Anything before this throws an error. Not sure how to deal with dates outside this range!
In Yii there is an error described here. The solution is presented there but for some reason it's not included in Yii 1.1.14 although it's marked as committed to trunk.
To make long story short - the fix tries to build timestamp only when there is a property in rules defined.
To use it in own project you have to extend CDateValidator and CDateTimeParser.

How do I get a file's age in days in Ruby?

How would I get the age of a file in days in Ruby?
NOTE that I need a way to accurately get the age of a given file; this means that leap years need to be taken into account.
I need this for a program that removes files after they reach a certain age in days, such as files that are 20 days or older.
And by age, I mean the last access time of a given file, so if a file hasn't been accessed in the past 20 days or more, it gets deleted.
In Perl, I know that you can use date::calc to calculate a date in terms of days since 1 AD, and I used to have a Common-Lisp program that used the Common-Lisp implementation of date::calc, but I don't have that anymore, so I've been looking for an alternative and Ruby seems to have the required capability.
path = '/path/to/file'
(Time.now - File.stat(path).mtime).to_i / 86400.0
#=> 1.001232
Here is the implementation of my above comment, it returns a floating point number expressing the number of days passed.
I know it is an old question, but I needed the same and came up with this solution that might be helpful for others.
As the difference is in days, there is not need to directly deal with seconds.
require 'date'
age_in_days = (Date.today - File.mtime(path).to_date).to_i
if age_in_days > 20
# delete logs
end
If using Rails, you can take advantage of ActiveSupport:
if File.mtime(path) < 20.days.ago
# delete logs
end
If you aren't using Rails, Eduardo's solution above would be my pick.

Why is my YouTube API Insight report query 404ing?

I have a Ruby script which downloads YouTube Insight reports for specific videos within specific date ranges. It authorizes with ClientLogin, retrieves <entry> XML data for a video, and extracts from that data a URL which points to a CSV report:
http://insight.youtube.com/video-analytics/csvreports
?query={VIDEO_ID}
&type=v
&starttime=1315353600000
&endtime=1317772800000
&user_starttime=1317168000000
&user_endtime=1317772800000
&region=world
&token={API_TOKEN}
&hl=en_US
The above URL works. However, I want a report for a specific date range, not the default range provided.
An Insight report query's requested date range is set in the user_starttime and user_endtime params. (In the above default case, it's 2011-09-27 through 2011-10-04.) The YouTube API docs say that you can specify your own date range (covering a span of up to 28 days) by substituting timestamps (in milliseconds) that represent the dates you want.
So, why does the following query 404?
http://insight.youtube.com/video-analytics/csvreports
?query={VIDEO_ID}
&type=v
&starttime=1315353600000
&endtime=1317772800000
&user_starttime=1307937600000
&user_endtime=1308110400000
&region=world
&token={API_TOKEN}
&hl=en_US
The date range looks OK:
ruby > Time.at 1307937600
=> 2011-06-13 00:00:00 -0400
ruby > Time.at 1308110400
=> 2011-06-15 00:00:00 -0400
The fact that the "default" URL works indicates that I must be doing something wrong with these date values, but I can't figure out what. What am I overlooking?
UPDATE 2 -- Nov. 4, 2011:
There has recently been a change to the YouTube Data API Protocol for Insight data, and the answer below is no longer accurate.
YouTube now permits date ranges of up to 31 days for a single Insight report query, and they now allow requests pertaining to dates going back to March 1, 2009. The docs now say:
You can adjust the date range for which a report contains data
to a period of up to 31 days beginning no earlier than March 1, 2009.
This is excellent news. (Leaving the below for posterity.)
I believe I've figured out why the above Insight report query with the custom date range is 404ing. I was able to retrieve a report with a custom date range like this:
http://insight.youtube.com/video-analytics/csvreports
?query={VIDEO_ID}
&type=v
&starttime=1315353600000
&endtime=1317772800000
&user_starttime=1315627200000
&user_endtime=1315972800000
&region=world
&token={API_TOKEN}
&hl=en_US
What's the difference between this (good) URL and the previous (bad) URL in my question above? Well, my custom date range here, specified in the user_starttime and user_endtime parameters both fall within the range set by YouTube in the starttime and endtime params.
In other words, starttime and endtime seem to represent the outer bounds of any user-specified custom date range possible through these types of requests.
So, when the YouTube API docs say:
You can adjust the date range for which a report contains data to a period of up to 28 days
What they mean, I suppose, is:
Your date range may not span more than 28 days ... AND ALSO ...
Your date range may not fall outside of a date range going back 28 days from the most recent date on which reports are currently available, which we tell you through the starttime and endtime parameters.
UPDATE:
In this thread, a YouTube API Team member says:
You should consider the starttime value the absolute earliest supported start date and endtime the absolute latest supported end date. If you try to set user_starttime to something earlier than starttime then you're going to ask for data that dates back more than 28 days, and that data isn't available.
This exact text should be in the documentation.

How to get time_zone_options_for_select with DST offsets?

The ActionView::Helpers::FormOptionsHelper provides the time_zone_options_for_select to get a list of options for a select control that includes all of the timezones with their UTC offset. The problem I'm having is how to get it to display the correct offset for when daylight savings time is in effect?
For instance U.S. Mountain time is usually -7 UTC but during the summer it's effectively -6 UTC. Is there a way to have that list correctly reflect that?
TL;DR
The time zone data you are receiving is "correct" because ActiveSupport::TimeZone and TZInfo::Timezone instances do not make assumptions about the current date, and therefore applying DST to them doesn't make "sense" in the context of the responsibility of those objects.
You notice the problem because the default model for time_zone_options_for_select, ActiveSupport::TimeZone, unfortunately returns the offset string when calling #to_s, which, if the location is currently is observing DST, will be incorrect. There is no way to fix the string values generated in the options, or even remove the offset from them.
If this isn't acceptable you'll need to skip on using time_zone_options_for_select, and use options_for_select instead, and generate the options yourself.
Some investigation
time_zone_options_for_select uses ::ActiveSupport::TimeZone as the default model parameter, so passing it in manually will not change your results. In order to construct options for a select box, that method will construct an array of tuples in the format of [time_zone.to_s, time_zone.name], for the purpose of passing it to the more generic options_for_select method. time_zone, in this case, is an instance of ::ActiveSupport::TimeZone.
The important factor here is that this time zone instance object is, conceptually, completely unrelated/divorced from the idea of "the current date". The definition of a time zone (strictly speaking) has nothing to do with the current date. We can confirm this "not using DST" issue like so:
::ActiveSupport::TimeZone.all.find { |tz| tz.name == "Adelaide" }.utc_offset
=> 34200 # 9 hours and 30 minutes, in seconds
Adelaide's non-DST time zone is ACST (Australian Central Standard Time) which is GMT+9.5. Currently (as in the time of writing), Adelaide is in DST which means they are on ACDT (Australian Central Daylight Time), which is GMT+10.5.
::ActiveSupport::TimeZone.all.find { |tz| tz.name == "Adelaide" }.now.utc_offset
=> 37800 # 10 hours and 30 minutes, in seconds
The crucial difference here is essentially what I've outlined above - the ActiveSupport::TimeZone instance is just not concerned with the current date. The class itself is a convenience wrapper around a TZInfo::DataTimezone instance, which has similar opinions on the current date - none.
If you noticed, in the second code snippet above, we called #now on the time zone object before calling #utc_offset. This returns an ActiveSupport::TimeWithZone instance, which includes information about the time zone, as well as the current date - and therefore we get an offset which reflects the fact that the current date should include the DST offset.
So, the only problem here is that including the UTC offset string in the return value of #to_s on ActiveSupport::TimeZone instances is sort of misleading in this instance. It's included because it's the "base" UTC offset for that time zone.
Resources:
Rails 6.1 time_zone_options_for_select implementation
Related GitHub issue, rails/rails#7297
Related GitHub pull request, rails/rails#22243
I had similar problem but ended up using this
time_zone_select('time_zone', TZInfo::Timezone.us_zones,
:default => "America/Los_Angeles",
:model => TZInfo::Timezone
Did you find a better solution?

Yearless Ruby dates?

Is there a way to represent dates like 12/25 without year information? I'm thinking of just using an array of [month, year] unless there is a better way.
You could use the Date class and hard set the year to a leap year (so that you could represent 2/29 if you wanted). This would be convenient if you needed to perform 'distance' calculations between two dates (assuming that you didn't need to wrap across year boundaries and that you didn't care about the off-by-one day answers you'd get when crossing 2/29 incorrectly for some years).
It might also be convenient because you could use #strftime to display the date as (for example) "Mar-3" if you wanted.
Depending on the usage, though, I think I would probably represent them explicitly, either in a paired array or something like YearlessDate = Struct.new(:month,:day). That way you're not tempted to make mistakes like those mentioned above.
However, I've never had a date that wasn't actually associated with a year. Assuming this is the case for you, then #SeanHill's answer is best: keep the year info but don't display it to the user when it's not appropriate.
You would use the strftime function from the Time class.
time = Time.now
time.strftime("%m/%d")
While #Phrogz answer makes perfect sense, it has a downside:
YearlessDate = Struct.new(:month,:day)
yearless_date = YearlessDate.new(5, 8)
This interface is prone to MM, DD versus DD, MM confusion.
You might want to use Date instead and consider the year 0 as "yearless date" (provided you're not a historian dealing with real dates around bc/ad of course).
The year 0 is a leap year and therefore accommodates every possible day/month duple:
Date.parse("0000-02-29").leap? #=> true
If you want to make this convention air tight, just define your own class around it, here's a minimalistic example:
class YearlessDate < Date
private :year
end
The most "correct" way to represent a date without a year is as a Fixnum between 001 and 365. You can do comparisons on them without having to turn it into a date, and can easily create a date for a given year as needed using Date.ordinal

Resources