feat: add parseISODuration for ISO 8601 duration parsing

This commit is contained in:
2026-05-19 22:46:57 -04:00
parent 9586afcb46
commit 1addddd8fc
2 changed files with 57 additions and 2 deletions
+43 -2
View File
@@ -12,6 +12,7 @@ module Roux.SchemaOrg (
parseISODuration,
) where
import Control.Monad (guard)
import Data.Aeson (FromJSON (..), (.!=), (.:), (.:?))
import qualified Data.Aeson as A
import qualified Data.Aeson.KeyMap as KM
@@ -219,6 +220,46 @@ parseSchemaOrgRecipe val = do
schemaOrgToCooklang :: SchemaOrgRecipe -> Either String Recipe
schemaOrgToCooklang _ = Left "Not yet implemented"
-- | Parse an ISO 8601 duration string. (Stub — will be implemented in Task 2)
{- | Parse an ISO 8601 duration string into a Cooklang Duration.
Supports formats like PT20M, PT1H30M, P1DT2H, P1D.
-}
parseISODuration :: Text -> Maybe Duration
parseISODuration _ = Nothing
parseISODuration t
| not ("P" `T.isPrefixOf` t) = Nothing
| otherwise = do
let rest = T.drop 1 t -- drop the P
(datePart, afterDate) = T.break (== 'T') rest
days = parseDateDays datePart
timePart = T.drop 1 afterDate -- drop the T
(hours, minutes, _seconds) = parseTimeComponent timePart
totalMinutes = days * 24 * 60 + hours * 60 + minutes
guard (totalMinutes > 0)
Just (Duration (toRational totalMinutes) (Just "minutes"))
where
parseDateDays :: Text -> Int
parseDateDays dp =
let (numStr, after) = T.break (== 'D') dp
in case T.uncons after of
Just ('D', _) -> fromMaybe 0 (readMaybe (T.unpack numStr))
_ -> 0
parseTimeComponent :: Text -> (Int, Int, Int)
parseTimeComponent tc =
let (hours, rest1) = parseUnit "H" tc
(minutes, rest2) = parseUnit "M" rest1
(seconds, _rest3) = parseUnit "S" rest2
in (hours, minutes, seconds)
parseUnit :: Text -> Text -> (Int, Text)
parseUnit unit tc =
let (numStr, after) = T.break (\c -> c == 'H' || c == 'M' || c == 'S') tc
in case T.uncons after of
Just (c, rest)
| T.singleton c == unit ->
(fromMaybe 0 (readMaybe (T.unpack numStr)), rest)
_ -> (0, tc)
readMaybe :: (Read a) => String -> Maybe a
readMaybe s = case reads s of
[(x, "")] -> Just x
_ -> Nothing
+14
View File
@@ -1,6 +1,7 @@
module Roux.SchemaOrgSpec (spec) where
import qualified Data.Aeson as A
import Data.CookLang (Duration (..))
import Data.Maybe (fromJust, isJust)
import Data.Text (Text)
import qualified Data.Text as T
@@ -49,3 +50,16 @@ spec = describe "SchemaOrg" $ do
case recipe of
Nothing -> expectationFailure "expected Just SchemaOrgRecipe, got Nothing"
Just r -> soName r `shouldBe` "Carrot Risotto With Chile Crisp"
describe "parseISODuration" $ do
let chk :: Text -> Maybe Duration -> IO ()
chk input expected = parseISODuration input `shouldBe` expected
it "PT20M -> 20 minutes" $ chk "PT20M" (Just (Duration 20 (Just "minutes")))
it "PT1H -> 60 minutes" $ chk "PT1H" (Just (Duration 60 (Just "minutes")))
it "PT1H30M -> 90 minutes" $ chk "PT1H30M" (Just (Duration 90 (Just "minutes")))
it "P1D -> 1440 minutes" $ chk "P1D" (Just (Duration 1440 (Just "minutes")))
it "P1DT2H -> 1560 minutes" $ chk "P1DT2H" (Just (Duration 1560 (Just "minutes")))
it "PT30M -> 30 minutes" $ chk "PT30M" (Just (Duration 30 (Just "minutes")))
it "empty string -> Nothing" $ chk "" Nothing
it "human-readable -> Nothing" $ chk "20 minutes" Nothing