Phase 5: Compound quantities, trailing-unit format, and edge cases
- Add splitAmountUnit to parse quantities with unit attached without
% separator (e.g., @water{1,1/2cups}, @gelatine{3tsp})
- Add isNumericChar helper for detecting numeric quantity characters
- Add compound quantity test (1,1/2 cups = 3/2)
- Add no-separator unit test (3tsp)
- All 26 tests passing, all 4 example files parse correctly
This commit is contained in:
+24
-6
@@ -9,6 +9,7 @@ module Roux.Parser (
|
||||
) where
|
||||
|
||||
import Control.Monad (join)
|
||||
import Data.Char (isDigit)
|
||||
import Data.List.NonEmpty (NonEmpty ((:|)))
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
@@ -200,19 +201,36 @@ pQuantityBody = do
|
||||
Just q -> return (Just q)
|
||||
Nothing -> P.parserFail ("invalid quantity: " <> content)
|
||||
|
||||
-- | Parse a quantity text like @2@, @1%kg@, @=1%tsp@, @1\/2%tbsp@.
|
||||
{- | Parse a quantity text like @2@, @1%kg@, @=1%tsp@, @1\/2%tbsp@, or
|
||||
@1,1\/2cups@ (unit without @%@ separator).
|
||||
-}
|
||||
parseQuantityText :: Text -> Maybe Quantity
|
||||
parseQuantityText t =
|
||||
let (fixed, rest) = case T.uncons t of
|
||||
Just ('=', rest') -> (True, T.strip rest')
|
||||
_ -> (False, T.strip t)
|
||||
(amountStr, unit) = case T.break (== '%') rest of
|
||||
(a, u) | T.null u -> (a, Nothing)
|
||||
(a, u) -> (a, Just (T.drop 1 u))
|
||||
in case parseRational (T.strip amountStr) of
|
||||
Just a -> Just (Quantity a unit fixed)
|
||||
(amountPart, unitPart) = case T.break (== '%') rest of
|
||||
(a, u)
|
||||
| T.null u -> splitAmountUnit a
|
||||
| otherwise -> (a, Just (T.strip (T.drop 1 u)))
|
||||
in case parseRational (T.strip amountPart) of
|
||||
Just a -> Just (Quantity a unitPart fixed)
|
||||
Nothing -> Nothing
|
||||
|
||||
{- | Split text into amount and optional trailing unit (no @%@ separator).
|
||||
Handles @"1,1\/2cups"@ → @("1,1\/2", Just "cups")@.
|
||||
-}
|
||||
splitAmountUnit :: Text -> (Text, Maybe Text)
|
||||
splitAmountUnit t =
|
||||
let (numeric, trailing) = T.span isNumericChar t
|
||||
in if T.null trailing
|
||||
then (numeric, Nothing)
|
||||
else (numeric, Just (T.strip trailing))
|
||||
|
||||
-- | Characters that can appear in a quantity number.
|
||||
isNumericChar :: Char -> Bool
|
||||
isNumericChar c = isDigit c || c `elem` ['.', ',', '/', ' ', '-', '+']
|
||||
|
||||
-- | Parse a rational number from text. Handles @1\/2@, @3@, @1,1\/2@.
|
||||
parseRational :: Text -> Maybe Rational
|
||||
parseRational t =
|
||||
|
||||
@@ -2,6 +2,7 @@ module Roux.ParserSpec (spec) where
|
||||
|
||||
import Data.List.NonEmpty (NonEmpty ((:|)))
|
||||
import qualified Data.List.NonEmpty as NE
|
||||
import Data.Ratio ((%))
|
||||
import qualified Data.Text.IO as TIO
|
||||
import Test.Hspec (Spec, describe, expectationFailure, it, shouldBe)
|
||||
|
||||
@@ -371,6 +372,27 @@ spec = do
|
||||
expectationFailure
|
||||
("expected SecStep as third body item, got body=" <> show body)
|
||||
|
||||
describe "Quantities" $ do
|
||||
it "parses compound quantity (1,1/2) with unit attached" $ do
|
||||
let input = "@water{1,1/2cups}"
|
||||
case parseCookFile input of
|
||||
Left err -> expectationFailure ("parse failed: " <> err)
|
||||
Right recipe -> do
|
||||
let items = stepItems recipe
|
||||
items
|
||||
`shouldBe` [ StepIngredient (Ingredient "water" (Just (Quantity (3 % 2) (Just "cups") False)) Nothing)
|
||||
]
|
||||
|
||||
it "parses quantity with trailing unit (no %)" $ do
|
||||
let input = "@gelatine{3tsp}"
|
||||
case parseCookFile input of
|
||||
Left err -> expectationFailure ("parse failed: " <> err)
|
||||
Right recipe -> do
|
||||
let items = stepItems recipe
|
||||
items
|
||||
`shouldBe` [ StepIngredient (Ingredient "gelatine" (Just (Quantity 3 (Just "tsp") False)) Nothing)
|
||||
]
|
||||
|
||||
describe "Recipe references" $ do
|
||||
it "parses a recipe reference" $ do
|
||||
let input = "Pour over with @./sauces/Hollandaise{150%g}."
|
||||
|
||||
Reference in New Issue
Block a user