Skip to content

Commit b77a64e

Browse files
committed
normalize durations prior to signed statement comparison
1 parent b897a03 commit b77a64e

File tree

2 files changed

+76
-1
lines changed

2 files changed

+76
-1
lines changed

src/main/com/yetanalytics/lrs/xapi/statements.cljc

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
[com.yetanalytics.lrs.xapi.statements.timestamp :as timestamp]
99
[com.yetanalytics.lrs.xapi.agents :as ag]
1010
[com.yetanalytics.lrs.xapi.activities :as ac]
11+
[com.yetanalytics.lrs.xapi.statements.duration :as dur]
1112
[xapi-schema.spec :as xs]
1213
[#?(:clj clojure.data.priority-map
1314
:cljs tailrecursion.priority-map) :as pm]
@@ -499,14 +500,28 @@
499500
(s/conformer (partial w/postwalk (fn [x] (if (set? x) (vec x) x))))
500501
::xs/statement))
501502

503+
(defn normalize-result-duration
504+
"If a Statement has a result duration, normalize it to the xAPI
505+
`xs/duration` format."
506+
[{:strs [result] :as s}]
507+
(if-let [duration (get-in result ["duration"])]
508+
(let [norm-duration (dur/normalize-duration duration)]
509+
(assoc s "result" (assoc result "duration" norm-duration)))
510+
s))
511+
512+
(s/fdef normalize-result-duration
513+
:args (s/cat :statement ::xs/statement)
514+
:ret ::xs/statement)
515+
502516
;; TODO: A bunch of other functions have args in the style of `& ss`.
503517
;; Check whether they work for zero args.
504518
(defn statements-immut-equal?
505519
"Return `true` if the Statements `ss` are equal after all Statement
506520
Immutability properties (except case insensitivity)."
507521
[& ss]
508522
(if (not-empty ss)
509-
(apply = (map dissoc-statement-properties ss))
523+
(apply = (map (comp normalize-result-duration
524+
dissoc-statement-properties) ss))
510525
;; Vacuously true
511526
true))
512527

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
(ns com.yetanalytics.lrs.xapi.statements.duration
2+
(:require [clojure.spec.alpha :as s]
3+
[clojure.string :as str]
4+
[xapi-schema.spec :as xs]
5+
#?@(:cljs [[goog.string :as gstring :refer [format]]
6+
[goog.string.format]])))
7+
8+
(s/fdef normalize-duration
9+
:args (s/cat :duration ::xs/duration)
10+
:ret ::xs/duration)
11+
12+
(defn normalize-duration
13+
"Normalize an xAPI duration string to 0.01 second precision"
14+
[duration]
15+
(let [duration-str (subs duration 1) ;; Remove 'P'
16+
[date-part time-part] (if (re-find #"T" duration-str)
17+
(str/split duration-str #"T" 2)
18+
[duration-str nil])
19+
;; Parse date components
20+
years (when-let [m (re-find #"(\d+(?:\.\d+)?)Y" date-part)] (parse-double (second m)))
21+
months (when-let [m (re-find #"(\d+(?:\.\d+)?)M" date-part)] (parse-double (second m)))
22+
days (when-let [m (re-find #"(\d+(?:\.\d+)?)D" date-part)] (parse-double (second m)))
23+
;; Parse time components
24+
hours (when time-part (when-let [m (re-find #"(\d+(?:\.\d+)?)H" time-part)] (parse-double (second m))))
25+
minutes (when time-part (when-let [m (re-find #"(\d+(?:\.\d+)?)M" time-part)] (parse-double (second m))))
26+
seconds (when time-part (when-let [m (re-find #"(\d+(?:\.\d+)?)S" time-part)] (parse-double (second m))))
27+
28+
;; Helper function to format numbers with proper precision
29+
format-num (fn [n unit]
30+
(let [;; Truncate to 0.01 precision (round down)
31+
truncated (/ (Math/floor (* n 100)) 100.0)]
32+
(cond
33+
;; For seconds, handle special formatting
34+
(= unit "S")
35+
(cond
36+
;; Special case: when truncated to 0.00, check if original was > 0
37+
(and (= truncated 0.0) (> n 0)) "0.00"
38+
(= truncated 0.0) "0"
39+
(= truncated (Math/floor truncated)) (str (int truncated))
40+
:else (let [formatted (format "%.2f" truncated)]
41+
;; Remove trailing zeros after decimal but keep at least one decimal place if needed
42+
(str/replace formatted #"\.?0+$" "")))
43+
44+
;; For other units, preserve integer format when possible
45+
(= truncated (Math/floor truncated)) (str (int truncated))
46+
:else (format "%.2f" truncated))))
47+
48+
;; Build the result preserving original structure when no changes needed
49+
date-str (str (when years (str (format-num years "Y") "Y"))
50+
(when months (str (format-num months "M") "M"))
51+
(when days (str (format-num days "D") "D")))
52+
53+
time-str (when (or hours minutes seconds)
54+
(str "T"
55+
(when hours (str (format-num hours "H") "H"))
56+
(when minutes (str (format-num minutes "M") "M"))
57+
(when seconds (str (format-num seconds "S") "S"))))]
58+
59+
(str "P" date-str time-str)))
60+

0 commit comments

Comments
 (0)