Rails absolute time from reference + french "ago" string - ruby-on-rails

I need to reimport some data that was exported using the "ago" stringification helper, in French.
I have a reference Time/DateTime date at which the import was done, and from there I need to substract this "time ago" difference to find the absolute time.
I need to code the parse_relative_time method below
Some sample input/output of what I'm trying to achieve
IMPORT_DATE = Time.parse('Sat, 11 Jun 2016 15:15:19 CEST +02:00')
sample_ago_day = 'Il y a 5j' # Note : 'Il y a 5j" = "5d ago"
parse_relative_time(from: IMPORT_DATE, ago: sample_ago_day)
# => Should output sthing like Sat, 6 Jun 2016 (DateTime object)
sample_ago_month = 'Il y a 1 mois' # Note : 'Il y a 5j" = "1 month ago"
parse_relative_time(from: IMPORT_DATE, ago: sample_ago_month)
# => 11 May 2016 (it's not big deal if it's 10 or 11 or 12 because of months with odd numbers, just need something approximate)
EDIT
Range of values
"il y a xj" -> x belongs to (1..31)
"il y a y mois" -> y belongs to (2..10) and "un"
(for one)

Let's divide the problem into 2 sub-tasks:
Parse the 'ago' string
Since there is no reversible way in ruby to parse an 'ago' string, lets use regular expressions to extract the data as seconds:
def parse_ago(value)
# If the current value matches 'il y a Xj'
if match = /^il y a (.*?)j$/i.match(value)
# Convert the matched data to an integer
value = match[1].to_i
# Validate the numeric value (between 1 and 31)
raise 'Invalid days value!' unless (1..31).include? value
# Convert to seconds with `days` rails helper
value.days
# If the current value matches 'il y a X mois'
elsif match = /^il y a (.*?) mois$/i.match(value)
# If the matched value is 'un', then use 1. Otherwise, use the matched value
value = match[1] == 'un' ? 1 : match[1].to_i
# Validate the numeric value (between 1 and 10)
raise 'Invalid months value!' unless (1..10).include? value
# Convert to seconds with `months` rails helper
value.months
# Otherwise, something is wrong (or not implemented)
else
raise "Invalid 'ago' value!"
end
end
Substract from current time
This is pretty straightforward; once we have the seconds from the 'ago' string; just call the ago method on the seconds extracted from the 'ago' string. An example of usage of this method for Ruby on Rails:
5.months.ago # "Tue, 12 Jan 2016 15:21:59 UTC +00:00"
The thing is, you are substracting it from IMPORT_DATE, and not from current time. For your code, you need to specify the current time to IMPORT_DATE:
parse_ago('Il y a 5j').ago(IMPORT_DATE)
Hope this helps!

Related

formatting function output similar os.date() in lua

I have a function that get current time and do some calculation that return a table. Something like this:
functuin newtime()
t1 = os.date("*t")
-- do some calculation that calculate this variable chha_ye , chha_mo , chha_da
t={}
t["ye"] = chha_ye
t["mo"] = chha_mo
t["da"] = chha_da
return t
end
Now I can get newtime().ye.
I need formatting output of this function something similar os.date()
for example, if chhaa_ye = 4 and chha_mo = 9 :
newtime("%ye")
4
newtime("%ye - %mo")
4 - 9
Default os.date do this, for example
os.date("%Y")
22
os.date("%Y - %m")
22 - 10
How should I do this?
I will provide an answer based on the comment from Egor, slightly modified to make the answer directly testable in a Lua interpreter. First, the os.date function from Lua is quite handy, it returns a hashtable with the corresponding field/values:
if format is the string "*t", then date returns a table with the following fields: year, month (1–12), day (1–31), hour (0–23), min (0–59), sec (0–61, due to leap seconds), wday (weekday, 1–7, Sunday is 1), yday (day of the year, 1–366), and isdst (daylight saving flag, a boolean). This last field may be absent if the information is not available.
We could test the function with the following code snippet:
for k,v in pairs(os.date("*t")) do
print(k,v)
end
This will print the following:
month 10
year 2022
yday 290
day 17
wday 2
isdst false
sec 48
min 34
hour 9
The gsub function, intended for string substitutions, is not limited to a single string, but could take a table as argument.
string.gsub (s, pattern, repl [, n])
If repl is a table, then the table is queried for every match, using the first capture as the key.
function my_stringformat (format)
local date_fields = os.date("*t")
local date_string = format:gsub("%%(%a+)", date_fields)
return date_string
end
The function can be used as most would expect:
> my_stringformat("%year - %month")
2022 - 10
Obviously, it's trivial to rename or add the fields names:
function my_stringformat_2 (format)
local date_fields = os.date("*t")
-- Rename the fields with shorter name
date_fields.ye = date_fields.year
date_fields.mo = date_fields.month
-- Delete previous field names
date_fields.year = nil
date_fields.month = nil
-- Interpolate the strings
local date_string = format:gsub("%%(%a+)", date_fields)
-- return the new string
return date_string
end
And the behavior is the same as the previous one:
> my_stringformat_2("%ye - %mo")
2022 - 10

