How to handle leap years in Ada - ada

I'm supposed to do a program that handles the dates of a year. I'm having a problem with handling the leap year in a good way. I'm not going to post the entire code because it'll be too much. My program runs but I don't like how my last if statement looks.
Information about months and leap years:
Month: Jan Feb Mar Apr May Jun Jul Aug Sep Okt Nov Dec
Days : 31 28/29 31 30 31 30 31 31 30 31 30 30
A year is a leap year if it's evenly dividable by 4, but not even when divided by 100. Years are also evenly dividable by 400 leap years. If it's a leap year, February has 29 days instead of 28.
This is my code (only the part I need help with):
Get(Days);
if Days > 31 or Day < 1 then
raise Day_Error;
elsif Day = 31 and (Month = 2 or Month = 4 or Month = 6 or Month = 9 or Month = 11) then
raise Day_Error;
elsif Day = 30 and (Month = 1 or Month = 3 or Month = 5 or Month = 7 or
Month = 8 or Month = 10 or Month = 12) then
raise Day_Error;
end if;
if Day = 29 and Month = 2 then
if Year mod 400 = 0 or (Year mod 4 = 0 and Year mod 100 /= 0) then
null;
else
raise Day_Error;
end if;
end if;
exit;
end;
end loop;
end Day_Procedure;
My concern is the if statement where I put Day = 29 and Month = 2, can this be solved without having the need to put two if statements?

