[m-rev.] diff: improve tests for converting durations to and from strings
Julien Fischer
jfischer at opturion.com
Wed Apr 8 22:25:19 AEST 2026
Improve tests for converting durations to and from strings.
Add more systematic tests of conversion of durations to and from strings.
Fix two bugs in duration_from_string/2 identified as a result of the new
tests. The first is that the duration string "P" was incorrectly accepted as
the zero duration; the second is that a fractional seconds component with no
whole number part (e.g. "PT.5S"), was incorrectly accepted.
library/calendar.m:
Fix the two bugs above.
tests/hard_coded/Mmakefile:
tests/hard_coded/calendar_duration_conv.{m,exp}:
Add the new test.
Julien.
diff --git a/library/calendar.m b/library/calendar.m
index bf0a90b60..496f8677a 100644
--- a/library/calendar.m
+++ b/library/calendar.m
@@ -998,6 +998,7 @@ duration_from_string(Str, Duration) :-
!:Chars = string.to_char_list(Str),
read_sign(Sign, !Chars),
read_char('P', !Chars),
+ !.Chars \= [],
read_years(Years, !Chars),
read_months(Months, !Chars),
read_days(Days, !Chars),
@@ -1523,7 +1524,8 @@ read_minutes(Minutes, !Chars) :-
read_seconds_and_microseconds(Seconds, MicroSeconds, !Chars) :-
( if
- read_int(Seconds0, !.Chars, Chars1),
+ read_int_and_num_chars(Seconds0, NumChars, !.Chars, Chars1),
+ NumChars > 0,
read_microseconds(MicroSeconds0, Chars1, Chars2),
read_char('S', Chars2, Chars3)
then
diff --git a/tests/hard_coded/Mmakefile b/tests/hard_coded/Mmakefile
index d60586b75..9b1cc4e87 100644
--- a/tests/hard_coded/Mmakefile
+++ b/tests/hard_coded/Mmakefile
@@ -793,6 +793,7 @@ ifeq "$(findstring profdeep,$(GRADE))" ""
bitwise_uint8 \
calendar_basics \
calendar_date_time_conv \
+ calendar_duration_conv \
calendar_init_date \
char_to_string \
clamp_int \
diff --git a/tests/hard_coded/calendar_duration_conv.exp
b/tests/hard_coded/calendar_duration_conv.exp
new file mode 100644
index 000000000..6ad3fd3a9
--- /dev/null
+++ b/tests/hard_coded/calendar_duration_conv.exp
@@ -0,0 +1,172 @@
+=== Testing duration_from_string/2 with valid inputs ===
+
+duration_from_string("P1Y") ===> TEST PASSED (accepted: duration(12, 0, 0, 0))
+duration_from_string("P1M") ===> TEST PASSED (accepted: duration(1, 0, 0, 0))
+duration_from_string("P1D") ===> TEST PASSED (accepted: duration(0, 1, 0, 0))
+duration_from_string("P1Y2M") ===> TEST PASSED (accepted:
duration(14, 0, 0, 0))
+duration_from_string("P1Y2D") ===> TEST PASSED (accepted:
duration(12, 2, 0, 0))
+duration_from_string("P1M2D") ===> TEST PASSED (accepted: duration(1, 2, 0, 0))
+duration_from_string("P1Y2M3D") ===> TEST PASSED (accepted:
duration(14, 3, 0, 0))
+duration_from_string("PT1H") ===> TEST PASSED (accepted: duration(0,
0, 3600, 0))
+duration_from_string("PT1M") ===> TEST PASSED (accepted: duration(0, 0, 60, 0))
+duration_from_string("PT1S") ===> TEST PASSED (accepted: duration(0, 0, 1, 0))
+duration_from_string("PT1H2M") ===> TEST PASSED (accepted:
duration(0, 0, 3720, 0))
+duration_from_string("PT1H2S") ===> TEST PASSED (accepted:
duration(0, 0, 3602, 0))
+duration_from_string("PT1M2S") ===> TEST PASSED (accepted:
duration(0, 0, 62, 0))
+duration_from_string("PT1H2M3S") ===> TEST PASSED (accepted:
duration(0, 0, 3723, 0))
+duration_from_string("P1DT1H") ===> TEST PASSED (accepted:
duration(0, 1, 3600, 0))
+duration_from_string("P1Y2M3DT4H5M6S") ===> TEST PASSED (accepted:
duration(14, 3, 14706, 0))
+duration_from_string("-P1D") ===> TEST PASSED (accepted: duration(0, -1, 0, 0))
+duration_from_string("-P1Y2M3DT4H5M6S") ===> TEST PASSED (accepted:
duration(-14, -3, -14706, 0))
+duration_from_string("P18M") ===> TEST PASSED (accepted: duration(18, 0, 0, 0))
+duration_from_string("PT25H") ===> TEST PASSED (accepted: duration(0,
1, 3600, 0))
+duration_from_string("PT90S") ===> TEST PASSED (accepted: duration(0,
0, 90, 0))
+duration_from_string("P1Y18M100DT10H15M90.0003S") ===> TEST PASSED
(accepted: duration(30, 100, 36990, 300))
+duration_from_string("PT1.1S") ===> TEST PASSED (accepted:
duration(0, 0, 1, 100000))
+duration_from_string("PT1.12S") ===> TEST PASSED (accepted:
duration(0, 0, 1, 120000))
+duration_from_string("PT1.123S") ===> TEST PASSED (accepted:
duration(0, 0, 1, 123000))
+duration_from_string("PT1.1234S") ===> TEST PASSED (accepted:
duration(0, 0, 1, 123400))
+duration_from_string("PT1.12345S") ===> TEST PASSED (accepted:
duration(0, 0, 1, 123450))
+duration_from_string("PT1.123456S") ===> TEST PASSED (accepted:
duration(0, 0, 1, 123456))
+duration_from_string("PT0.000001S") ===> TEST PASSED (accepted:
duration(0, 0, 0, 1))
+duration_from_string("PT0.999999S") ===> TEST PASSED (accepted:
duration(0, 0, 0, 999999))
+duration_from_string("PT0.5S") ===> TEST PASSED (accepted:
duration(0, 0, 0, 500000))
+duration_from_string("PT0.0003S") ===> TEST PASSED (accepted:
duration(0, 0, 0, 300))
+duration_from_string("PT0S") ===> TEST PASSED (accepted: duration(0, 0, 0, 0))
+duration_from_string("PT0M") ===> TEST PASSED (accepted: duration(0, 0, 0, 0))
+duration_from_string("PT0H") ===> TEST PASSED (accepted: duration(0, 0, 0, 0))
+duration_from_string("P0M") ===> TEST PASSED (accepted: duration(0, 0, 0, 0))
+duration_from_string("P0D") ===> TEST PASSED (accepted: duration(0, 0, 0, 0))
+duration_from_string("P0Y0M0DT0H0M0S") ===> TEST PASSED (accepted:
duration(0, 0, 0, 0))
+
+=== Testing duration_from_string/2 with invalid inputs ===
+
+duration_from_string("") ===> TEST PASSED (rejected: empty string)
+duration_from_string(" ") ===> TEST PASSED (rejected: blank string)
+duration_from_string("not a duration") ===> TEST PASSED (rejected:
not a duration)
+duration_from_string("1Y2M3D") ===> TEST PASSED (rejected: missing P prefix)
+duration_from_string("P") ===> TEST PASSED (rejected: P with no components)
+duration_from_string(" P") ===> TEST PASSED (rejected: P with no
components and leading whitespace)
+duration_from_string("P ") ===> TEST PASSED (rejected: P with no
components and trailing whitespace)
+duration_from_string("T1H") ===> TEST PASSED (rejected: missing P
prefix, starts with T)
+duration_from_string("PT") ===> TEST PASSED (rejected: P and T but no
time components)
+duration_from_string("-P") ===> TEST PASSED (rejected: negative P
with no components)
+duration_from_string("p1Y") ===> TEST PASSED (rejected: lowercase p)
+duration_from_string("P1y") ===> TEST PASSED (rejected: lowercase
unit designator)
+duration_from_string("P1DT1H30MS") ===> TEST PASSED (rejected: bare S
with no digits)
+duration_from_string("P1H") ===> TEST PASSED (rejected: hours without
T separator)
+duration_from_string("P1S") ===> TEST PASSED (rejected: seconds
without T separator)
+duration_from_string("P1DT1H2M3Y") ===> TEST PASSED (rejected: years
after time components)
+duration_from_string("P1M1Y") ===> TEST PASSED (rejected: years after months)
+duration_from_string("PT1S1H") ===> TEST PASSED (rejected: hours after seconds)
+duration_from_string("PT1S1M") ===> TEST PASSED (rejected: minutes
after seconds)
+duration_from_string("PT1M1H") ===> TEST PASSED (rejected: hours after minutes)
+duration_from_string("P1Y1Y") ===> TEST PASSED (rejected: years
specified twice)
+duration_from_string("P1D1D") ===> TEST PASSED (rejected: days specified twice)
+duration_from_string("PT1H1H") ===> TEST PASSED (rejected: hours
specified twice)
+duration_from_string("P-1Y") ===> TEST PASSED (rejected: negative
number after P)
+duration_from_string("PT-1H") ===> TEST PASSED (rejected: negative
number after T)
+duration_from_string("--P1D") ===> TEST PASSED (rejected: double
negative prefix)
+duration_from_string("-P-1D") ===> TEST PASSED (rejected: negative
prefix and negative component)
+duration_from_string("P1.5Y") ===> TEST PASSED (rejected: fractional years)
+duration_from_string("P1.5M") ===> TEST PASSED (rejected: fractional months)
+duration_from_string("P1.5D") ===> TEST PASSED (rejected: fractional days)
+duration_from_string("PT1.5H") ===> TEST PASSED (rejected: fractional hours)
+duration_from_string("PT1.5M") ===> TEST PASSED (rejected: fractional minutes)
+duration_from_string("PT1.1234567S") ===> TEST PASSED (rejected:
seven fractional digits on seconds)
+duration_from_string("PT1.S") ===> TEST PASSED (rejected: decimal
point with no fraction digits)
+duration_from_string("PT.5S") ===> TEST PASSED (rejected: no integer
part before decimal point)
+duration_from_string("P1D ") ===> TEST PASSED (rejected: trailing space)
+duration_from_string(" P1D") ===> TEST PASSED (rejected: leading space)
+duration_from_string("P1DT1H30M extra") ===> TEST PASSED (rejected:
trailing text)
+duration_from_string("P1DX") ===> TEST PASSED (rejected: invalid
character after component)
+duration_from_string("PxY") ===> TEST PASSED (rejected: letter where
number expected)
+duration_from_string("PT1HxM") ===> TEST PASSED (rejected: letter in
time component)
+duration_from_string("P1DT") ===> TEST PASSED (rejected: T with no
time components)
+
+=== Testing det_duration_from_string/1 with valid inputs ===
+
+det_duration_from_string("P1Y") ===> TEST PASSED (accepted:
duration(12, 0, 0, 0))
+det_duration_from_string("P1M") ===> TEST PASSED (accepted:
duration(1, 0, 0, 0))
+det_duration_from_string("P1D") ===> TEST PASSED (accepted:
duration(0, 1, 0, 0))
+det_duration_from_string("P1Y2M") ===> TEST PASSED (accepted:
duration(14, 0, 0, 0))
+det_duration_from_string("P1Y2D") ===> TEST PASSED (accepted:
duration(12, 2, 0, 0))
+det_duration_from_string("P1M2D") ===> TEST PASSED (accepted:
duration(1, 2, 0, 0))
+det_duration_from_string("P1Y2M3D") ===> TEST PASSED (accepted:
duration(14, 3, 0, 0))
+det_duration_from_string("PT1H") ===> TEST PASSED (accepted:
duration(0, 0, 3600, 0))
+det_duration_from_string("PT1M") ===> TEST PASSED (accepted:
duration(0, 0, 60, 0))
+det_duration_from_string("PT1S") ===> TEST PASSED (accepted:
duration(0, 0, 1, 0))
+det_duration_from_string("PT1H2M") ===> TEST PASSED (accepted:
duration(0, 0, 3720, 0))
+det_duration_from_string("PT1H2S") ===> TEST PASSED (accepted:
duration(0, 0, 3602, 0))
+det_duration_from_string("PT1M2S") ===> TEST PASSED (accepted:
duration(0, 0, 62, 0))
+det_duration_from_string("PT1H2M3S") ===> TEST PASSED (accepted:
duration(0, 0, 3723, 0))
+det_duration_from_string("P1DT1H") ===> TEST PASSED (accepted:
duration(0, 1, 3600, 0))
+det_duration_from_string("P1Y2M3DT4H5M6S") ===> TEST PASSED
(accepted: duration(14, 3, 14706, 0))
+det_duration_from_string("-P1D") ===> TEST PASSED (accepted:
duration(0, -1, 0, 0))
+det_duration_from_string("-P1Y2M3DT4H5M6S") ===> TEST PASSED
(accepted: duration(-14, -3, -14706, 0))
+det_duration_from_string("P18M") ===> TEST PASSED (accepted:
duration(18, 0, 0, 0))
+det_duration_from_string("PT25H") ===> TEST PASSED (accepted:
duration(0, 1, 3600, 0))
+det_duration_from_string("PT90S") ===> TEST PASSED (accepted:
duration(0, 0, 90, 0))
+det_duration_from_string("P1Y18M100DT10H15M90.0003S") ===> TEST
PASSED (accepted: duration(30, 100, 36990, 300))
+det_duration_from_string("PT1.1S") ===> TEST PASSED (accepted:
duration(0, 0, 1, 100000))
+det_duration_from_string("PT1.12S") ===> TEST PASSED (accepted:
duration(0, 0, 1, 120000))
+det_duration_from_string("PT1.123S") ===> TEST PASSED (accepted:
duration(0, 0, 1, 123000))
+det_duration_from_string("PT1.1234S") ===> TEST PASSED (accepted:
duration(0, 0, 1, 123400))
+det_duration_from_string("PT1.12345S") ===> TEST PASSED (accepted:
duration(0, 0, 1, 123450))
+det_duration_from_string("PT1.123456S") ===> TEST PASSED (accepted:
duration(0, 0, 1, 123456))
+det_duration_from_string("PT0.000001S") ===> TEST PASSED (accepted:
duration(0, 0, 0, 1))
+det_duration_from_string("PT0.999999S") ===> TEST PASSED (accepted:
duration(0, 0, 0, 999999))
+det_duration_from_string("PT0.5S") ===> TEST PASSED (accepted:
duration(0, 0, 0, 500000))
+det_duration_from_string("PT0.0003S") ===> TEST PASSED (accepted:
duration(0, 0, 0, 300))
+det_duration_from_string("PT0S") ===> TEST PASSED (accepted:
duration(0, 0, 0, 0))
+det_duration_from_string("PT0M") ===> TEST PASSED (accepted:
duration(0, 0, 0, 0))
+det_duration_from_string("PT0H") ===> TEST PASSED (accepted:
duration(0, 0, 0, 0))
+det_duration_from_string("P0M") ===> TEST PASSED (accepted:
duration(0, 0, 0, 0))
+det_duration_from_string("P0D") ===> TEST PASSED (accepted:
duration(0, 0, 0, 0))
+det_duration_from_string("P0Y0M0DT0H0M0S") ===> TEST PASSED
(accepted: duration(0, 0, 0, 0))
+
+=== Testing det_duration_from_string/1 with invalid inputs ===
+
+det_duration_from_string("") ===> TEST PASSED (exception: empty string)
+det_duration_from_string(" ") ===> TEST PASSED (exception: blank string)
+det_duration_from_string("not a duration") ===> TEST PASSED
(exception: not a duration)
+det_duration_from_string("1Y2M3D") ===> TEST PASSED (exception:
missing P prefix)
+det_duration_from_string("P") ===> TEST PASSED (exception: P with no
components)
+det_duration_from_string(" P") ===> TEST PASSED (exception: P with
no components and leading whitespace)
+det_duration_from_string("P ") ===> TEST PASSED (exception: P with
no components and trailing whitespace)
+det_duration_from_string("T1H") ===> TEST PASSED (exception: missing
P prefix, starts with T)
+det_duration_from_string("PT") ===> TEST PASSED (exception: P and T
but no time components)
+det_duration_from_string("-P") ===> TEST PASSED (exception: negative
P with no components)
+det_duration_from_string("p1Y") ===> TEST PASSED (exception: lowercase p)
+det_duration_from_string("P1y") ===> TEST PASSED (exception:
lowercase unit designator)
+det_duration_from_string("P1DT1H30MS") ===> TEST PASSED (exception:
bare S with no digits)
+det_duration_from_string("P1H") ===> TEST PASSED (exception: hours
without T separator)
+det_duration_from_string("P1S") ===> TEST PASSED (exception: seconds
without T separator)
+det_duration_from_string("P1DT1H2M3Y") ===> TEST PASSED (exception:
years after time components)
+det_duration_from_string("P1M1Y") ===> TEST PASSED (exception: years
after months)
+det_duration_from_string("PT1S1H") ===> TEST PASSED (exception: hours
after seconds)
+det_duration_from_string("PT1S1M") ===> TEST PASSED (exception:
minutes after seconds)
+det_duration_from_string("PT1M1H") ===> TEST PASSED (exception: hours
after minutes)
+det_duration_from_string("P1Y1Y") ===> TEST PASSED (exception: years
specified twice)
+det_duration_from_string("P1D1D") ===> TEST PASSED (exception: days
specified twice)
+det_duration_from_string("PT1H1H") ===> TEST PASSED (exception: hours
specified twice)
+det_duration_from_string("P-1Y") ===> TEST PASSED (exception:
negative number after P)
+det_duration_from_string("PT-1H") ===> TEST PASSED (exception:
negative number after T)
+det_duration_from_string("--P1D") ===> TEST PASSED (exception: double
negative prefix)
+det_duration_from_string("-P-1D") ===> TEST PASSED (exception:
negative prefix and negative component)
+det_duration_from_string("P1.5Y") ===> TEST PASSED (exception:
fractional years)
+det_duration_from_string("P1.5M") ===> TEST PASSED (exception:
fractional months)
+det_duration_from_string("P1.5D") ===> TEST PASSED (exception: fractional days)
+det_duration_from_string("PT1.5H") ===> TEST PASSED (exception:
fractional hours)
+det_duration_from_string("PT1.5M") ===> TEST PASSED (exception:
fractional minutes)
+det_duration_from_string("PT1.1234567S") ===> TEST PASSED (exception:
seven fractional digits on seconds)
+det_duration_from_string("PT1.S") ===> TEST PASSED (exception:
decimal point with no fraction digits)
+det_duration_from_string("PT.5S") ===> TEST PASSED (exception: no
integer part before decimal point)
+det_duration_from_string("P1D ") ===> TEST PASSED (exception: trailing space)
+det_duration_from_string(" P1D") ===> TEST PASSED (exception: leading space)
+det_duration_from_string("P1DT1H30M extra") ===> TEST PASSED
(exception: trailing text)
+det_duration_from_string("P1DX") ===> TEST PASSED (exception: invalid
character after component)
+det_duration_from_string("PxY") ===> TEST PASSED (exception: letter
where number expected)
+det_duration_from_string("PT1HxM") ===> TEST PASSED (exception:
letter in time component)
+det_duration_from_string("P1DT") ===> TEST PASSED (exception: T with
no time components)
+
diff --git a/tests/hard_coded/calendar_duration_conv.m
b/tests/hard_coded/calendar_duration_conv.m
new file mode 100644
index 000000000..9507f8b76
--- /dev/null
+++ b/tests/hard_coded/calendar_duration_conv.m
@@ -0,0 +1,272 @@
+%---------------------------------------------------------------------------%
+% vim: ft=mercury ts=4 sw=4 et
+%---------------------------------------------------------------------------%
+%
+% Test predicates and functions from the calendar module that convert
+% durations to and from strings.
+%
+%---------------------------------------------------------------------------%
+
+:- module calendar_duration_conv.
+:- interface.
+
+:- import_module io.
+
+:- pred main(io::di, io::uo) is cc_multi.
+
+%---------------------------------------------------------------------------%
+%---------------------------------------------------------------------------%
+
+:- implementation.
+
+:- import_module calendar.
+:- import_module list.
+:- import_module string.
+
+%---------------------------------------------------------------------------%
+
+main(!IO) :-
+ test_valid_durations(!IO),
+ test_invalid_durations(!IO),
+ test_exception_valid_durations(!IO),
+ test_exception_invalid_durations(!IO).
+
+%---------------------------------------------------------------------------%
+
+:- pred test_valid_durations(io::di, io::uo) is det.
+
+test_valid_durations(!IO) :-
+ io.write_string(
+ "=== Testing duration_from_string/2 with valid inputs ===\n\n",
+ !IO),
+ list.foldl(do_test_valid_duration, valid_durations, !IO),
+ io.nl(!IO).
+
+:- pred do_test_valid_duration(duration_test::in, io::di, io::uo) is det.
+
+do_test_valid_duration(Test, !IO) :-
+ Test = duration_test(Desc, TestString),
+ io.format("duration_from_string(\"%s\") ===> ", [s(TestString)], !IO),
+ ( if duration_from_string(TestString, Duration) then
+ io.format("TEST PASSED (accepted: %s)\n",
+ [s(string(Duration))], !IO)
+ else
+ io.format("TEST FAILED (rejected: %s)\n", [s(Desc)], !IO)
+ ).
+
+%---------------------------------------------------------------------------%
+
+:- pred test_invalid_durations(io::di, io::uo) is det.
+
+test_invalid_durations(!IO) :-
+ io.write_string(
+ "=== Testing duration_from_string/2 with invalid inputs ===\n\n",
+ !IO),
+ list.foldl(do_test_invalid_duration, invalid_durations, !IO),
+ io.nl(!IO).
+
+:- pred do_test_invalid_duration(duration_test::in, io::di, io::uo) is det.
+
+do_test_invalid_duration(Test, !IO) :-
+ Test = duration_test(Desc, TestString),
+ io.format("duration_from_string(\"%s\") ===> ", [s(TestString)], !IO),
+ ( if duration_from_string(TestString, Duration) then
+ DurationString = duration_to_string(Duration),
+ io.format("TEST FAILED (accepted: %s)\n",
+ [s(DurationString)], !IO)
+ else
+ io.format("TEST PASSED (rejected: %s)\n", [s(Desc)], !IO)
+ ).
+
+%---------------------------------------------------------------------------%
+
+:- pred test_exception_valid_durations(io::di, io::uo) is cc_multi.
+
+test_exception_valid_durations(!IO) :-
+ io.write_string(
+ "=== Testing det_duration_from_string/1 with valid inputs
===\n\n", !IO),
+ list.foldl(do_test_exception_valid_duration, valid_durations, !IO),
+ io.nl(!IO).
+
+:- pred do_test_exception_valid_duration(duration_test::in, io::di, io::uo)
+ is cc_multi.
+
+do_test_exception_valid_duration(Test, !IO) :-
+ Test = duration_test(Desc, TestString),
+ io.format("det_duration_from_string(\"%s\") ===> ", [s(TestString)], !IO),
+ ( try []
+ Duration = det_duration_from_string(TestString)
+ then
+ io.format("TEST PASSED (accepted: %s)\n",
+ [s(string(Duration))], !IO)
+ catch_any _ ->
+ io.format("TEST FAILED (exception: %s)\n", [s(Desc)], !IO)
+ ).
+
+%---------------------------------------------------------------------------%
+
+:- pred test_exception_invalid_durations(io::di, io::uo) is cc_multi.
+
+test_exception_invalid_durations(!IO) :-
+ io.write_string(
+ "=== Testing det_duration_from_string/1 with invalid inputs
===\n\n", !IO),
+ list.foldl(do_test_exception_invalid_duration, invalid_durations, !IO),
+ io.nl(!IO).
+
+:- pred do_test_exception_invalid_duration(duration_test::in, io::di, io::uo)
+ is cc_multi.
+
+do_test_exception_invalid_duration(Test, !IO) :-
+ Test = duration_test(Desc, TestString),
+ io.format("det_duration_from_string(\"%s\") ===> ", [s(TestString)], !IO),
+ ( try []
+ Duration = det_duration_from_string(TestString)
+ then
+ io.format("TEST FAILED (accepted: %s)\n",
+ [s(string(Duration))], !IO)
+ catch_any _ ->
+ io.format("TEST PASSED (exception: %s)\n", [s(Desc)], !IO)
+ ).
+
+%---------------------------------------------------------------------------%
+
+:- type duration_test
+ ---> duration_test(
+ description :: string,
+ test_string :: string
+ ).
+
+%---------------------------------------------------------------------------%
+
+:- func valid_durations = list(duration_test).
+
+valid_durations = [
+
+ % Date components only.
+ duration_test("years only", "P1Y"),
+ duration_test("months only", "P1M"),
+ duration_test("days only", "P1D"),
+ duration_test("years and months", "P1Y2M"),
+ duration_test("years and days", "P1Y2D"),
+ duration_test("months and days", "P1M2D"),
+ duration_test("all date components", "P1Y2M3D"),
+
+ % Time components only.
+ duration_test("hours only", "PT1H"),
+ duration_test("minutes only", "PT1M"),
+ duration_test("seconds only", "PT1S"),
+ duration_test("hours and minutes", "PT1H2M"),
+ duration_test("hours and seconds", "PT1H2S"),
+ duration_test("minutes and seconds", "PT1M2S"),
+ duration_test("all time components", "PT1H2M3S"),
+
+ % Mixed date and time components.
+ duration_test("days and hours", "P1DT1H"),
+ duration_test("all components", "P1Y2M3DT4H5M6S"),
+
+ % Negative durations.
+ duration_test("negative days", "-P1D"),
+ duration_test("negative all components", "-P1Y2M3DT4H5M6S"),
+
+ % Large values that normalise.
+ duration_test("large months", "P18M"),
+ duration_test("large hours", "PT25H"),
+ duration_test("large seconds", "PT90S"),
+ duration_test("large all components",
+ "P1Y18M100DT10H15M90.0003S"),
+
+ % Fractional seconds.
+ duration_test("one fractional digit", "PT1.1S"),
+ duration_test("two fractional digits", "PT1.12S"),
+ duration_test("three fractional digits", "PT1.123S"),
+ duration_test("four fractional digits", "PT1.1234S"),
+ duration_test("five fractional digits", "PT1.12345S"),
+ duration_test("six fractional digits", "PT1.123456S"),
+ duration_test("smallest nonzero microsecond", "PT0.000001S"),
+ duration_test("largest microsecond value", "PT0.999999S"),
+ duration_test("fractional seconds only", "PT0.5S"),
+ duration_test("zero seconds with fraction", "PT0.0003S"),
+
+ % Zero durations.
+ duration_test("zero seconds", "PT0S"),
+ duration_test("zero minutes", "PT0M"),
+ duration_test("zero hours", "PT0H"),
+ duration_test("zero months", "P0M"),
+ duration_test("zero days", "P0D"),
+ duration_test("all explicit zeros", "P0Y0M0DT0H0M0S")
+].
+
+%---------------------------------------------------------------------------%
+
+:- func invalid_durations = list(duration_test).
+
+invalid_durations = [
+ % Structural/format errors.
+ duration_test("empty string", ""),
+ duration_test("blank string", " "),
+ duration_test("not a duration", "not a duration"),
+ duration_test("missing P prefix", "1Y2M3D"),
+ duration_test("P with no components", "P"),
+ duration_test("P with no components and leading whitespace", " P"),
+ duration_test("P with no components and trailing whitespace", "P "),
+ duration_test("missing P prefix, starts with T", "T1H"),
+ duration_test("P and T but no time components", "PT"),
+ duration_test("negative P with no components", "-P"),
+ duration_test("lowercase p", "p1Y"),
+ duration_test("lowercase unit designator", "P1y"),
+ duration_test("bare S with no digits", "P1DT1H30MS"),
+
+ % Missing T separator.
+ duration_test("hours without T separator", "P1H"),
+ duration_test("seconds without T separator", "P1S"),
+
+ % Wrong component order.
+ duration_test("years after time components", "P1DT1H2M3Y"),
+ duration_test("years after months", "P1M1Y"),
+ duration_test("hours after seconds", "PT1S1H"),
+ duration_test("minutes after seconds", "PT1S1M"),
+ duration_test("hours after minutes", "PT1M1H"),
+
+ % Duplicate components.
+ duration_test("years specified twice", "P1Y1Y"),
+ duration_test("days specified twice", "P1D1D"),
+ duration_test("hours specified twice", "PT1H1H"),
+
+ % Negative numbers in components.
+ duration_test("negative number after P", "P-1Y"),
+ duration_test("negative number after T", "PT-1H"),
+
+ % Double sign.
+ duration_test("double negative prefix", "--P1D"),
+ duration_test("negative prefix and negative component", "-P-1D"),
+
+ % Fractional parts on non-seconds components.
+ duration_test("fractional years", "P1.5Y"),
+ duration_test("fractional months", "P1.5M"),
+ duration_test("fractional days", "P1.5D"),
+ duration_test("fractional hours", "PT1.5H"),
+ duration_test("fractional minutes", "PT1.5M"),
+
+ % Fractional seconds errors.
+ duration_test("seven fractional digits on seconds",
+ "PT1.1234567S"),
+ duration_test("decimal point with no fraction digits", "PT1.S"),
+ duration_test("no integer part before decimal point", "PT.5S"),
+
+ % Leading, trailing, and embedded whitespace.
+ duration_test("trailing space", "P1D "),
+ duration_test("leading space", " P1D"),
+ duration_test("trailing text", "P1DT1H30M extra"),
+ duration_test("invalid character after component", "P1DX"),
+
+ % Non-digit where digit expected.
+ duration_test("letter where number expected", "PxY"),
+ duration_test("letter in time component", "PT1HxM"),
+
+ % T present but no time components follow.
+ duration_test("T with no time components", "P1DT")
+].
+
+%---------------------------------------------------------------------------%
+:- end_module calendar_duration_conv.
+%---------------------------------------------------------------------------%
More information about the reviews
mailing list