mapping date with unique id ruby on rails

I want to write an action where I can return one quote per day from DB. my db look like this
|id|quote|
so,if I call this action today it should return the same quote throughout the day but tomorrow it should return another quote from the db. I thought of mapping the quote ids to the today's date but not sure how to do it.
You can get consecutive quotes via:
Quote.order(:id).offset(0).first
Quote.order(:id).offset(1).first
Quote.order(:id).offset(2).first
# ...
To get a quote for the current day, you can utilize Date#jd which returns the date's Julian day:
Date.parse('2020-06-01').jd #=> 2459002
Date.parse('2020-06-02').jd #=> 2459003
Date.parse('2020-06-03').jd #=> 2459004
Since you (probably) don't have that many quotes, this value needs to be transformed into a value we can pass to offset, i.e. a value between 0 and the total number of quotes. This is where the modulo operator % can help – it returns just that, looping over at the end: (e.g. for 22 quotes)
Date.parse('2020-06-01').jd % 22 #=> 18
Date.parse('2020-06-02').jd % 22 #=> 19
Date.parse('2020-06-03').jd % 22 #=> 20
Date.parse('2020-06-04').jd % 22 #=> 21
Date.parse('2020-06-05').jd % 22 #=> 0
Date.parse('2020-06-06').jd % 22 #=> 1
Putting it all together:
require 'date'
def quote_of_the_day(date = Date.current)
offset = date.jd % Quote.count
Quote.order(:id).offset(offset).first
end
Note that this runs two queries: one to get the total number of quotes and another one to get the quote for the given date. You might want to cache the result.
Also note that adding new quotes might have unexpected results because the modulo operation would return a totally different offset:
Date.parse('2020-06-03').jd % 22 #=> 20
# after adding a new Quote
Date.parse('2020-06-03').jd % 23 #=> 5
You can compensate this by adjusting the jd result accordingly:
ADJUSTMENT = 15
(Date.parse('2020-06-03').jd + ADJUSTMENT) % 23 #=> 20

How do I write a function to convert duration to time in milliseconds and account for invalid values?

