I need to output to the console the calendar of the current month in Ruby. The result should be similar to ncal on UNIX-like systems. I found a solution for C ++ but can't adapt for Ruby. So far, I only realized that I need to use nested loops to output the height and width. Tell me in which direction to move?
require 'date'
days = %w[Mun Tue Wed Thu Fri Sat Sun]
puts " #{Date::MONTHNAMES[Date.today.month]} #{Date.today.year}"
i = 0
start_month = (Date.today - Date.today.mday + 1).strftime("%a")
while i < days.size
print days[i]
j = 1
while j <= 31
if days[i] == start_month
print " #{j}"
end
j += 7
end
i += 1
puts
end
I'll take your solution so far, and try to give some specific pointers for how to progress with it - but of course, there are many different ways to approach this problem in general, so this is by no means the only approach!
The first critical issue (as you're aware!) is that you're only printing things for the row starting on the 1st of the month, due to this line:
if days[i] == start_month
Sticking with the current overall design, we know we'll need to print something for every line, so clearly a conditional like this isn't going to work. Let's try removing it.
Firstly, it will be more convenient to know which day of the week the month started on as a number, not a string, so we can easily calculate offsets against another day. Let's do that with:
# e.g. for 1st July 2021 this was a Thursday, so we get `4`.
start_of_month_weekday = (Date.today - Date.today.mday + 1).cwday
Next (and this is the crucial step!), we can use the above information to find out "which day of the month is it, on this day of the week?"
Here a first version of that calculation, incorporated into your solution so far:
require 'date'
days = %w[Mon Tue Wed Thu Fri Sat Sun]
puts " #{Date::MONTHNAMES[Date.today.month]} #{Date.today.year}"
i = 0
# e.g. for 1st July 2021 this was a Thursday, so we get `4`.
start_of_month_weekday = (Date.today - Date.today.mday + 1).cwday
while i < days.size
print days[i]
day_of_month = i - start_of_month_weekday + 2 # !!!
while day_of_month <= 31
print " #{day_of_month}"
day_of_month += 7
end
i += 1
puts
end
This outputs:
July 2021
Mon -2 5 12 19 26
Tue -1 6 13 20 27
Wed 0 7 14 21 28
Thu 1 8 15 22 29
Fri 2 9 16 23 30
Sat 3 10 17 24 31
Sun 4 11 18 25
Not bad! Now we're getting somewhere!
I'll leave you to figure out the rest 😉 .... But here are some clues, for what I'd tackle next:
This code, print " #{day_of_month}", needs to print a "blank space" if the day number is less than 1. This could be done with a simple if statement.
Similarly, since you want this calendar to line up neatly in a grid, you need this code to always print a something two characters wide. sprintf is your friend here! Check out the "Examples of width", about halfway down the page.
You've hardcoded 31 for the number of days in the month. This should be fixed, of course. (Use the Date library!)
It's funny how you used strftime("%a") in one place, yet constructed the calendar title awkwardly in the line above! 😄 Take a look at the documentation for formatting dates; it's extremely flexible. I think you can use: Date.today.strftime("%B %Y").
If you'd like to add some colour (or background colour?) to the current day of the month, consider doing something like this, or use a library to assist.
Using while loops works OK, but is quite un-rubyish. In 99% of cases, ruby has even better tools for the job; it's a very expressive language - iterators are king! (I'm guessing you first learned another language, before ruby? Seeing while loops, and/or for loops, is a dead giveaway that you're more familiar with a different language.) Instead of the outer while loop (while i < days.size), you could use days.each_with_index. And instead of the inner while loop (while j < 31), you could use day_of_month.step(31, 7) (how cool is that!!).
This is one way:
Construct a one-dimensional array, beginning with the daynames (Mon Tue ...).
Figure out a way to determine with how many "blanks" the month starts (these are days from the previous month. wday might help). Attach that amount of empty strings to the array.
Determine how many days the month has (hint Date.new(2021,7,-1), and attach all these daynumbers to the array.
Attach empty strings to the array until the size of the array is divisible by 7 (or better, calculate). Skip this if you're skipping the last bullet.
Convert all elements of this array to right-adjusted strings of size 3 or some-such.
Use each_slice(7) to slice the array into weeks.
If desired, transpose this array of week-slices to mimic the ncal output.
Thank you for your help, literally 10 hours and I figured it out thanks to you. I apologize once again for the initially incorrectly posed question.
With the help of hints, I assembled such a solution.
require 'date'
days = %w[Mon Tue Wed Thu Fri Sat Sun]
p days
blanks = Date.new(2021,7,1).wday - 1
blanks.times do
days.push(' ')
end
days_in_month = Date.new(2021, 7, -1).day
days_in_month
day = 1
while day <= days_in_month
days.push(day)
day += 1
end
unless (days.size % 7) == 0
days.push(' ')
end
days.join(', ')
new_arr = days.each_slice(7).to_a
puts"Массив дней: #{new_arr}"
for i in 0...7
for j in 0...new_arr.size
print " #{new_arr[j][i]}"
end
puts
end
require 'date'
# init
DAYS_ORDER = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
today = Date.today
month = today.month
year = today.year
first_day = Date.new(year, month, 1)
last_day = Date.new(year, month, -1)
hash_days = {}
# get all current months days and add to hash_days
first_day.upto(last_day) { |day| hash_days[day.day] = day.strftime('%a') }
# group by wday
grouped_hash = hash_days.group_by { |day| day.pop }.transform_values { |days| days.flatten }
# sort by wday from DAYS_ORDER
sorted_arr = grouped_hash.sort_by { |k, v| DAYS_ORDER.index(k) }
# rendering current month's calendar with mark current day
## title
print "\x1b[4m#{today.strftime("%B %Y")}\x1b[0m\n"
## calendar
indent = true
sorted_arr.each do |wday, days|
print wday
if days[0] != 1 && indent == true
print " "
else
indent = false
end
days.each do |value|
spaces = " " * (value > 9 ? 1 : 2)
str_day = spaces + value.to_s
current_day = "\x1b[1;31m#{str_day}\x1b[0m"
print value == today.day ? current_day : str_day
end
puts
end
view
Related
Trying to convert strings like 9 weeks ago, 1 year, 6 months ago, 20 hours ago into a ruby time object like Tue, 10 Mar 2015 12:06:15 PDT -07:00.
I've been doing this:
eval("10 days ago".gsub(' ', '.'))
This works fine, but for strings like 1 year, 6 months ago blows up.
I just need to do comparisons like:
eval("10 days ago".gsub(' ', '.')) < (Time.now - 7.days)
I'm using sinatra so no fancy rails helpers.
Please never use eval in production code..
Converting from timeago notation would be quite complex and resource intensive.
However, this way seems the least error prone: It will convert a string like "5 seconds ago" to "5S" and use mapping to find what it means in time, after which it will subtract that time from the current time.
The parse string is dynamically built so it can accomodate most every timeago notation.
require('date')
mapping = {"D"=> "%d","W"=>"%U","H"=>"%T","Y"=>"%Y","M"=>"%m","S"=>"%S"}
timerel = "1 year, 6 months ago".split(",").map { |n| n.gsub(/\s+/, "").upcase()[0,2].split('')}
Date.strptime(
timerel.map {|n| n[0]}.join(" "),
timerel.map {|n| mapping[n[1]]}.join(" ")
)
date = Date.new(0) + (Date.today - Date.strptime(timerel.map {|n| n[0]}.join(" "), timerel.map {|n| mapping[n[1]]}.join(" ")))
=> #<Date: 2014-10-10 ((2456941j,0s,0n),+0s,2299161j)>
It goes without saying that is very error prone. Use at your own risk:
def parse(date:)
eval(date.gsub(/ ?(,|and) ?/, '+').tr(' ', '.').gsub(/^(.*)(\.ago)$/, '(\1)\2'))
end
parse(date: '1 year, 6 months ago') # => Wed, 10 Sep 2014 21:29:11 BST +01:00
parse(date: '1 year, 6 months, 3 weeks, 6 days, 9 hours and 12 seconds ago')
# => Thu, 14 Aug 2014 12:33:07 BST +01:00
The idea is to convert the original string to:
'(1.year+6.months).ago'
I have a table date range that i'm retrieving with one date specified x_VisitDate
And four variables defined that will use the x_VisitDate
For this instance x_VisitDate = 22 May 2014 the download button should only be available 10 days prior to the given date as at today we are 7 days away.
x_TodaysDate
x_DaysBeForeToDownloadFile = 10
x_AdditionalDayToDownloadFile = 1
x_CanDownloadFile = DateAdd("d", x_DaysBeForeToDownloadFile , x_VisitDate)
x_ExtraCanDownloadFile = DateAdd("d", x_AdditionalDayToDownloadFile, x_VisitDate)
I'm currently writing the following
If x_CanDownloadFile <= x_TodaysDate then
and it's showing the file when the date is set for 21 Oct 2018 (Future) and 23 Feb 2014 (past)
I'm going crazy here...
thanks
Don't use comparisons to constructed limits, but DateDiff for range checks. It's much easier to get right. Demo code:
>> nDaysAvailable = 3
>> dtToday = Date
>> For i = -5 To 0
>> dtCheck = DateAdd("d", i, dtToday)
>> nDiff = DateDiff("d", dtCheck, dtToday)
>> WScript.Echo dtCheck, nDiff, CStr(nDiff < nDaysAvailable)
>> Next
>>
10.05.2014 5 False
11.05.2014 4 False
12.05.2014 3 False
13.05.2014 2 True
14.05.2014 1 True
15.05.2014 0 True
(german locale)
Maybe I'm confused by your question but can't you just do this?
x_VisitDate = #5/22/14#
If Date >= x_VisitDate - 10 And Date <= x_VisitDate + 1 Then
' Show file
End If
Also, you don' t need to use DateDiff() when you're working with days. Just use integer math.
I made new object Date.new with args (year, month). After create ruby added 01 number of day to this object by default. Is there any way to add not first day, but last day of month that i passed as arg(e.g. 28 if it will be 02month or 31 if it will be 01month) ?
use Date.civil
With Date.civil(y, m, d) or its alias .new(y, m, d), you can create a new Date object. The values for day (d) and month (m) can be negative in which case they count backwards from the end of the year and the end of the month respectively.
=> Date.civil(2010, 02, -1)
=> Sun, 28 Feb 2010
>> Date.civil(2010, -1, -5)
=> Mon, 27 Dec 2010
To get the end of the month you can also use ActiveSupport's helper end_of_month.
# Require extensions explicitly if you are not in a Rails environment
require 'active_support/core_ext'
p Time.now.utc.end_of_month # => 2013-01-31 23:59:59 UTC
p Date.today.end_of_month # => Thu, 31 Jan 2013
You can find out more on end_of_month in the Rails API Docs.
So I was searching in Google for the same thing here...
I wasn't happy with above so my solution after reading documentation
in RUBY-DOC was:
Example to get 10/31/2014
Date.new(2014,10,1).next_month.prev_day
require "date"
def find_last_day_of_month(_date)
if(_date.instance_of? String)
#end_of_the_month = Date.parse(_date.next_month.strftime("%Y-%m-01")) - 1
else if(_date.instance_of? Date)
#end_of_the_month = _date.next_month.strftime("%Y-%m-01") - 1
end
return #end_of_the_month
end
find_last_day_of_month("2018-01-01")
This is another way to find
You can do something like that:
def last_day_of_month?
(Time.zone.now.month + 1.day) > Time.zone.now.month
end
Time.zone.now.day if last_day-of_month?
This is my Time based solution. I have a personal preference to it compared to Date although the Date solutions proposed above read somehow better.
reference_time ||= Time.now
return (Time.new(reference_time.year, (reference_time.month % 12) + 1) - 1).day
btw for December you can see that year is not flipped. But this is irrelevant for the question because december always has 31 day. And for February year does not need flipping. So if you have another use case that needs year to be correct, then make sure to also change year.
Here is taking the first and third answers to find the last day of the previous month.
today_c = Date.civil(Date.today.prev_month.year, -1, -1)
p today_c
I would like to know how to get the current week number from Rails and how do I manipulate it:
Translate the week number into date.
Make an interval based on week number.
Thanks.
Use strftime:
%U - Week number of the year. The week starts with Sunday. (00..53)
%W - Week number of the year. The week starts with Monday. (00..53)
Time.now.strftime("%U").to_i # 43
# Or...
Date.today.strftime("%U").to_i # 43
If you want to add 43 weeks (or days,years,minutes, etc...) to a date, you can use 43.weeks, provided by ActiveSupport:
irb(main):001:0> 43.weeks
=> 301 days
irb(main):002:0> Date.today + 43.weeks
=> Thu, 22 Aug 2013
irb(main):003:0> Date.today + 10.days
=> Sun, 04 Nov 2012
irb(main):004:0> Date.today + 1.years # or 1.year
=> Fri, 25 Oct 2013
irb(main):005:0> Date.today + 5.months
=> Mon, 25 Mar 2013
You are going to want to stay away from strftime("%U") and "%W".
Instead, use Date.cweek.
The problem is, if you ever want to take a week number and convert it to a date, strftime won't give you a value that you can pass back to Date.commercial.
Date.commercial expects a range of values that are 1 based.
Date.strftime("%U|%W") returns a value that is 0 based. You would think you could just +1 it and it would be fine. The problem will hit you at the end of a year when there are 53 weeks. (Like what just happened...)
For example, let's look at the end of Dec 2015 and the results from your two options for getting a week number:
Date.parse("2015-12-31").strftime("%W") = 52
Date.parse("2015-12-31").cweek = 53
Now, let's look at converting that week number to a date...
Date.commercial(2015, 52, 1) = Mon, 21 Dec 2015
Date.commercial(2015, 53, 1) = Mon, 28 Dec 2015
If you blindly just +1 the value you pass to Date.commercial, you'll end up with an invalid date in other situations:
For example, December 2014:
Date.commercial(2014, 53, 1) = ArgumentError: invalid date
If you ever have to convert that week number back to a date, the only surefire way is to use Date.cweek.
date.commercial([cwyear=-4712[, cweek=1[, cwday=1[, start=Date::ITALY]]]]) → date
Creates a date object denoting the given week date.
The week and the day of week should be a negative
or a positive number (as a relative week/day from the end of year/week when negative).
They should not be zero.
For the interval
require 'date'
def week_dates( week_num )
year = Time.now.year
week_start = Date.commercial( year, week_num, 1 )
week_end = Date.commercial( year, week_num, 7 )
week_start.strftime( "%m/%d/%y" ) + ' - ' + week_end.strftime("%m/%d/%y" )
end
puts week_dates(22)
EG: Input (Week Number): 22
Output: 06/12/08 - 06/19/08
credit: Siep Korteling http://www.ruby-forum.com/topic/125140
Date#cweek seems to get the ISO-8601 week number (a Monday-based week) like %V in strftime (mentioned by #Robban in a comment).
For example, the Monday and the Sunday of the week I'm writing this:
[ Date.new(2015, 7, 13), Date.new(2015, 7, 19) ].map { |date|
date.strftime("U: %U - W: %W - V: %V - cweek: #{date.cweek}")
}
# => ["U: 28 - W: 28 - V: 29 - cweek: 29", "U: 29 - W: 28 - V: 29 - cweek: 29"]
I try to get a decimal amount of months for a date range. Example:
ruby-1.9.2-p0 > from = Date.new(2011, 7, 6)
=> Wed, 06 Jul 2011
ruby-1.9.2-p0 > to = Date.new(2011, 8, 31)
=> Wed, 31 Aug 2011
ruby-1.9.2-p0 > to - from
=> (56/1)
So the difference is 56 days. But I want and need the amount of months: 1.83
I have created the following piece of code which returns the correct result but doesn't feel like the ruby way:
months = Hash.new
(from..to).each do |date|
unless months.key? date.beginning_of_month
months[date.beginning_of_month] = 1
else
months[date.beginning_of_month] += 1
end
end
multiplicator = 0.0
months.each do |month, days|
multiplicator += days.to_f/month.end_of_month.day
end
return multiplicator.floor_to(2)
To be honest: It looks ugly and really inefficient. But I just cannot figure out any easier way.
Can you help me to find a better solution?
For further questions feel free to ask me.
Many thanks in advance!
Update/Solution: Solved the problem with the following piece of code:
months = 0.0
months += ((date_to < date_from.end_of_month ? date_to : date_from.end_of_month) - date_from + 1) / Time.days_in_month(date_from.month)
unless date_to.month == date_from.month
months += (date_to - date_to.beginning_of_month + 1) / Time.days_in_month(date_to.month)
months += date_to.month - date_from.month - 1
end
return months.floor_to(2)
a better way to do would be summation of
number of days left in from / number of days in from
number of days completed in to / number of days in to
number of months between from and to (from, to excluded)
This way you wont have iterations to do