converting a hash string into a date object ruby - ruby

I created a hash out of file that contains date as a string in different formats (like September 1988, the other line would be July 11th 1960, and sometimes year only)
require 'date'
def create_book_hash(book_array)
{
link: book_array[0],
title: book_array[1],
author: book_array[2],
pages: book_array[3].to_i,
date: book_array[4],
rating: book_array[5].to_f,
genre: book_array[6]
}
end
def books_sorted_by_date (books_array)
books_array.sort_by { |key| Date.strptime(key[:date], '%Y, %m') }
end
book_file= File.read("books.txt")
.split("\n")
.map { |line| line.split("|")}
.map { |book_array| create_book_hash(book_array)}
puts books_sorted_by_date(book_file)
I'm trying to sort books by date, so it would be in ascending order by year and since I have different string types, i put a hash key as the first argument in strptime to access all the values in :date . And that gives me \strptime': invalid date (Date::Error).` I don't understand why and what can I do to convert these strings into date objects? (just ruby, no rails)

Handle Both Standard and Custom Date Strings
Date#parse doesn't handle arbitrary strings in all cases. Even when it does, it may not handle them the way you expect. For example:
parse_date "1/1/18"
#=> #<Date: 2001-01-18 ((2451928j,0s,0n),+0s,2299161j)>
While Date#parse handles many date formats automagically, it only successfully parses objects that match its internal expectations. When you have multiple or arbitrary date formats, you have to define your own date specifications using Date#strptime to handle those formats that Date#parse doesn't understand, or that it handles incorrectly. For example:
require 'date'
def parse_date str
Date.parse str
rescue Date::Error
case str
when /\A\d{4}\z/
Date.strptime str, '%Y'
when /\A\d{2}\z/
Date.strptime str, '%y'
else
raise "unexpected date format: #{str}"
end
end
date_samples = ["July 11th 1960", "September 1988", "1776"]
date_samples.map { |date| parse_date(date) }
#=> [#<Date: 1960-07-11 ((2437127j,0s,0n),+0s,2299161j)>, #<Date: 1988-09-01 ((2447406j,0s,0n),+0s,2299161j)>, #<Date: 1776-01-01 ((2369731j,0s,0n),+0s,2299161j)>]
This obviously is not an exhaustive list of potential formats, but you can add more examples to date_samples and update the case statement to include any unambiguous date formats you expect from your data set.

Date.strptime needs two parameters date-string and format of the date. To use strptime you need to know what is the format of the string beforehand.
see some examples here - https://apidock.com/ruby/Date/strptime/class
In your program you don't know exact format of the date on that line when it parses so you need to try something like -
def books_sorted_by_date (books_array)
books_array.sort_by { |key| Date.parse(key[:date]) }
end
Date.parse needs one argument - date string, it then tries to guess the date.
see details - https://apidock.com/ruby/v2_6_3/Date/parse/class
You will still have problems with just year with this approach.

Related

How to use ":template"-type strings in ruby?

Both rails routes and whenever and a few other things I can't remember have a user-specified template string like so:
template_str = "I am going to :place at :time"
And then there's some magic function which embeds data in place of :place and :time, like so:
template_str.magic_embed_function(place: 'bed', time: '10 pm')
#=> "I am going to bed at 10 pm"
How can I do this in my ruby projects? Is there a gem that implements String#magic_embed_function?
Use Percent-Style Interpolation
There is a special type of interpolation that uses the String#% method. This allows you to interpolate ordinal (Array) and non-ordinal (Hash) inputs into a format string similar to that provided by Kernel#sprintf. However, the use of a hash argument with this method enables support for named variables in the format string. As a minimalist example:
"%{foo} %{bar}" % {foo: 'baz', bar: 'quux'}
#=> "baz quux"
With a hash argument, the format-string placeholders are treated as hash keys to be replaced by the associated values in your hash. This makes the order of the variables passed in unimportant. Based on the code in your original post, you could use it as follows:
template_str = 'I am going to %{place} at %{time}.'
template_str % {time: '10:00 PM', place: 'bed'}
#=> "I am going to bed at 10:00 PM."
This is a useful technique when you want to pass an array or hash for interpolation, but may or may not offer advantages over other types of interpolation in the general case. Your mileage may vary.
I extended String class with a magic_embed_function as you asked..rs
It's very simple, first we split our string and collect the words and check if matches with this simple regex for symbols, basically says "if something starts with : , that's a symbol", after we found a symbol we replace using gsub! (global substitution, with the bang to change our object) passing our symbol as first param and the param received that corresponds to that symbol and at the end we return self, to return the string that called the method.
template_str = "I am goind to :place at :time"
class String
def magic_embed_function(params)
self.split(" ").collect do |value|
if value =~ /:.*/
self.gsub! value, params[value[1..value.length].to_sym]
end
end
self
end
end
p template_str.magic_embed_function({place: "bed", time: "10 pm"})
#"I am goind to bed at 10 pm"

Regex date format less than operator

I am trying to use regex to verify a date format and I would like to check if the day is less than 32. Similarly, that the month is also less than 12. I have no idea how to about it. Currently, this is what I have;
^[0-1]?[0-9]{1}\-[0-3]?[0-9]{1}\-[0-9]{2,4}$
This regex achieves the format (m)m-(d)d-(yy)yy
TL;DR
Don't use regular expressions for comparison operations. Use a regex to split off values to compare, or use an actual parser.
Use Regular Expressions to Extract Comparables
Date comparisons is a really poor problem for regex to solve. At most, you should use a regular expression to extract your days of the month for a numeric comparison. For example:
date = '01-01-1970'
date.split('-')[1].to_i < 32
#=> true
However, the code above won't really tell you if a given date is valid. For example, what about February 30th or November 31st? Instead, you should attempt to parse the date to determine its validity.
Use a Date Parser
The best way to tell if a given date is valid is to parse it with a date parser, and then report a Boolean result or handle the exception. For example, you could attempt to parse the date with Date#parse.
Boolean Results
If you just want a Boolean result, you can coerce a valid/invalid parse to true or false. For example:
require 'date'
date = '01-33-1970'
!!(Date.parse date rescue nil)
#=> false
Rescuing and Reporting the Exception
Less magically, you would need to rescue ArgumentError from Date#parse. For example:
require 'date'
def valid_date? date_string
true if Date.parse date_string
rescue ArgumentError => e
STDERR.puts "#{e.class}: #{e}: '#{date_string}'"
false
end
valid_date? '11-31-1970'
This will do what you expect, albeit more verbosely. For example, the above example will print the exception to standard error, and then return false as the result.
ArgumentError: invalid date: '11-31-1970'
#=> false
^(?:[0-1][1-2]|[1-9])\-(?:3[0-1]|[0-2][1-9]|[1-9])\-[0-9]{2}(?:[0-9]{2})?$
should do what you're looking for. It will only allow months from 1-12 (either 1-9 or 01-12), days from 1-31 (either 1-9 or 01-31) and years of at least 2 digits with a maximum of four. Tested on regex101.
Basic:
Here is a regex that should do what you want:
^(0[1-9]|1[0-2]|[1-9])-(0[1-9]|[1-2][0-9]|3[0-1]|[1-9])-\d{2}(\d{2})?$
It matches months greater than 0 and less than 13, then -, then days greater than 0 and less than 32, then -, then years (2 digits or 4 digits).
Bonus:
Full regex for matching dates in that format with validation:
^((0?[13578]|10|12)-(([1-9])|(0[1-9])|([12])([0-9]?)|(3[01]?))-((19)([2-9])(\d{1})|(20)([01])(\d{1})|([8901])(\d{1}))|(0?[2469]|11)-(([1-9])|(0[1-9])|([12])([0-9]?)|(3[0]?))-((19)([2-9])(\d{1})|(20)([01])(\d{1})|([8901])(\d{1})))$
If you want to determine the string is a valid date, you'd be better off attempting to convert it. If it won't convert, it's not valid.
def date_valid?(date_string)
format = '%m/%d/' + (date_string.split(-).last.size == 4 ? '%Y' : '%y')
return true if Date.strptime(date_string, format)
rescue ArgumentError
return false
end

Ruby date conversion from 10-Aug-14 to dd/mm/yy

I'm reading from xls & csv files with the dates that have the following formatting;
10-Aug-14
And I need them to be: dd/mm/yyyy (11/08/2014)
Have tried the date_format gem the standard Ruby Date & Time classes with no luck.
Inspection shows it's an array consisting of a Date object & a String;
p date_start #=> #<Date: -4712-01-01 ((0j,0s,0n),+0s,2299161j)> "11-Aug-14"
puts date_start #=> -4712-01-01
#=> 11-Aug-14
puts date_start.class #=> Array
puts date_start[0].class #=> Date
puts date_start[1].class #=> String
Any idea how I can parse this into a date that Ruby understands.
Also I need to get the weekdays in numbers between two dates so getting this right is key.
For parse date:
my_date = Date.strptime("10-Aug-14 ", "%d-%b-%y")
To the other format(dd/mm/yy):
puts my_date.strftime("%d/%m/%Y")
For weekdays count you can use 'weekdays gem' --> https://github.com/mdarby/weekdays

Parse Date string in Ruby

I have a String 20120119 which represents a date in the format 'YYYYMMDD'.
I want to parse this string into a Ruby object that represents a Date so that I can do some basic date calculation, such as diff against today's date.
I am using version 1.8.6 (requirement).
You could use the Date.strptime method provided in Ruby's Standard Library:
require 'date'
string = "20120723"
date = Date.strptime(string,"%Y%m%d")
Alternately, as suggested in the comments, you could use Date.parse, because the heuristics work correctly in this case:
require 'date'
string = "20120723"
date = Date.parse(string)
Both will raise an ArgumentError if the date is not valid:
require 'date'
Date.strptime('2012-March', '%Y-%m')
#=> ArgumentError: invalid date
Date.parse('2012-Foo') # Note that '2012-March' would actually work here
#=> ArgumentError: invalid date
If you also want to represent hours, minutes, and so on, you should look at DateTime. DateTime also provides a parse method which works like the parse method on Date. The same goes for strptime.

Ensuring a string is of a time or date format?

I have a method which parses a string in to a date, but i want to validate that i don't try to parse a non numeric string or a string which dosent represent a date or time format?
how can id o this?
at the moment i have:
if(string=~ /^\D*$/ )
{
return false
else
do something_else
}
this was fine for a non numeric string like "UNKNOWN" but wouldn't work for "UNKNOWN1"
any idea what i can use to make sure that only date or time formats are parsed?
DateTime.strptime v ParseDate.parsedate
No pun intended but the information herein is now out of date (2015) and some methods and modules have been removed from Ruby 2.x I'm leaving it here just in case someone, somewhere is still using 1.8.7
Ok, maybe there was a small pun intended there ;-)
You would think that you could use either Date.parse or DateTime.parse to check for bad dates (see more on Date.parse here)
d = Date.parse(string) rescue nil
if d
do_something
else
return false
end
because bad values throw an exception which you can catch. However the test strings suggested actually return a Date with Date.parse
For example ..
~\> irb
>> Date.parse '12-UNKN/34/OWN1'
=> #<Date: 4910841/2,0,2299161>
>>
Date.parse just isn't clever enough to do the job :-(
ParseDate.parsedate does a better job. You can see that it attempts to parse the date but in the test examples, doesn't find a valid year or month. More information here
>> require 'parsedate'
=> true
>> ParseDate.parsedate '2010-09-09'
=> [2010, 9, 9, nil, nil, nil, nil, nil]
>> ParseDate.parsedate 'dsadasd'
=> [nil, nil, nil, nil, nil, nil, nil, nil]
>> ParseDate.parsedate '12-UNKN/34/OWN1'
=> [nil, nil, 12, nil, nil, nil, nil, nil]
>> ParseDate.parsedate '12-UNKN/34/OWN1'
=> [nil, nil, 12, nil, nil, nil, nil, nil]
Regardless of which method you use to parse a date, you can validate strict conformance by reformatting the resulting date and comparing it with the original input. For example:
def strict_parse(input, format)
Time.strptime(input, format).tap { |output| expect(output.strftime(format)).to eq input }
end
This is strict however, e.g. "1/9/2014" won't parse with format "%d/%m/%Y". It would have to be "01/09/2014" to be acceptable.
Ruby's parsers are optimistic, if you can throw out a bunch of garbage and get a result from the input string, Date.parse and DateTime.strptime will try to do it.
You want a pessimistic and strict check, which means instead of assuming acceptance after trying to hunt for garbage with a regex, you should assume rejection and hunt for treasure with your regex.
Your first check: "Is a string numeric" is using a regex to try and find a string which is comprised entirely of non-numeric characters, and rejecting if it finds it. \D (with a capital D) is looking for non-numeric characters, and input strings will only match your regex if it is comprised entirely of 0 or more non-numeric characters.
You'll likely have better luck with the following logic for numerics:
if(string=~ /^\d*$/ )
something_else
else
return false
end
This matches a string comprised entirely of 0 or more numeric characters, does something_else if it finds it, and returns false otherwise.
For times you want to explicitly search for times and reject all other values. For an HH:MM:SSAM format which tolerates omitting leading 0's for each field, with 12 hour times you could use the following:
if (string =~ /^[01]?\d:[0-5]?\d:[0-5]?\d[AP]M$/)
something_else
else
return false
end
Likewise for dates you want to explicitly search for dates that are valid, and reject all other values. For MM/DD/YYYY which tolerates omitting leading 0's for everything but years field you could go with:
if (string =~ /^[0-1]\d\/[0-3]?\d\/\d{4}/)
something_else
else
return false
end
Ruby's utility functions try to be verbose in what they accept, but for validation that is not a useful trait. Be strict, assume that everything is invalid until it proves otherwise, then accept it.
I'd advise you to establish a list of date and datetime formats that you expect and intend to support. You can define them using strftime compatible strings, and then use the same strings when parsing dates, using DateTime#strptime. Try to parse your input strings with each supported pattern, the first one which doesn't throw an exception will return parsed date. If each throws an exception, the string is not valid date.
Check this out:
Returns true is string is a valid time, false otherwise:
require 'time'
def is_a_time?(string)
!!(Time.parse(string) rescue false)
end
Returns true is string is a valid date, false otherwise:
require 'date'
def is_a_date?(string)
!!(Date.parse(string) rescue false)
end

Resources