Skip to content

Commit 9125d78

Browse files
authored
Merge pull request #3 from AltGr/main
Add C implementation; separate and package Python and C implementations
2 parents c6ba4be + 94c821a commit 9125d78

File tree

19 files changed

+717
-7
lines changed

19 files changed

+717
-7
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
This library handles dates (`YYYY-MM-DD`) and periods (in days, months and years). It provides operators on dates and periods. The addition of dates and periods containing months or years is a tricky case that may require roundings. We have taken special care to define those rounding operators and expose different rounding modes for users.
66

7-
This library is a work in progress. You can find the library's description in `lib/dates.mli`. There is also a Python implementation (which corresponds to a port of the OCaml implementation).
7+
This library is a work in progress. You can find the library's description in `lib/dates.mli`. There are also a Python and C implementations (which correspond to ports of the OCaml implementation).
88

99
The full semantics of the library has been formalized and is available in the related ESOP 2024 paper [Formalizing Date Arithmetic and Statically Detecting Ambiguities for the Law](https://hal.science/hal-04536403).
1010

dates_calc.opam

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ A date calculation library, with exact operators to add a given
66
number of days to a date, and approximate operators to add months or
77
years."""
88
maintainer: ["Raphaël Monat <[email protected]>"]
9-
authors: ["Aymeric Fromherz" "Denis Merigoux" "Raphaël Monat"]
9+
authors: ["Aymeric Fromherz" "Denis Merigoux" "Raphaël Monat" "Louis Gesbert"]
1010
license: "Apache-2.0"
1111
homepage: "https://github.com/CatalaLang/dates-calc"
1212
bug-reports: "https://github.com/CatalaLang/dates-calc/issues"
1313
depends: [
14-
"dune" {>= "2.7"}
14+
"dune" {>= "3.11"}
1515
"ocaml" {>= "4.11.0"}
1616
"alcotest" {with-test & >= "1.5.0"}
1717
"qcheck" {with-test & >= "0.15"}

dune-project

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
(lang dune 2.7)
1+
(lang dune 3.11)
22

33
(name "dates_calc")
44
(authors

lib_c/dates_calc.c

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
/* This file is part of the Dates_calc library. Copyright (C) 2024 Inria,
2+
contributors: Louis Gesbert <[email protected]>, Raphaël Monat
3+
4+
5+
Licensed under the Apache License, Version 2.0 (the "License"); you may not
6+
use this file except in compliance with the License. You may obtain a copy of
7+
the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
License for the specific language governing permissions and limitations under
15+
the License. */
16+
17+
#include <assert.h>
18+
#include <stdio.h>
19+
20+
#define BOOL int
21+
#define FALSE 0
22+
#define TRUE 1
23+
24+
/* Layout of this C version:
25+
- types and functions in this module are prefixed with [dc_]
26+
- dates and periods are manipulated as pointers to the defined structs
27+
- functions return through a first [ret] pointer argument, the other
28+
arguments are [const].
29+
- functions that can fail return the [dc_success] type. What is stored into
30+
[ret] is unspecified when that is [dc_error].
31+
- it is expected that [ret] and other arguments may overlap, so, in the code
32+
below, a field in [ret] should never be written before the same field in
33+
any other argument of the same type is read.
34+
*/
35+
36+
typedef enum dc_success {
37+
dc_error, dc_ok
38+
} dc_success;
39+
40+
typedef enum dc_date_rounding {
41+
dc_date_round_up,
42+
dc_date_round_down,
43+
dc_date_round_abort
44+
} dc_date_rounding;
45+
46+
47+
typedef struct dc_date {
48+
long int year;
49+
unsigned long int month;
50+
unsigned long int day;
51+
} dc_date;
52+
53+
typedef struct dc_period {
54+
long int years;
55+
long int months;
56+
long int days;
57+
} dc_period;
58+
59+
void dc_make_period(dc_period *ret, const long int y, const long int m, const long int d) {
60+
ret->years = y;
61+
ret->months = m;
62+
ret->days = d;
63+
}
64+
65+
void dc_print_period (const dc_period *p) {
66+
printf("[%ld years, %ld months, %ld days]", p->years, p->months, p->days);
67+
}
68+
69+
dc_success dc_period_of_string (dc_period *ret, const char* s) {
70+
if
71+
(sscanf(s, "[%ld years, %ld months, %ld days]",
72+
&ret->years, &ret->months, &ret->days)
73+
== 3)
74+
return dc_ok;
75+
else
76+
return dc_error;
77+
}
78+
79+
void dc_add_periods (dc_period *ret, const dc_period *p1, const dc_period *p2) {
80+
ret->years = p1->years + p2->years;
81+
ret->months = p1->months + p2->months;
82+
ret->days = p1->days + p2->days;
83+
}
84+
85+
void dc_sub_periods (dc_period *ret, const dc_period *p1, const dc_period *p2) {
86+
ret->years = p1->years - p2->years;
87+
ret->months = p1->months - p2->months;
88+
ret->days = p1->days - p2->days;
89+
}
90+
91+
void dc_mul_periods (dc_period *ret, const dc_period *p, const long int m) {
92+
ret->years = p->years * m;
93+
ret->months = p->months * m;
94+
ret->days = p->days * m;
95+
}
96+
97+
dc_success dc_period_to_days (long int *ret, const dc_period *p) {
98+
if (p->years || p->months) return dc_error;
99+
else *ret = p->days;
100+
return dc_ok;
101+
}
102+
103+
BOOL dc_is_leap_year (const long int y) {
104+
return (y % 400 == 0 || (y % 4 == 0 && y % 100 != 0));
105+
}
106+
107+
unsigned long int dc_days_in_month (const dc_date *d) {
108+
switch (d->month) {
109+
case 2:
110+
return (dc_is_leap_year(d->year) ? 29 : 28);
111+
case 4: case 6: case 9: case 11:
112+
return 30;
113+
default:
114+
return 31;
115+
}
116+
}
117+
118+
BOOL dc_is_valid_date (const dc_date *d) {
119+
return (1 <= d->day && d->day <= dc_days_in_month(d));
120+
}
121+
122+
dc_success dc_make_date(dc_date *ret, const long int y, const unsigned long int m, const unsigned long int d) {
123+
ret->year = y;
124+
ret->month = m;
125+
ret->day = d;
126+
if (dc_is_valid_date(ret)) return dc_ok;
127+
else return dc_error;
128+
}
129+
130+
void dc_copy_date(dc_date *ret, const dc_date *d) {
131+
if (ret != d) {
132+
ret->year = d->year;
133+
ret->month = d->month;
134+
ret->day = d->day;
135+
}
136+
}
137+
138+
/* Precondition: [1 <= d->month <= 12]. The returned day is always [1] */
139+
void dc_add_months(dc_date *ret, const dc_date *d, const long int months) {
140+
long int month = d->month - 1 + months;
141+
/* The month variable is shifted -1 to be in range [0, 11] for modulo
142+
calculations */
143+
/* assert (1 <= d->month && d->month <= 12); */
144+
ret->day = 1;
145+
ret->month = (month >= 0 ? month % 12 : month % 12 + 12) + 1;
146+
ret->year = d->year + (month >= 0 ? month / 12 : month / 12 - 1);
147+
}
148+
149+
/* If the date is valid, does nothing. We expect the month number to be always
150+
valid when calling this. If the date is invalid due to the day number, then
151+
this function rounds down: if the day number is >= days_in_month, to the last
152+
day of the current month. */
153+
void dc_prev_valid_date (dc_date *ret, const dc_date *d) {
154+
assert (1 <= d->month && d->month <= 12);
155+
assert (1 <= d->day && d->day <= 31);
156+
if (dc_is_valid_date(d))
157+
dc_copy_date(ret, d);
158+
else {
159+
ret->year = d->year;
160+
ret->month = d->month;
161+
ret->day = dc_days_in_month(d);
162+
}
163+
}
164+
165+
/* If the date is valid, does nothing. We expect the month number to be always
166+
valid when calling this. If the date is invalid due to the day number, then
167+
this function rounds down: if the day number is >= days_in_month, to the
168+
first day of the next month. */
169+
void dc_next_valid_date (dc_date *ret, const dc_date *d) {
170+
assert (1 <= d->month && d->month <= 12);
171+
assert (1 <= d->day && d->day <= 31);
172+
if (dc_is_valid_date(d))
173+
dc_copy_date(ret, d);
174+
else {
175+
dc_add_months (ret, d, 1);
176+
}
177+
}
178+
179+
dc_success dc_round_date (dc_date *ret, const dc_date_rounding rnd, const dc_date *d) {
180+
if (dc_is_valid_date(d)) {
181+
dc_copy_date(ret, d);
182+
return dc_ok;
183+
} else switch (rnd) {
184+
case dc_date_round_down:
185+
dc_prev_valid_date(ret, d);
186+
return dc_ok;
187+
case dc_date_round_up:
188+
dc_next_valid_date(ret, d);
189+
return dc_ok;
190+
default:
191+
return dc_error;
192+
}
193+
}
194+
195+
void add_dates_days (dc_date *ret, const dc_date *d, const long int days) {
196+
unsigned long int days_in_d_month;
197+
unsigned long int day_num;
198+
/* Hello, dear reader! Buckle up because it will be a hard ride. The first
199+
thing to do here is to retrieve how many days there are in the current
200+
month of [d]. */
201+
days_in_d_month = dc_days_in_month(d);
202+
/* Now, we case analyze of the situation. To do that, we add the current days
203+
of the month with [days], and see what happens. Beware, [days] is algebraic
204+
and can be negative! */
205+
day_num = d->day + days;
206+
if (day_num < 1) {
207+
/* we substracted too many days and the current month can't handle it. So we
208+
warp to the previous month and let a recursive call handle the situation
209+
from there. */
210+
dc_date d1;
211+
/* We warp to the last day of the previous month. */
212+
dc_add_months(&d1, d, -1);
213+
d1.day = dc_days_in_month(&d1);
214+
/* What remains to be substracted (as [days] is negative) has to be
215+
diminished by the number of days of the date in the current month. */
216+
add_dates_days(ret, &d1, days + d->day);
217+
} else if (days_in_d_month < day_num) {
218+
/* Here there is an overflow : you have added too many days and the current
219+
month cannot handle them any more. The strategy here is to fill the
220+
current month, and let the next month handle the situation via a
221+
recursive call. */
222+
dc_date d1;
223+
/* We warp to the first day of the next month! */
224+
dc_add_months(&d1, d, 1);
225+
/* Now we compute how many days we still have left to add. Because we have
226+
warped to the next month, we already have added the rest of the days in
227+
the current month: [days_in_d_month - d.day]. But then we switch
228+
months, and that corresponds to adding another day. */
229+
add_dates_days(ret, &d1, days - (days_in_d_month - d->day) - 1);
230+
} else {
231+
/* this is the easy case: when you add [days], the new day keeps
232+
being a valid day in the current month. All is good, we simply warp to
233+
that new date without any further changes. */
234+
ret->year = d->year;
235+
ret->month = d->month;
236+
ret->day = day_num;
237+
}
238+
}
239+
240+
dc_success dc_add_dates (dc_date *ret, const dc_date_rounding rnd, const dc_date *d, const dc_period *p) {
241+
dc_success success;
242+
ret->year = d->year + p->years;
243+
ret->month = d->month;
244+
/* NB: at this point, the date may not be correct.
245+
Rounding is performed after add_months */
246+
dc_add_months(ret, ret, p->months);
247+
ret->day = d->day;
248+
success = dc_round_date(ret, rnd, ret);
249+
if (success == dc_ok) {
250+
add_dates_days(ret, ret, p->days);
251+
return dc_ok;
252+
} else
253+
return success;
254+
}
255+
256+
int dc_compare_dates (const dc_date *d1, const dc_date *d2) {
257+
long int cmp;
258+
cmp = d1->year - d2->year;
259+
if (cmp > 0) return 1;
260+
if (cmp < 0) return -1;
261+
cmp = d1->month - d2->month;
262+
if (cmp > 0) return 1;
263+
if (cmp < 0) return -1;
264+
cmp = d1->day - d2->day;
265+
if (cmp > 0) return 1;
266+
if (cmp < 0) return -1;
267+
return 0;
268+
}
269+
270+
/* Respects ISO8601 format. */
271+
void dc_print_date (const dc_date *d) {
272+
printf("%04ld-%02lu-%02lu", d->year, d->month, d->day);
273+
}
274+
275+
dc_success dc_date_of_string (dc_date *ret, const char* s) {
276+
if (sscanf(s, "%4ld-%2lu-%2lu",
277+
&ret->year, &ret->month, &ret->day)
278+
== 3)
279+
return dc_ok;
280+
else
281+
return dc_error;
282+
}
283+
284+
void dc_first_day_of_month (dc_date *ret, const dc_date *d) {
285+
assert(dc_is_valid_date(d));
286+
ret->year = d->year;
287+
ret->month = d->month;
288+
ret->day = 1;
289+
}
290+
291+
void dc_last_day_of_month (dc_date *ret, const dc_date *d) {
292+
assert(dc_is_valid_date(d));
293+
ret->year = d->year;
294+
ret->month = d->month;
295+
ret->day = dc_days_in_month(d);
296+
}
297+
298+
void dc_neg_period (dc_period *ret, const dc_period *p) {
299+
ret->years = - p->years;
300+
ret->months = - p->months;
301+
ret->days = - p->days;
302+
}
303+
304+
/* The returned [period] is always expressed as a number of days. */
305+
void dc_sub_dates (dc_period *ret, const dc_date *d1, const dc_date *d2) {
306+
ret->years = 0;
307+
ret->months = 0;
308+
if (d1->year == d2->year && d1->month == d2->month) {
309+
/* Easy case: the two dates are in the same month. */
310+
ret->days = d1->day - d2->day;
311+
} else if (dc_compare_dates(d1, d2) < 0) {
312+
/* The case were d1 is after d2 is symmetrical so we handle it via a
313+
recursive call changing the order of the arguments. */
314+
dc_sub_dates(ret, d2, d1);
315+
dc_neg_period(ret, ret);
316+
} else { /* d1 > d2 : */
317+
/* We warp d2 to the first day of the next month. */
318+
dc_date d2x;
319+
dc_add_months(&d2x, d2, 1);
320+
/* Next we divide the result between the number of days we've added to go
321+
to the end of the month, and the remaining handled by a recursive
322+
call. */
323+
dc_sub_dates(ret, d1, &d2x);
324+
/* The number of days is the difference between the last day of the
325+
month and the current day of d1, plus one day because we go to
326+
the next month. */
327+
ret->days += dc_days_in_month(d2) - d2->day + 1;
328+
}
329+
}
330+
331+
long int dc_date_year(const dc_date *d) {
332+
return d->year;
333+
}
334+
335+
unsigned long int dc_date_month(const dc_date *d) {
336+
return d->month;
337+
}
338+
339+
unsigned long int dc_date_day(const dc_date *d) {
340+
return d->day;
341+
}
342+
343+
long int dc_period_years(const dc_period *p) {
344+
return p->years;
345+
}
346+
347+
long int dc_period_months(const dc_period *p) {
348+
return p->months;
349+
}
350+
351+
long int dc_period_days(const dc_period *p) {
352+
return p->days;
353+
}
354+

0 commit comments

Comments
 (0)