Functional Weave
Code in Python

education.cpd-hours@1.0.0

vectors.json

10,617 bytes · view raw

[
 {
  "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"
 }
]