I think a lot can be gained by putting certain tests in subroutines, even if they are only called once. In the code below I have a routine GetMaxDayInMonth(Month, Year) that gives the maximum day for the given month in the given year. The year is necessary only in the case of February, where the maximum allowed day of the month depends on whether it is in a leap year.
For all other months the return value is constant, which I have recorded in a const array MaxDaysInMonth. Only days in February must be treated specially. That treatment has again been stowed away in its own little subroutine GetFebMaxDay(Day, Month), making everything neat and intelligible.
I have used the new Ada equivalent to the C ternary operator cond?a:b, (if cond then a else b):
function GetFebMaxDay(Year: YearT) return DayInMonthT is
begin
return (if IsLeapYear(Year) then 29 else 28);
end GetFebMaxDay;
The test for valid day then is thus reduced to a single function call
Date.Day <= GetMaxDayInMonth(Date.Month, Date.Year);
I have also created distinct types for year, month and day, imposing constraints where possible (max day in month is 31, after all). Many mistakes would have been avoided if programmers had put feet and meters in their own subtypes that need explicit conversion.
Here is the entire program that checks a few borderline dates:
with Ada.Text_IO; use Ada.Text_IO;
procedure DateCheck is
subtype DayInMonthT is Integer range 1..31;
type MonthT is (JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC);
subtype YearT is Integer;
type DateT is record Year: YearT; Month: MonthT; Day: DayInMonthT; end record;
function IsLeapYear(Y: YearT) return Boolean is
begin
return (Y mod 4 = 0 and Y mod 100 /= 0)
or Y mod 400 = 0;
end IsLeapYear;
function GetFebMaxDay(Year: YearT) return DayInMonthT is
begin
return (if IsLeapYear(Year) then 29 else 28);
end GetFebMaxDay;
function GetMaxDayInMonth(Month: MonthT; Year: YearT) return DayInMonthT is
MaxDaysInMonth: constant array(MonthT) of DayInMonthT :=
( JAN => 31,
FEB => 29,
MAR => 31,
APR => 30,
MAY => 31,
JUN => 30,
JUL => 31,
AUG => 31,
SEP => 30,
OCT => 31,
NOV => 30,
DEC => 31 );
begin return (if Month = FEB then GetFebMaxDay(Year) else MaxDaysInMonth(Month)); end;
function IsValidateDate(Date: DateT) return Boolean is
begin
return Date.Day <= GetMaxDayInMonth(Date.Month, Date.Year);
end;
Dates: array (1..6) of DateT := (others => (0, JAN, 1));
begin
Dates(1) := (2021, FEB, 28);
Dates(2) := (2021, FEB, 29);
Dates(3) := (2021, FEB, 30);
Dates(4) := (2000, FEB, 31);
-- Dates(4) := (2000, FEB, 32); -- raises warning, fails with constraint error
Dates(5) := (2000, FEB, 29);
Dates(6) := (1900, FEB, 29);
for i in Dates'Range loop
Put("Date ");
Put(Dates(i).Year'Image);
Put("-");
Put(Dates(i).Month'Image);
Put("-");
Put(Dates(i).Day'Image);
Put(": Valid date? ");
Put_Line(IsValidateDate(Dates(i))'Image);
end loop;
end DateCheck;
Sample session:
$ gnatmake -gnatwa datecheck.adb && ./datecheck.exe
gcc -c -gnatwa datecheck.adb
gnatbind -x datecheck.ali
gnatlink datecheck.ali
Date 2021-FEB- 28: Valid date? TRUE
Date 2021-FEB- 29: Valid date? FALSE
Date 2021-FEB- 30: Valid date? FALSE
Date 2000-FEB- 31: Valid date? FALSE
Date 2000-FEB- 29: Valid date? TRUE
Date 1900-FEB- 29: Valid date? FALSE

Related

How to get date of two month old date from current date

I am trying to pull data (some transaction related data) from DB. To pull the data, I am passing start date and end date as an argument to the query.
Here I need to pull the data of last 2 months. i.e., Start time would be Jun 01, 2022 and End time would be Aug 01, 2022.
Below is the script:
#!/usr/bin/perl
use strict;
use warnings;
use DateTime;
use Date::Format;
use Date::Parse;
my $nowdate = DateTime->now(time_zone => 'local');
my ($month, $year) = ($nowdate->month, $nowdate->year);
my $date = DateTime->new(
year => $year,
month => $month,
day => 1,
);
my $end_time = str2time($date);
print "END:$end_time\n";
print time2str("%d-%m-%Y %T", $end_time)."\n"; #printing just to see in human readable format
my $start_time = $date->clone;
$start_time->add( months => 1 )->subtract( days => 92 );
$start_time = str2time($start_time);
print "START:$start_time\n";
print time2str("%d-%m-%Y %T", $start_time)."\n"; #printing just to see in human readable format
I have two issues over here:
I am using DateTime object two times to get end time. Can it be done in one shot?
$start_time->add( months => 1 )->subtract( days => 92 ); In this line of code, I have to explicitly mention subtract 92 days, which wouldn't be right always. Since some months have 30 days or 31 days or even 29 days. How can I get 2 month's beginning day date?
Another example: Lets assume if we are in September 2022, then Start time and End time would be Jul 01, 2022 and Sep 01, 2022 respectively.
Note: Perl version is 5.16.3
It would be good If I can do it with Core modules which comes with 5.16.3
You could simplify it by using truncate(to => 'month') to get to the first in the current month:
my $end = DateTime->now(time_zone => 'local')
->truncate(to => 'month');
This may however fail on a day without a midnight so this may be an option:
my $end = DateTime->now(time_zone => 'local')
->set_time_zone('floating')
->truncate(to => 'month');
Then subtract the number of months to get the start date:
my $start = $end->clone->subtract( months => 2 );
Then:
my $start_time = str2time($start);
my $end_time = str2time($end);
print "START:$start_time\n";
print time2str("%d-%m-%Y %T", $start_time)."\n";
print "END:$end_time\n";
print time2str("%d-%m-%Y %T", $end_time)."\n";
Possible output:
START:1654034400
01-06-2022 00:00:00
END:1659304800
01-08-2022 00:00:00

How to parse CCYY-MM-DDThh:mm:ss[.sss...] date format

As we all know, date parsing in Go has it's quirks*.
However, I have now come up against needing to parse a datetime string in CCYY-MM-DDThh:mm:ss[.sss...] to a valid date in Go.
This CCYY format is a format that seems to be ubiquitous in astronomy, essentially the CC is the current century, so although we're in 2022, the century is the 21st century, meaning the date in CCYY format would be 2122.
How do I parse a date string in this format, when we can't specify a coded layout?
Should I just parse in that format, and subtract one "century" e.g., 2106 becomes 2006 in the parsed datetime...?
Has anyone come up against this niche problem before?
*(I for one would never have been able to remember January 2nd, 3:04:05 PM of 2006, UTC-0700 if it wasn't the exact time of my birth! I got lucky)
The time package does not support parsing centuries. You have to handle it yourself.
Also note that a simple subtraction is not enough, as e.g. the 21st century takes place between January 1, 2001 and December 31, 2100 (the year may start with 20 or 21). If the year ends with 00, you do not have to subtract 100 years.
I would write a helper function to parse such dates:
func parse(s string) (t time.Time, err error) {
t, err = time.Parse("2006-01-02T15:04:05[.000]", s)
if err == nil && t.Year()%100 != 0 {
t = t.AddDate(-100, 0, 0)
}
return
}
Testing it:
fmt.Println(parse("2101-12-31T12:13:14[.123]"))
fmt.Println(parse("2122-10-29T12:13:14[.123]"))
fmt.Println(parse("2100-12-31T12:13:14[.123]"))
fmt.Println(parse("2201-12-31T12:13:14[.123]"))
Which outputs (try it on the Go Playground):
2001-12-31 12:13:14.123 +0000 UTC <nil>
2022-10-29 12:13:14.123 +0000 UTC <nil>
2100-12-31 12:13:14.123 +0000 UTC <nil>
2101-12-31 12:13:14.123 +0000 UTC <nil>
As for remembering the layout's time:
January 2, 15:04:05, 2006 (zone: -0700) is a common order in the US, and in this representation parts are in increasing numerical order: January is month 1, 15 hour is 3PM, year 2006 is 6. So the ordinals are 1, 2, 3, 4, 5, 6, 7.
I for one would never have been able to remember January 2nd, 3:04:05 PM of 2006, UTC-0700 if it wasn't the exact time of my birth! I got lucky.
The reason for the Go time package layout is that it is derived from the Unix (and Unix-like) date command format. For example, on Linux,
$ date
Fri Apr 15 08:20:43 AM EDT 2022
$
Now, count from left to right,
Month = 1
Day = 2
Hour = 3 (or 15 = 12 + 3)
Minute = 4
Second = 5
Year = 6
Note: Rob Pike is an author of The Unix Programming Environment

Unix epoch to date String – Slightly wrong month day

I am trying to format SystemTime as a String in Rust without using external crates, but I am facing a bizarre issue regarding the "day" field.
Can anyone explain me why the line preceded by the "FIXME" comment yields slightly wrong month days as the time distance from Thu, 01 Jan 1970 00:00:00 GMT increases?
fn time_to_http_date_string(time: &SystemTime) -> String {
const MINUTE: u64 = 60;
const HOUR: u64 = 3600;
const DAY: u64 = 86400;
const WEEKDAYS: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const MONTH: u64 = 2629743;
const MONTHS: [&str; 12] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const YEAR: u64 = 31556926;
let time = time.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
let year = 1970 + (time / YEAR);
let month = MONTHS[(time % YEAR / MONTH) as usize];
let weekday = WEEKDAYS[((time / DAY + 4) % 7) as usize];
// FIXME: Slightly wrong as time distance from the UNIX epoch increases.
let day = (time % MONTH / DAY) + 1;
let hour = time % DAY / HOUR;
let minute = time % HOUR / MINUTE;
let second = time % MINUTE;
format!(
"{weekday}, {day:02} {month} {year} {hour:02}:{minute:02}:{second:02} GMT",
weekday = weekday,
day = day,
month = month,
year = year,
hour = hour,
minute = minute,
second = second,
)
}
Let me give you more details about this issue; here are some tests I ran this function through:
Unix Timestamp
Expected
Mine
OK
0
Thu, 01 Jan 1970 00:00:00 GMT
Thu, 01 Jan 1970 00:00:00 GMT
👍
68169600
Tue, 29 Feb 1972 00:00:00 GMT
Tue, 29 Feb 1972 00:00:00 GMT
👍
874540800
Thu, 18 Sep 1997 00:00:00 GMT
Thu, 17 Sep 1997 00:00:00 GMT
❌
1052790840
Tue, 13 May 2003 01:54:00 GMT
Tue, 11 May 2003 01:54:00 GMT
❌
As you may have already noticed from my test cases, the day is the only wrong field in any of the failing cases. Any ideas about the culprit?
PS: I have already tried to use f64 instead of u64 and to round the results... but it didn't fix all of the test cases.
It turns out I was looking for an impossible solution. It is indeed impossible to perform such a computation without taking into account leap years and the simple fact that months do not share the same duration in days. Even though now I feel a bit dumb about posting this question, I'd like to thank the people who first commented this thread. They pushed me in the right direction.

Return local beginning of day time object

To get a local beginning of today time object I extract YMD and reconstruct the new date. That looks like a kludge. Do I miss some other standard library function?
code also runnable at http://play.golang.org/p/OSRl0nxyB7 :
func Bod(t time.Time) time.Time {
year, month, day := t.Date()
return time.Date(year, month, day, 0, 0, 0, 0, t.Location())
}
func main() {
fmt.Println(Bod(time.Now()))
}
Both the title and the text of the question asked for "a local [Chicago] beginning of today time." The Bod function in the question did that correctly. The accepted Truncate function claims to be a better solution, but it returns a different result; it doesn't return a local [Chicago] beginning of today time. For example,
package main
import (
"fmt"
"time"
)
func Bod(t time.Time) time.Time {
year, month, day := t.Date()
return time.Date(year, month, day, 0, 0, 0, 0, t.Location())
}
func Truncate(t time.Time) time.Time {
return t.Truncate(24 * time.Hour)
}
func main() {
chicago, err := time.LoadLocation("America/Chicago")
if err != nil {
fmt.Println(err)
return
}
now := time.Now().In(chicago)
fmt.Println(Bod(now))
fmt.Println(Truncate(now))
}
Output:
2014-08-11 00:00:00 -0400 EDT
2014-08-11 20:00:00 -0400 EDT
The time.Truncate method truncates UTC time.
The accepted Truncate function also assumes that there are 24 hours in a day. Chicago has 23, 24, or 25 hours in a day.
EDIT: This only works for UTC times (it was tested in the playground, so the location-specific test was probably wrong). See PeterSO's answer for issues of this solution in location-specific scenarios.
You can use the Truncate method on the date, with 24 * time.Hour as duration:
http://play.golang.org/p/zJ8s9-6Pck
func main() {
// Test with a location works fine too
loc, _ := time.LoadLocation("Europe/Berlin")
t1, _ := time.ParseInLocation("2006 Jan 02 15:04:05 (MST)", "2012 Dec 07 03:15:30 (CEST)", loc)
t2, _ := time.Parse("2006 Jan 02 15:04:05", "2012 Dec 07 00:00:00")
t3, _ := time.Parse("2006 Jan 02 15:04:05", "2012 Dec 07 23:15:30")
t4, _ := time.Parse("2006 Jan 02 15:04:05", "2012 Dec 07 23:59:59")
t5, _ := time.Parse("2006 Jan 02 15:04:05", "2012 Dec 08 00:00:01")
times := []time.Time{t1, t2, t3, t4, t5}
for _, d := range times {
fmt.Printf("%s\n", d.Truncate(24*time.Hour))
}
}
To add some explanation, it works because truncate "rounds down to a multiple of" the specified duration since the zero time, and the zero time is January 1, year 1, 00:00:00. So truncating to the nearest 24-hour boundary always returns a "beginning of day".

File renaming based on file content in UNIX

I have pattern namely QUARTERDATE and FILENAME inside the file.
Both will have some value as in below eg.
My requirement is, I should rename the file like FILENAME_QUARTERDATE.
My file(myfile.txt) will be as below:
QUARTERDATE: 03/31/14 - 06/29/14
FILENAME : LEAD
field1 field2
34567
20.0 5,678
20.0 5,678
20.0 5,678
20.0 5,678
20.0 5,678
I want the the file name to be as LEAD_201402.txt
Date range in the file is for Quarter 2, so i given as 201402.
Thanks in advance for the replies.
newname=$(awk '/QUARTERDATE/ { split($4, d, "/");
quarter=sprintf("%04d%02d", 2000+d[3], int((d[1]-1)/3)+1); }
/FILENAME/ { fn = $3; print fn "_" quarter; exit; }' "$file")
mv "$file" "$newname"
How is a quarter defined?
As noted in comments to the main question, the problem is as yet ill-defined.
What data would appear in the previous quarter's QUARTERDATE line? Could Q1 ever start with a date in December of the previous year? Could the end date of Q2 ever be in July (or Q1 in April, or Q3 in October, or Q4 in January)? Since the first date of Q2 is in March, these alternatives need to be understood. Could a quarter ever start early and end late simultaneously (a 14 week quarter)?
To which the response was:
QUARTERDATE of Q2 will start as 1st Monday of April and end as last Sunday of June.
Which triggered a counter-response:
2014-03-31 is a Monday, but hardly a Monday in April. What this mainly means is that your definition of a quarter is, as yet, not clear. For example, next year, 2015-03-30 is a Monday, but 'the first Monday in April' is 2015-04-06. The last Sunday in March 2015 is 2015-03-29. So which quarter does the week (Mon) 2015-03-30 to (Sun) 2015-04-05 belong to, and why? If you don't know (both how and why), we can't help you reliably.
Plausible working hypothesis
The lessons of Y2K have been forgotten already (why else are two digits used for the year, dammit!).
Quarters run for an integral number of weeks.
Quarters start on a Monday and end on a Sunday.
Quarters remain aligned with the calendar quarters, rather than drifting around the year. (There are 13 weeks in 91 days, and 4 such quarters in a year, but there's a single extra day in an ordinary year and two extra in a leap year, which mean that occasionally you will get a 14-week quarter, to ensure things stay aligned.)
The date for the first date in a quarter will be near 1st January, 1st April, 1st July or 1st October, but the month might be December, March (as in the question), June or September.
The date for the last date in a quarter will be near 31st March, 30th June, 30th September, 31st December, but the month might be April, July, October or January.
By adding 1 modulo 12 (values in the range 1..12, not 0..11) to the start month, you should end up with a month firmly in the calendar quarter.
By subtracting 1 modulo 12 (values in the range 1..12 again) to the end month, you should end up with a month firmly in calendar quarter.
If the data is valid, the 'start + 1' and 'end - 1' months should be in the same quarter.
The early year might be off-by-one if the start date is in December (but that indicates Q1 of the next year).
The end year might be off-by-one if the end date is in January (but that indicates Q4 of the prior year).
More resilient code
Despite the description above, it is possible to write code that detects the quarter despite any or all of the idiosyncrasies of the quarter start and end dates. This code borrows a little from Barmar's answer, but the algorithm is more resilient to the vagaries of the calendar and the quarter start and end dates.
#!/bin/sh
awk '/QUARTERDATE/ {
split($2, b, "/")
split($4, e, "/")
if (b[1] == 12) { q = 1; y = e[3] }
else if (e[1] == 1) { q = 4; y = b[3] }
else
{
if (b[3] != e[3]) {
print "Year mismatch (" $2 " vs " $4 ") in file " FILENAME
exit 1
}
m = int((b[1] + e[1]) / 2)
q = int((m - 1) / 3) + 1
y = e[3]
}
quarter = sprintf("%.4d%.2d", y + 2000, q)
}
/FILENAME/ {
print $3 "_" quarter
# exit
}' "$#"
The calculation for m adds the start month plus one to the end month minus one and then does integer division by two. With the extreme cases already taken care of, this always yields a month number that is in the correct quarter.
The comment in front of the exit associated with FILENAME allows testing more easily. When processing each file separately, as in Barmar's example, that exit is an important optimization. Note that the error message gives an empty file name if the input comes from standard input. (Offhand, I'm not sure how to print the error message to standard error rather than standard output, other than by a platform-specific technique such as print "message" > "/dev/stderr" or print "message" > "/dev/fd/2".)
Given this sample input data (semi-plausible start and end dates for 6 quarters from 2014Q1 through 2015Q2):
QUARTERDATE: 12/30/13 - 03/30/14
FILENAME : LEAD
QUARTERDATE: 03/31/14 - 06/29/14
FILENAME : LEAD
QUARTERDATE: 06/30/14 - 09/28/14
FILENAME : LEAD
QUARTERDATE: 09/29/14 - 12/28/14
FILENAME : LEAD
QUARTERDATE: 12/29/14 - 03/29/15
FILENAME : LEAD
QUARTERDATE: 03/30/15 - 06/29/15
FILENAME : LEAD
The output from this script is:
LEAD_201401
LEAD_201402
LEAD_201403
LEAD_201404
LEAD_201501
LEAD_201502
You can juggle the start and end dates of the quarters within reason and you should still get the required output. But always be wary of calendrical calculations; they are almost invariably harder than you expect.

Resources