I'm using Rails 5 with Ruby 2.4. I want to convert a string, which represents duration (time) to milliseconds, so I have this function
def duration_in_milliseconds(input)
if input && input.count("a-zA-Z ") == 0
if input.index(".") && !input.index(":")
input.gsub!(/\./, ':')
end
# replace bizarre apostraphes where they intended ":"'s
input.gsub!(/'/, ':')
# Strip out unnecessary white space
input.gsub!(/\A\p{Space}+|\p{Space}+\z/, '')
input.split(':').map(&:to_f).inject(0) { |a, b| a * 60 + b } * 1000
else
0
end
end
This works great for strings like "6:52" (6 mintues, 52 seconds), but also works for things like "52:6" (which is invalid, "52:06" would be correct). I wish to have the function return zero if someone enters in a single digit in any spot other than the first number before the ":". How do I do this? NOte that I still want a duration like "15:24.8" to parse normally (that is 15 minutes, 24 seconds, and eight tenths of a second).
I was trying to come up with a solution for this and ended up reworking the code.
I started splitting the string and removing all the white space. To remove the white space first I ensure that there's no leading nor trailing white space and then I incorporate any amount of extra space before or after the : characters.
Then I assign the values returned from the split to 3 variables I named hr, min and seg. If you're unfamiliar with this, the variables are filled out from left to right and, in case there aren't enough elements, the remaining vars get a nil.
Then, the condition. For this I say: "Return zero if either min or seg don't match two consecutive numeric characters while being present"
And finally, I sum their respective values in seconds, which returns the numer of seconds. And at the end of the line I convert it to milliseconds by multiplying it by 1000
def duration_in_milliseconds(input)
hr, min, seg = input.strip.split(/\s*:\s*/)
return 0 if [min, seg].any? {|value| value && !(value =~ /\d\d/)}
(hr.to_i * 60 * 60 + min.to_i * 60 + seg.to_i) * 1000
end
And I tried it out on a bunch of strings to make sure it worked.
duration_in_milliseconds("0:00:01") # => 1000
duration_in_milliseconds("0:01:01") # => 61000
duration_in_milliseconds("1:01:01") # => 3661000
duration_in_milliseconds("0:15") # => 900000
duration_in_milliseconds("1: 05") # => 3900000
duration_in_milliseconds("1:5") # => 0
duration_in_milliseconds("1:5 ") # => 0
duration_in_milliseconds(" \t 1 : 05 : 15 ") # => 3915000
duration_in_milliseconds("1:5:15") # => 0
duration_in_milliseconds("1") # => 3600000
duration_in_milliseconds("1:5D:15") # => 0
I hope it helped and answered the question (in some way), if not, please don't hessitate to ping me.
Cheers
Fede

Parsing JSON datetime?

I am consuming a JSON API that returns datetime values as a string in the following format:
/Date(1370651105153)/
How can I parse a value like this into a datetime variable in rails?
That appears to be a UNIX timestamp (seconds since epoch). Additionally it appears to be milliseconds since epoch.
So you can convert it like so - given that value is a String that looks like:
value = "/Date(1370651105153)/"
if value =~ /\/Date\((\d+)\)\//
timestamp = $1.to_i
time = Time.at(timestamp / 1000)
# time is now a Time object
end
You need to divide by 1000 since Time#at expects its argument to be seconds and not milliseconds since the epoch.
There are a few errors in andyisnowskynet's answer (https://stackoverflow.com/a/25817039/5633460): There's a minor RegEx error which means it won't detect positive timezones; and it won't support timezones that aren't aligned to full hours such as -0430 (VET). Also, the Time object it returns could be honouring proper Ruby timezone handling instead of adding a time offset, which would be the more-conventional way to do this.
This is my improved version of that answer...
First, for datetime values expressed in this format, the time serial part (e.g. 1370651105153 in OP's example) is ALWAYS the number of milliseconds since 1970-01-01 00:00:00 GMT, and that is not influenced by the presence or absence of a timezone suffix. Hence, any timezone included as part of the string does not change the point in history that this represents. It only serves to state which timezone the "observer" was in at the time.
Ruby Time objects are able to handle these two pieces of information (i.e. the actual datetime "value" and the timezone "metadata"). To demonstrate:
a = Time.at(-1).utc
# => 1969-12-31 23:59:59 UTC
b = Time.at(-1).getlocal('+09:30')
# => 1970-01-01 09:29:59 +0930
a == b
# => true
As you can see, even though both look like different values (owing to their different timezones in which they are expressed), the == equality operator shows they actually reference the same moment in time.
Knowing this, we could improve on parse_json_datetime as follows (correcting the other errors too):
def parse_json_datetime(datetime)
# "/Date(-62135575200000-0600)/" seems to be the default date returned
# if the value is null:
if datetime == "/Date(-62135575200000-0600)/"
# Return nil because it is probably a null date that is defaulting to 0.
# To be more technically correct you could just return 0 here if you wanted:
return nil
elsif datetime =~ %r{/Date\(([-+]?\d+)([-+]\d+)?\)/}
# We've now parsed the string into:
# - $1: Number of milliseconds since the 1/1/1970 epoch.
# - $2: [Optional] timezone offset.
# Divide $1 by 1000 because it is in milliseconds and Time uses seconds:
seconds_since_epoch = $1.to_i / 1000.0
time = Time.at(seconds_since_epoch.to_i).utc
# We now have the exact moment in history that this represents,
# stored as a UTC-based "Time" object.
if $2
# We have a timezone, so convert its format (adding a colon)...
timezone = $2.gsub(/(...)(..)/, '\1:\2')
# ...then apply it to the Time object:
time = time.getlocal(timezone)
end
time
else
raise "Unrecognized date format."
end
end
Then we can test:
# See: http://momentjs.com/docs/#/parsing/asp-net-json-date/
parse_json_datetime("/Date(1198908717056-0700)/")
# => 2007-12-28 23:11:57 -0700
# 1 minute before the epoch, but in ACST:
parse_json_datetime("/Date(-60000+0930)/")
# => 1970-01-01 09:29:00 +0930
# Same as above, but converted naturally to its UTC equivalent:
parse_json_datetime("/Date(-60000+0930)/").utc
# => 1969-12-31 23:59:00 UTC
# Same as above, with timezone unspecified (implying UTC):
parse_json_datetime("/Date(-60000)/")
# => 1969-12-31 23:59:00 UTC
# OP's example:
parse_json_datetime("/Date(1370651105153)/")
# => 2013-06-08 00:25:05 UTC
# Same as above, but stated in two different timezones:
aaa = parse_json_datetime("/Date(1370651105153-0200)/")
# => 2013-06-07 22:25:05 -0200
bbb = parse_json_datetime("/Date(1370651105153-0800)/")
# => 2013-06-07 16:25:05 -0800
# As "rendered" strings they're not the same:
aaa.to_s == bbb.to_s
# => false
# But as moments in time, they are equivalent:
aaa == bbb
# => true
# And, for the sake of the argument, if they're both expressed in the
# same timezone (arbitrary '-04:00' in this case) then they also render
# as equal strings:
aaa.getlocal('-04:00').to_s == bbb.getlocal('-04:00').to_s
The integer seems to be a unixtime (in milliseconds). Just cut the last three digits off and feed the rest to Time.at:
Time.at(1370651105) # => 2013-06-08 04:25:05 +0400
For anyone who needed a bit more robust solution that included a timezone, this is the parser I came up with:
def parse_json_datetime(datetime)
# "/Date(-62135575200000-0600)/" seems to be the default date returned if the value is null
if datetime == "/Date(-62135575200000-0600)/"
# return nil because it is probably a null date that is defaulting to 0.
# to be more technically correct you could just return 0 here if you wanted.
return nil
elsif datetime =~ /\/Date\(([-+]?\d+)(-+\d+)?\)\// # parse the seconds and timezone (if present)
milliseconds_since_epoch = $1
time = Time.at(milliseconds_since_epoch.to_i/1000.0).utc # divide by 1000 because it is in milliseconds and Time uses seconds
if timezone_hourly_offset = $2
timezone_hourly_offset = timezone_hourly_offset.gsub("0","").to_i
time = time+(timezone_hourly_offset*60*60) # convert hourly timezone offset into seconds and add onto time
end
time
else
raise "Unrecognized date format."
end
end
Active Support makes it easy to do this:
#!/usr/bin/env ruby
require 'active_support/json'
require 'active_support/time'
Time.zone = 'Eastern Time (US & Canada)'
ActiveSupport.parse_json_times = true
puts ActiveSupport::JSON.decode('{"hi":"2009-08-10T19:01:02Z"}')
Which will output:
{"hi"=>Mon, 10 Aug 2009 15:01:02 EDT -04:00}
Once you turn the ActiveSupport.parse_json_times flag on, then any JSON payload you decode will get any times automatically convered into ActiveSupport::TimeWithZone objects.
Active Support uses a regex to detect times. You can read the tests to get a good idea of the time formats that it supports. Looks like it supports ISO 8601, but does not support Unix/POSIX/Epoch timestamps.
I was informed of this feature in Active Support when I created this issue against Rails.

Using distance_of_time_in_words in Rails

I have a start month (3), start year (2004), and I have an end year (2008). I want to calculate the time in words between the start and end dates. This is what I'm trying and it's not working..
# first want to piece the start dates together to make an actual date
# I don't have a day, so I'm using 01, couldn't work around not using a day
st = (start_year + "/" + start_month + "/01").to_date
ed = (end_year + "/01/01").to_date
# the above gives me the date March 1st, 2004
# now I go about using the method
distance_of_time_in_words(st, ed)
..this throws an error, "string can't me coerced into fixnum". Anyone seen this error?
You can't just concatenate strings and numbers in Ruby. You should either convert numbers to strings as mliebelt suggested or use string interpolation like that:
st = "#{start_year}/#{start_month}/01".to_date
But for your particular case I think there is no need for strings at all. You can do it like that:
st = Date.new(start_year, start_month, 1)
ed = Date.new(end_year, 1, 1)
distance_of_time_in_words(st, ed)
or even like that:
st = Date.new(start_year, start_month)
ed = Date.new(end_year)
distance_of_time_in_words(st, ed)
See Date class docs for more information.
Given that the context in which you are calling the method is one that knows the methods from ActionView::Helpers::DateHelper, you should change the following:
# first want to piece the start dates together to make an actual date
# I don't have a day, so I'm using 01, couldn't work around not using a day
st = (start_year.to_s + "/" + start_month.to_s + "/01").to_date
ed = (end_year.to_s + "/01/01").to_date
# the above gives me the date March 1st, 2004
# now I go about using the method
distance_of_time_in_words(st, ed)
=> "almost 3 years"
So I have added calls to to_s for the numbers, to ensure that the operation + is working. There may be more efficient ways to construct a date, but yours is sufficient.

Resources