|
| 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