[
{
"name": "an April-March CPD year on 15 March: the first day counts, the day before and a booked course after today do not",
"args": [
[
{
"date": "2026-03-20",
"minutes": 90
},
{
"date": "2025-03-31",
"minutes": 120
},
{
"date": "2025-04-01",
"minutes": 180
},
{
"date": "2025-10-10",
"minutes": 600
},
{
"date": "2026-03-15",
"minutes": 60
}
],
{
"requiredMinutes": 2100,
"periodMonths": 12,
"rolling": false,
"anchor": "2025-04-01"
},
"2026-03-15"
],
"expect": {
"periodStart": "2025-04-01",
"periodEnd": "2026-03-31",
"loggedMinutes": 840,
"entriesCounted": 3,
"requiredMinutes": 2100,
"shortfallMinutes": 1260,
"met": false,
"daysRemaining": 16
}
},
{
"name": "the requirement met",
"args": [
[
{
"date": "2026-03-20",
"minutes": 90
},
{
"date": "2025-03-31",
"minutes": 120
},
{
"date": "2025-04-01",
"minutes": 180
},
{
"date": "2025-10-10",
"minutes": 600
},
{
"date": "2026-03-15",
"minutes": 60
}
],
{
"requiredMinutes": 800,
"periodMonths": 12,
"rolling": false,
"anchor": "2025-04-01"
},
"2026-03-15"
],
"expect": {
"periodStart": "2025-04-01",
"periodEnd": "2026-03-31",
"loggedMinutes": 840,
"entriesCounted": 3,
"requiredMinutes": 800,
"shortfallMinutes": 0,
"met": true,
"daysRemaining": 16
}
},
{
"name": "exactly met",
"args": [
[
{
"date": "2026-03-20",
"minutes": 90
},
{
"date": "2025-03-31",
"minutes": 120
},
{
"date": "2025-04-01",
"minutes": 180
},
{
"date": "2025-10-10",
"minutes": 600
},
{
"date": "2026-03-15",
"minutes": 60
}
],
{
"requiredMinutes": 840,
"periodMonths": 12,
"rolling": false,
"anchor": "2025-04-01"
},
"2026-03-15"
],
"expect": {
"periodStart": "2025-04-01",
"periodEnd": "2026-03-31",
"loggedMinutes": 840,
"entriesCounted": 3,
"requiredMinutes": 840,
"shortfallMinutes": 0,
"met": true,
"daysRemaining": 16
}
},
{
"name": "the new CPD year starts from nothing",
"args": [
[
{
"date": "2026-03-20",
"minutes": 90
},
{
"date": "2025-03-31",
"minutes": 120
},
{
"date": "2025-04-01",
"minutes": 180
},
{
"date": "2025-10-10",
"minutes": 600
},
{
"date": "2026-03-15",
"minutes": 60
}
],
{
"requiredMinutes": 2100,
"periodMonths": 12,
"rolling": false,
"anchor": "2025-04-01"
},
"2026-04-05"
],
"expect": {
"periodStart": "2026-04-01",
"periodEnd": "2027-03-31",
"loggedMinutes": 0,
"entriesCounted": 0,
"requiredMinutes": 2100,
"shortfallMinutes": 2100,
"met": false,
"daysRemaining": 360
}
},
{
"name": "the last day of a fixed year",
"args": [
[
{
"date": "2026-03-20",
"minutes": 90
},
{
"date": "2025-03-31",
"minutes": 120
},
{
"date": "2025-04-01",
"minutes": 180
},
{
"date": "2025-10-10",
"minutes": 600
},
{
"date": "2026-03-15",
"minutes": 60
}
],
{
"requiredMinutes": 2100,
"periodMonths": 12,
"rolling": false,
"anchor": "2025-04-01"
},
"2026-03-31"
],
"expect": {
"periodStart": "2025-04-01",
"periodEnd": "2026-03-31",
"loggedMinutes": 930,
"entriesCounted": 4,
"requiredMinutes": 2100,
"shortfallMinutes": 1170,
"met": false,
"daysRemaining": 0
}
},
{
"name": "an anchor in the future still finds the period, counting back",
"args": [
[
{
"date": "2026-03-20",
"minutes": 90
},
{
"date": "2025-03-31",
"minutes": 120
},
{
"date": "2025-04-01",
"minutes": 180
},
{
"date": "2025-10-10",
"minutes": 600
},
{
"date": "2026-03-15",
"minutes": 60
}
],
{
"requiredMinutes": 2100,
"periodMonths": 12,
"rolling": false,
"anchor": "2027-04-01"
},
"2026-03-15"
],
"expect": {
"periodStart": "2025-04-01",
"periodEnd": "2026-03-31",
"loggedMinutes": 840,
"entriesCounted": 3,
"requiredMinutes": 2100,
"shortfallMinutes": 1260,
"met": false,
"daysRemaining": 16
}
},
{
"name": "a rolling year on 5 April 2026 runs from 6 April 2025",
"args": [
[
{
"date": "2026-03-20",
"minutes": 90
},
{
"date": "2025-03-31",
"minutes": 120
},
{
"date": "2025-04-01",
"minutes": 180
},
{
"date": "2025-10-10",
"minutes": 600
},
{
"date": "2026-03-15",
"minutes": 60
}
],
{
"requiredMinutes": 2100,
"periodMonths": 12,
"rolling": true,
"anchor": null
},
"2026-04-05"
],
"expect": {
"periodStart": "2025-04-06",
"periodEnd": "2026-04-05",
"loggedMinutes": 750,
"entriesCounted": 3,
"requiredMinutes": 2100,
"shortfallMinutes": 1350,
"met": false,
"daysRemaining": 0
}
},
{
"name": "a rolling three years, as for revalidation",
"args": [
[
{
"date": "2026-03-20",
"minutes": 90
},
{
"date": "2025-03-31",
"minutes": 120
},
{
"date": "2025-04-01",
"minutes": 180
},
{
"date": "2025-10-10",
"minutes": 600
},
{
"date": "2026-03-15",
"minutes": 60
}
],
{
"requiredMinutes": 2100,
"periodMonths": 36,
"rolling": true,
"anchor": null
},
"2026-03-15"
],
"expect": {
"periodStart": "2023-03-16",
"periodEnd": "2026-03-15",
"loggedMinutes": 960,
"entriesCounted": 4,
"requiredMinutes": 2100,
"shortfallMinutes": 1140,
"met": false,
"daysRemaining": 0
}
},
{
"name": "a rolling year ending on 29 February starts on 1 March",
"args": [
[
{
"date": "2023-02-28",
"minutes": 60
},
{
"date": "2023-03-01",
"minutes": 30
},
{
"date": "2024-02-29",
"minutes": 45
}
],
{
"requiredMinutes": 100,
"periodMonths": 12,
"rolling": true,
"anchor": null
},
"2024-02-29"
],
"expect": {
"periodStart": "2023-03-01",
"periodEnd": "2024-02-29",
"loggedMinutes": 75,
"entriesCounted": 2,
"requiredMinutes": 100,
"shortfallMinutes": 25,
"met": false,
"daysRemaining": 0
}
},
{
"name": "monthly periods anchored on the 31st: February's starts on the 28th, March's on the 31st again",
"args": [
[
{
"date": "2025-03-30",
"minutes": 60
},
{
"date": "2025-03-31",
"minutes": 30
}
],
{
"requiredMinutes": 120,
"periodMonths": 1,
"rolling": false,
"anchor": "2025-01-31"
},
"2025-03-15"
],
"expect": {
"periodStart": "2025-02-28",
"periodEnd": "2025-03-30",
"loggedMinutes": 0,
"entriesCounted": 0,
"requiredMinutes": 120,
"shortfallMinutes": 120,
"met": false,
"daysRemaining": 15
}
},
{
"name": "an empty log",
"args": [
[],
{
"requiredMinutes": 2100,
"periodMonths": 12,
"rolling": false,
"anchor": "2025-04-01"
},
"2025-06-01"
],
"expect": {
"periodStart": "2025-04-01",
"periodEnd": "2026-03-31",
"loggedMinutes": 0,
"entriesCounted": 0,
"requiredMinutes": 2100,
"shortfallMinutes": 2100,
"met": false,
"daysRemaining": 303
}
},
{
"name": "no requirement is always met",
"args": [
[],
{
"requiredMinutes": 0,
"periodMonths": 12,
"rolling": true,
"anchor": null
},
"2025-06-01"
],
"expect": {
"periodStart": "2024-06-02",
"periodEnd": "2025-06-01",
"loggedMinutes": 0,
"entriesCounted": 0,
"requiredMinutes": 0,
"shortfallMinutes": 0,
"met": true,
"daysRemaining": 0
}
},
{
"name": "a zero-minute entry counts as an entry and adds nothing",
"args": [
[
{
"date": "2025-05-01",
"minutes": 0
}
],
{
"requiredMinutes": 2100,
"periodMonths": 12,
"rolling": false,
"anchor": "2025-04-01"
},
"2025-06-01"
],
"expect": {
"periodStart": "2025-04-01",
"periodEnd": "2026-03-31",
"loggedMinutes": 0,
"entriesCounted": 1,
"requiredMinutes": 2100,
"shortfallMinutes": 2100,
"met": false,
"daysRemaining": 303
}
},
{
"name": "negative minutes are an error",
"args": [
[
{
"date": "2025-05-01",
"minutes": -30
}
],
{
"requiredMinutes": 2100,
"periodMonths": 12,
"rolling": false,
"anchor": "2025-04-01"
},
"2025-06-01"
],
"expectError": "minutes must be a whole number of 0 or more, received -30 on 2025-05-01"
},
{
"name": "fractional minutes are an error",
"args": [
[
{
"date": "2025-05-01",
"minutes": 1.5
}
],
{
"requiredMinutes": 2100,
"periodMonths": 12,
"rolling": false,
"anchor": "2025-04-01"
},
"2025-06-01"
],
"expectError": "minutes must be a whole number of 0 or more, received 1.5"
},
{
"name": "a negative requirement is an error",
"args": [
[],
{
"requiredMinutes": -1,
"periodMonths": 12,
"rolling": true,
"anchor": null
},
"2025-06-01"
],
"expectError": "requiredMinutes must be a whole number of 0 or more, received -1"
},
{
"name": "a zero-month period is an error",
"args": [
[],
{
"requiredMinutes": 60,
"periodMonths": 0,
"rolling": true,
"anchor": null
},
"2025-06-01"
],
"expectError": "periodMonths must be a whole number of 1 or more, received 0"
},
{
"name": "a fixed period without an anchor is an error",
"args": [
[],
{
"requiredMinutes": 60,
"periodMonths": 12,
"rolling": false,
"anchor": null
},
"2025-06-01"
],
"expectError": "a fixed period needs an anchor date"
},
{
"name": "a rolling period with an anchor is an error",
"args": [
[],
{
"requiredMinutes": 60,
"periodMonths": 12,
"rolling": true,
"anchor": "2025-04-01"
},
"2025-06-01"
],
"expectError": "a rolling period takes no anchor"
},
{
"name": "an impossible entry date is an error",
"args": [
[
{
"date": "2025-02-30",
"minutes": 60
}
],
{
"requiredMinutes": 2100,
"periodMonths": 12,
"rolling": false,
"anchor": "2025-04-01"
},
"2025-06-01"
],
"expectError": "\"2025-02-30\" is not a real calendar date"
}
]