diff --git a/README.md b/README.md index 186bd87..e9bacd1 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ 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. -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). +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). 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). diff --git a/dates_calc.opam b/dates_calc.opam index 8cd661b..4921445 100644 --- a/dates_calc.opam +++ b/dates_calc.opam @@ -6,12 +6,12 @@ A date calculation library, with exact operators to add a given number of days to a date, and approximate operators to add months or years.""" maintainer: ["Raphaël Monat "] -authors: ["Aymeric Fromherz" "Denis Merigoux" "Raphaël Monat"] +authors: ["Aymeric Fromherz" "Denis Merigoux" "Raphaël Monat" "Louis Gesbert"] license: "Apache-2.0" homepage: "https://github.com/CatalaLang/dates-calc" bug-reports: "https://github.com/CatalaLang/dates-calc/issues" depends: [ - "dune" {>= "2.7"} + "dune" {>= "3.11"} "ocaml" {>= "4.11.0"} "alcotest" {with-test & >= "1.5.0"} "qcheck" {with-test & >= "0.15"} diff --git a/dune-project b/dune-project index aa98c8f..774b143 100644 --- a/dune-project +++ b/dune-project @@ -1,4 +1,4 @@ -(lang dune 2.7) +(lang dune 3.11) (name "dates_calc") (authors diff --git a/lib_c/dates_calc.c b/lib_c/dates_calc.c new file mode 100644 index 0000000..7d12406 --- /dev/null +++ b/lib_c/dates_calc.c @@ -0,0 +1,354 @@ +/* This file is part of the Dates_calc library. Copyright (C) 2024 Inria, + contributors: Louis Gesbert , Raphaël Monat + + + Licensed under the Apache License, Version 2.0 (the "License"); you may not + use this file except in compliance with the License. You may obtain a copy of + the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + License for the specific language governing permissions and limitations under + the License. */ + +#include +#include + +#define BOOL int +#define FALSE 0 +#define TRUE 1 + +/* Layout of this C version: + - types and functions in this module are prefixed with [dc_] + - dates and periods are manipulated as pointers to the defined structs + - functions return through a first [ret] pointer argument, the other + arguments are [const]. + - functions that can fail return the [dc_success] type. What is stored into + [ret] is unspecified when that is [dc_error]. + - it is expected that [ret] and other arguments may overlap, so, in the code + below, a field in [ret] should never be written before the same field in + any other argument of the same type is read. +*/ + +typedef enum dc_success { + dc_error, dc_ok +} dc_success; + +typedef enum dc_date_rounding { + dc_date_round_up, + dc_date_round_down, + dc_date_round_abort +} dc_date_rounding; + + +typedef struct dc_date { + long int year; + unsigned long int month; + unsigned long int day; +} dc_date; + +typedef struct dc_period { + long int years; + long int months; + long int days; +} dc_period; + +void dc_make_period(dc_period *ret, const long int y, const long int m, const long int d) { + ret->years = y; + ret->months = m; + ret->days = d; +} + +void dc_print_period (const dc_period *p) { + printf("[%ld years, %ld months, %ld days]", p->years, p->months, p->days); +} + +dc_success dc_period_of_string (dc_period *ret, const char* s) { + if + (sscanf(s, "[%ld years, %ld months, %ld days]", + &ret->years, &ret->months, &ret->days) + == 3) + return dc_ok; + else + return dc_error; +} + +void dc_add_periods (dc_period *ret, const dc_period *p1, const dc_period *p2) { + ret->years = p1->years + p2->years; + ret->months = p1->months + p2->months; + ret->days = p1->days + p2->days; +} + +void dc_sub_periods (dc_period *ret, const dc_period *p1, const dc_period *p2) { + ret->years = p1->years - p2->years; + ret->months = p1->months - p2->months; + ret->days = p1->days - p2->days; +} + +void dc_mul_periods (dc_period *ret, const dc_period *p, const long int m) { + ret->years = p->years * m; + ret->months = p->months * m; + ret->days = p->days * m; +} + +dc_success dc_period_to_days (long int *ret, const dc_period *p) { + if (p->years || p->months) return dc_error; + else *ret = p->days; + return dc_ok; +} + +BOOL dc_is_leap_year (const long int y) { + return (y % 400 == 0 || (y % 4 == 0 && y % 100 != 0)); +} + +unsigned long int dc_days_in_month (const dc_date *d) { + switch (d->month) { + case 2: + return (dc_is_leap_year(d->year) ? 29 : 28); + case 4: case 6: case 9: case 11: + return 30; + default: + return 31; + } +} + +BOOL dc_is_valid_date (const dc_date *d) { + return (1 <= d->day && d->day <= dc_days_in_month(d)); +} + +dc_success dc_make_date(dc_date *ret, const long int y, const unsigned long int m, const unsigned long int d) { + ret->year = y; + ret->month = m; + ret->day = d; + if (dc_is_valid_date(ret)) return dc_ok; + else return dc_error; +} + +void dc_copy_date(dc_date *ret, const dc_date *d) { + if (ret != d) { + ret->year = d->year; + ret->month = d->month; + ret->day = d->day; + } +} + +/* Precondition: [1 <= d->month <= 12]. The returned day is always [1] */ +void dc_add_months(dc_date *ret, const dc_date *d, const long int months) { + long int month = d->month - 1 + months; + /* The month variable is shifted -1 to be in range [0, 11] for modulo + calculations */ + /* assert (1 <= d->month && d->month <= 12); */ + ret->day = 1; + ret->month = (month >= 0 ? month % 12 : month % 12 + 12) + 1; + ret->year = d->year + (month >= 0 ? month / 12 : month / 12 - 1); +} + +/* If the date is valid, does nothing. We expect the month number to be always + valid when calling this. If the date is invalid due to the day number, then + this function rounds down: if the day number is >= days_in_month, to the last + day of the current month. */ +void dc_prev_valid_date (dc_date *ret, const dc_date *d) { + assert (1 <= d->month && d->month <= 12); + assert (1 <= d->day && d->day <= 31); + if (dc_is_valid_date(d)) + dc_copy_date(ret, d); + else { + ret->year = d->year; + ret->month = d->month; + ret->day = dc_days_in_month(d); + } +} + +/* If the date is valid, does nothing. We expect the month number to be always + valid when calling this. If the date is invalid due to the day number, then + this function rounds down: if the day number is >= days_in_month, to the + first day of the next month. */ +void dc_next_valid_date (dc_date *ret, const dc_date *d) { + assert (1 <= d->month && d->month <= 12); + assert (1 <= d->day && d->day <= 31); + if (dc_is_valid_date(d)) + dc_copy_date(ret, d); + else { + dc_add_months (ret, d, 1); + } +} + +dc_success dc_round_date (dc_date *ret, const dc_date_rounding rnd, const dc_date *d) { + if (dc_is_valid_date(d)) { + dc_copy_date(ret, d); + return dc_ok; + } else switch (rnd) { + case dc_date_round_down: + dc_prev_valid_date(ret, d); + return dc_ok; + case dc_date_round_up: + dc_next_valid_date(ret, d); + return dc_ok; + default: + return dc_error; + } +} + +void add_dates_days (dc_date *ret, const dc_date *d, const long int days) { + unsigned long int days_in_d_month; + unsigned long int day_num; + /* Hello, dear reader! Buckle up because it will be a hard ride. The first + thing to do here is to retrieve how many days there are in the current + month of [d]. */ + days_in_d_month = dc_days_in_month(d); + /* Now, we case analyze of the situation. To do that, we add the current days + of the month with [days], and see what happens. Beware, [days] is algebraic + and can be negative! */ + day_num = d->day + days; + if (day_num < 1) { + /* we substracted too many days and the current month can't handle it. So we + warp to the previous month and let a recursive call handle the situation + from there. */ + dc_date d1; + /* We warp to the last day of the previous month. */ + dc_add_months(&d1, d, -1); + d1.day = dc_days_in_month(&d1); + /* What remains to be substracted (as [days] is negative) has to be + diminished by the number of days of the date in the current month. */ + add_dates_days(ret, &d1, days + d->day); + } else if (days_in_d_month < day_num) { + /* Here there is an overflow : you have added too many days and the current + month cannot handle them any more. The strategy here is to fill the + current month, and let the next month handle the situation via a + recursive call. */ + dc_date d1; + /* We warp to the first day of the next month! */ + dc_add_months(&d1, d, 1); + /* Now we compute how many days we still have left to add. Because we have + warped to the next month, we already have added the rest of the days in + the current month: [days_in_d_month - d.day]. But then we switch + months, and that corresponds to adding another day. */ + add_dates_days(ret, &d1, days - (days_in_d_month - d->day) - 1); + } else { + /* this is the easy case: when you add [days], the new day keeps + being a valid day in the current month. All is good, we simply warp to + that new date without any further changes. */ + ret->year = d->year; + ret->month = d->month; + ret->day = day_num; + } +} + +dc_success dc_add_dates (dc_date *ret, const dc_date_rounding rnd, const dc_date *d, const dc_period *p) { + dc_success success; + ret->year = d->year + p->years; + ret->month = d->month; + /* NB: at this point, the date may not be correct. + Rounding is performed after add_months */ + dc_add_months(ret, ret, p->months); + ret->day = d->day; + success = dc_round_date(ret, rnd, ret); + if (success == dc_ok) { + add_dates_days(ret, ret, p->days); + return dc_ok; + } else + return success; +} + +int dc_compare_dates (const dc_date *d1, const dc_date *d2) { + long int cmp; + cmp = d1->year - d2->year; + if (cmp > 0) return 1; + if (cmp < 0) return -1; + cmp = d1->month - d2->month; + if (cmp > 0) return 1; + if (cmp < 0) return -1; + cmp = d1->day - d2->day; + if (cmp > 0) return 1; + if (cmp < 0) return -1; + return 0; +} + +/* Respects ISO8601 format. */ +void dc_print_date (const dc_date *d) { + printf("%04ld-%02lu-%02lu", d->year, d->month, d->day); +} + +dc_success dc_date_of_string (dc_date *ret, const char* s) { + if (sscanf(s, "%4ld-%2lu-%2lu", + &ret->year, &ret->month, &ret->day) + == 3) + return dc_ok; + else + return dc_error; +} + +void dc_first_day_of_month (dc_date *ret, const dc_date *d) { + assert(dc_is_valid_date(d)); + ret->year = d->year; + ret->month = d->month; + ret->day = 1; +} + +void dc_last_day_of_month (dc_date *ret, const dc_date *d) { + assert(dc_is_valid_date(d)); + ret->year = d->year; + ret->month = d->month; + ret->day = dc_days_in_month(d); +} + +void dc_neg_period (dc_period *ret, const dc_period *p) { + ret->years = - p->years; + ret->months = - p->months; + ret->days = - p->days; +} + +/* The returned [period] is always expressed as a number of days. */ +void dc_sub_dates (dc_period *ret, const dc_date *d1, const dc_date *d2) { + ret->years = 0; + ret->months = 0; + if (d1->year == d2->year && d1->month == d2->month) { + /* Easy case: the two dates are in the same month. */ + ret->days = d1->day - d2->day; + } else if (dc_compare_dates(d1, d2) < 0) { + /* The case were d1 is after d2 is symmetrical so we handle it via a + recursive call changing the order of the arguments. */ + dc_sub_dates(ret, d2, d1); + dc_neg_period(ret, ret); + } else { /* d1 > d2 : */ + /* We warp d2 to the first day of the next month. */ + dc_date d2x; + dc_add_months(&d2x, d2, 1); + /* Next we divide the result between the number of days we've added to go + to the end of the month, and the remaining handled by a recursive + call. */ + dc_sub_dates(ret, d1, &d2x); + /* The number of days is the difference between the last day of the + month and the current day of d1, plus one day because we go to + the next month. */ + ret->days += dc_days_in_month(d2) - d2->day + 1; + } +} + +long int dc_date_year(const dc_date *d) { + return d->year; +} + +unsigned long int dc_date_month(const dc_date *d) { + return d->month; +} + +unsigned long int dc_date_day(const dc_date *d) { + return d->day; +} + +long int dc_period_years(const dc_period *p) { + return p->years; +} + +long int dc_period_months(const dc_period *p) { + return p->months; +} + +long int dc_period_days(const dc_period *p) { + return p->days; +} + diff --git a/lib_c/dates_calc.h b/lib_c/dates_calc.h new file mode 100644 index 0000000..0876df9 --- /dev/null +++ b/lib_c/dates_calc.h @@ -0,0 +1,75 @@ +/* This file is part of the Dates_calc library. Copyright (C) 2024 Inria, + contributors: Louis Gesbert , Raphaël Monat + + + Licensed under the Apache License, Version 2.0 (the "License"); you may not + use this file except in compliance with the License. You may obtain a copy of + the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + License for the specific language governing permissions and limitations under + the License. */ + +#ifndef __DATES_CALC_H__ +#define __DATES_CALC_H__ + +typedef enum dc_success { + dc_error, dc_ok +} dc_success; + + +/* It is expected for [dc_date] and [dc_period] to be stack-allocated by the + caller, so although we don't expose their contents, we provide [_opaque_data] + as a hint about their sizes to the compiler. */ +typedef struct dc_date { long int _opaque_data[3]; } dc_date; + +typedef struct dc_period { long int _opaque_data[3]; } dc_period; + +typedef enum dc_date_rounding { + dc_date_round_up, + dc_date_round_down, + dc_date_round_abort +} dc_date_rounding; + +dc_success dc_make_date(dc_date *ret, const long int y, const unsigned long int m, const unsigned long int d); + +dc_success dc_add_dates (dc_date *ret, const dc_date_rounding rnd, const dc_date *d, const dc_period *p); + +void dc_sub_dates (dc_period *ret, const dc_date *d1, const dc_date *d2); + +int dc_compare_dates (const dc_date *d1, const dc_date *d2); + +long int dc_date_year(const dc_date *d); +unsigned long int dc_date_month(const dc_date *d); +unsigned long int dc_date_day(const dc_date *d); + +void dc_print_date (const dc_date *d); + +dc_success dc_date_of_string (dc_date *ret, const char* s); + +void dc_first_day_of_month (dc_date *ret, const dc_date *d); + +void dc_last_day_of_month (dc_date *ret, const dc_date *d); + +int dc_is_leap_year (const long int y); + +void dc_make_period(dc_period *ret, const long int y, const long int m, const long int d); +void dc_neg_period (dc_period *ret, const dc_period *p); +void dc_add_periods (dc_period *ret, const dc_period *p1, const dc_period *p2); +void dc_sub_periods (dc_period *ret, const dc_period *p1, const dc_period *p2); +void dc_mul_periods (dc_period *ret, const dc_period *p, const long int m); + +void dc_print_period (const dc_period *p); +dc_success dc_period_of_string (dc_period *ret, const char* s); + +dc_success dc_period_to_days (long int *ret, const dc_period *p); + +long int dc_period_years(const dc_period *d); +long int dc_period_months(const dc_period *d); +long int dc_period_days(const dc_period *d); + +#endif /* __DATES_CALC_H */ diff --git a/lib_c/dune b/lib_c/dune new file mode 100644 index 0000000..64108d4 --- /dev/null +++ b/lib_c/dune @@ -0,0 +1,10 @@ +(rule + (deps dates_calc.c) + (target dates_calc.o) + (action (run %{cc} --std=c89 -Wall -Werror -pedantic -c %{deps} -o %{target}))) + +(install + (section lib) + (files + (dates_calc.o as c/dates_calc.o) + (dates_calc.h as c/dates_calc.h))) diff --git a/lib/dates.ml b/lib_ocaml/dates.ml similarity index 99% rename from lib/dates.ml rename to lib_ocaml/dates.ml index 4d0e66d..e164e2b 100644 --- a/lib/dates.ml +++ b/lib_ocaml/dates.ml @@ -264,7 +264,7 @@ let rec sub_dates (d1 : date) (d2 : date) : period = neg_period (sub_dates d2 d1) else (* we know cmp != 0 so cmp > 0*) - (* We warp d1 to the first day of the next month. *) + (* We warp d2 to the first day of the next month. *) let new_d2_year, new_d2_month = add_months_to_first_of_month_date ~year:d2.year ~month:d2.month ~months:1 diff --git a/lib/dates.mli b/lib_ocaml/dates.mli similarity index 100% rename from lib/dates.mli rename to lib_ocaml/dates.mli diff --git a/lib/dune b/lib_ocaml/dune similarity index 100% rename from lib/dune rename to lib_ocaml/dune diff --git a/lib_python/README.md b/lib_python/README.md new file mode 100644 index 0000000..9678905 --- /dev/null +++ b/lib_python/README.md @@ -0,0 +1 @@ +A date calculation library diff --git a/lib_python/dune b/lib_python/dune new file mode 100644 index 0000000..50f4c8d --- /dev/null +++ b/lib_python/dune @@ -0,0 +1,6 @@ +(install + (section lib) + (files + (pyproject.toml as python/pyproject.toml) + (README.md as python/README.md) + (glob_files_rec (src/** with_prefix python/src)))) diff --git a/lib_python/pyproject.toml b/lib_python/pyproject.toml new file mode 100644 index 0000000..0dd6a4b --- /dev/null +++ b/lib_python/pyproject.toml @@ -0,0 +1,28 @@ +[project] +name = "dates_calc" +description = "A date calculation library" +version = "0.10.0" +dependencies = [ + "typing", + "mypy", + "types-termcolor", + "typer[all]", + "typing-extensions" +] + +[project.optional-dependencies] +test = [ + "unittest", + "csv" +] + +[project.urls] +"Homepage" = "https://github.com/CatalaLang/dates-calc" +"Bug Tracker" = "https://github.com/CatalaLang/dates-calc/issues" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/catala"] diff --git a/lib_python/src/dates_calc/__init__.py b/lib_python/src/dates_calc/__init__.py new file mode 100644 index 0000000..b18efd3 --- /dev/null +++ b/lib_python/src/dates_calc/__init__.py @@ -0,0 +1 @@ +from .dates import * diff --git a/lib/dates.py b/lib_python/src/dates_calc/dates.py similarity index 99% rename from lib/dates.py rename to lib_python/src/dates_calc/dates.py index 01eb645..7aa419a 100644 --- a/lib/dates.py +++ b/lib_python/src/dates_calc/dates.py @@ -337,7 +337,7 @@ def __repr__(self): @classmethod def from_string(self, s : str) -> Period: - rege = re.compile("\[([0-9]+) years, ([0-9]+) months, ([0-9]+) days\]") + rege = re.compile("\\[([0-9]+) years, ([0-9]+) months, ([0-9]+) days\\]") match = rege.fullmatch(s) d = int(match[3]) m = int(match[2]) diff --git a/lib/__init__.py b/lib_python/src/dates_calc/py.typed similarity index 100% rename from lib/__init__.py rename to lib_python/src/dates_calc/py.typed diff --git a/test/__init__.py b/test/__init__.py index e69de29..faf5a48 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -0,0 +1,7 @@ +import os +import sys +PROJECT_PATH = os.getcwd() +SOURCE_PATH = os.path.join( + PROJECT_PATH,"../lib_python/src/dates_calc/" +) +sys.path.append(SOURCE_PATH) diff --git a/test/dune b/test/dune index bd7490b..f6ae47c 100644 --- a/test/dune +++ b/test/dune @@ -2,3 +2,27 @@ (names unit prop) (deps exact_computations.csv ambiguous_computations.csv first_last_day_of_month.csv) (libraries dates_calc alcotest qcheck)) + +(rule + (alias runtest) + (deps + (source_tree ../lib_python) + unit.py __init__.py + ambiguous_computations.csv + exact_computations.csv + first_last_day_of_month.csv) + (action (chdir .. (run python3 -m unittest test.unit)))) + +(rule + (target unit.c.exe) + (deps + ../lib_c/dates_calc.o + ../lib_c/dates_calc.h + unit.c) + (action + (run %{cc} -Wall -I ../lib_c unit.c ../lib_c/dates_calc.o -o unit.c.exe))) + +(rule + (alias runtest) + (action + (run ./unit.c.exe))) diff --git a/test/unit.c b/test/unit.c new file mode 100644 index 0000000..fc3060f --- /dev/null +++ b/test/unit.c @@ -0,0 +1,204 @@ +#include +#include +#include +#include + +unsigned long int test_add_dates_exact() +{ + const char* fname = "exact_computations.csv"; + FILE* f = fopen(fname, "r"); + char* readbuf = NULL; + size_t readbuflen = 0; + int failed = 0; + int total = 0; + if (f == NULL) { + fprintf(stderr, "Could not open file %s\n", fname); + abort(); + } + getline(&readbuf, &readbuflen, f); + while (getline(&readbuf, &readbuflen, f) >= 0) + { + char *s_t, *s_dt, *s_expect; + dc_date t, expect, result; + dc_period dt; + total++; + if (sscanf(readbuf, "%m[^;];%m[^;];%m[^;]", &s_t, &s_dt, &s_expect) < 3) { + fprintf(stderr, "Error: Bad test %s", readbuf); + failed++; + continue; + } + dc_date_of_string(&t, s_t); + dc_period_of_string(&dt, s_dt); + dc_date_of_string(&expect, s_expect); + dc_add_dates(&result, dc_date_round_abort, &t, &dt); + if (dc_compare_dates (&result, &expect) != 0) { + printf("Test FAILED: %s", readbuf); + printf(" "); + dc_print_date(&result); + printf(" != "); + dc_print_date(&expect); + printf("\n"); + failed++; + } + free(s_t); + free(s_dt); + free(s_expect); + } + free(readbuf); + fclose(f); + if (errno) { + fprintf(stderr, "Could not read file %s\n", fname); + abort(); + } + return ((total << 16) | failed); +} + +unsigned long int test_add_dates_ambiguous() +{ + const char* fname = "ambiguous_computations.csv"; + FILE* f = fopen(fname, "r"); + char* readbuf = NULL; + size_t readbuflen = 0; + int failed = 0; + int total = 0; + if (f == NULL) { + fprintf(stderr, "Could not open file %s\n", fname); + abort(); + } + getline(&readbuf, &readbuflen, f); + while (getline(&readbuf, &readbuflen, f) >= 0) + { + char *s_t, *s_dt, *s_expect_up, *s_expect_down; + dc_date t, expect_up, expect_down, result_up, result_down; + dc_period dt; + total++; + if (sscanf(readbuf, "%m[^;];%m[^;];%m[^;];%m[^;]", + &s_t, &s_dt, &s_expect_up, &s_expect_down) + < 4) + { + fprintf(stderr, "Error: Bad test %s", readbuf); + failed++; + continue; + } + dc_date_of_string(&t, s_t); + dc_period_of_string(&dt, s_dt); + dc_date_of_string(&expect_up, s_expect_up); + dc_date_of_string(&expect_down, s_expect_down); + dc_add_dates(&result_up, dc_date_round_up, &t, &dt); + dc_add_dates(&result_down, dc_date_round_down, &t, &dt); + if (dc_compare_dates (&result_up, &expect_up) != 0) { + printf("Test FAILED: %s", readbuf); + printf(" "); + dc_print_date(&result_up); + printf(" != "); + dc_print_date(&expect_up); + printf("\n"); + failed++; + } + else if (dc_compare_dates (&result_down, &expect_down) != 0) { + printf("Test FAILED: %s", readbuf); + printf(" "); + dc_print_date(&result_down); + printf(" != "); + dc_print_date(&expect_down); + printf("\n"); + failed++; + } + free(s_t); + free(s_dt); + free(s_expect_up); + free(s_expect_down); + } + free(readbuf); + fclose(f); + if (errno) { + fprintf(stderr, "Could not read file %s\n", fname); + abort(); + } + return ((total << 16) | failed); +} + +unsigned long int test_first_last_day_of_month() +{ + const char* fname = "first_last_day_of_month.csv"; + FILE* f = fopen(fname, "r"); + char* readbuf = NULL; + size_t readbuflen = 0; + int failed = 0; + int total = 0; + if (f == NULL) { + fprintf(stderr, "Could not open file %s\n", fname); + abort(); + } + getline(&readbuf, &readbuflen, f); + while (getline(&readbuf, &readbuflen, f) >= 0) + { + char *s_t, *s_expect_first, *s_expect_last; + dc_date t, expect_first, expect_last, result_first, result_last; + total++; + if (sscanf(readbuf, "%m[^;];%m[^;];%m[^;]", + &s_t, &s_expect_first, &s_expect_last) + < 3) + { + fprintf(stderr, "Error: Bad test %s", readbuf); + failed++; + continue; + } + dc_date_of_string(&t, s_t); + dc_date_of_string(&expect_first, s_expect_first); + dc_date_of_string(&expect_last, s_expect_last); + dc_first_day_of_month(&result_first, &t); + dc_last_day_of_month(&result_last, &t); + if (dc_compare_dates (&result_first, &expect_first) != 0) { + printf("Test FAILED: %s", readbuf); + printf(" "); + dc_print_date(&result_first); + printf(" != "); + dc_print_date(&expect_first); + printf("\n"); + failed++; + } + else if (dc_compare_dates (&result_last, &expect_last) != 0) { + printf("Test FAILED: %s", readbuf); + printf(" "); + dc_print_date(&result_last); + printf(" != "); + dc_print_date(&expect_last); + printf("\n"); + failed++; + } + free(s_t); + free(s_expect_first); + free(s_expect_last); + } + free(readbuf); + fclose(f); + if (errno) { + fprintf(stderr, "Could not read file %s\n", fname); + abort(); + } + return ((total << 16) | failed); +} + +int main() +{ + unsigned long int results = 0; + int total = 0; + int failed = 0; + results += test_add_dates_exact(); + results += test_add_dates_ambiguous(); + results += test_first_last_day_of_month(); + failed = results & 0xFFFF; + total = results >> 16; + if (failed > 0) { + printf("=== C Tests \x1b[31mFAILED\x1b[m ===\n"); + printf("Tests failed: \x1b[31m%d\x1b[m\n", failed); + printf("Tests passed: %d / %d\n", total - failed, total); + return 1; + } + else { + printf("=== C Tests \x1b[32mPASSED\x1b[m ===\n"); + printf("Tests passed: %d / %d\n", total, total); + return 0; + } +} diff --git a/test/unit.py b/test/unit.py index d1169fd..d1f0095 100644 --- a/test/unit.py +++ b/test/unit.py @@ -1,7 +1,7 @@ # Run from root directory of dates-calc with `python3 -m unittest test.unit` import unittest import csv -from lib.dates import Date, Period, AbortOnRound, RoundUp, RoundDown, addup, adddown, AmbiguousComputation +from lib_python.src.dates_calc import Date, Period, AbortOnRound, RoundUp, RoundDown, addup, adddown, AmbiguousComputation class TestDates(unittest.TestCase): def read_csv(self, filename, drop_header=True):