diff --git a/src/Database/CrossLinking.hs b/src/Database/CrossLinking.hs index f3523ab..30b95fd 100644 --- a/src/Database/CrossLinking.hs +++ b/src/Database/CrossLinking.hs @@ -398,16 +398,15 @@ findSupplierInIndexedDBs LinkingContext{..} productName location unit = if T.null location then extractBracketedLocation productName else location - in if null allCandidates - then CrossDBNotLinked NoNameMatch - else + in case allCandidates of + [] -> CrossDBNotLinked NoNameMatch + ((_, firstSe) : _) -> -- Check unit compatibility first let unitCompatible = filter (\(_, se) -> unitsAreCompatible lcUnitConfig unit (seUnit se)) allCandidates in if null unitCompatible then -- All candidates failed unit check — report the first supplier's unit - let (_, firstSe) = head allCandidates - in CrossDBNotLinked (UnitIncompatible unit (seUnit firstSe)) + CrossDBNotLinked (UnitIncompatible unit (seUnit firstSe)) else -- Score by effective location let scoredCandidates = map (scoreEntry effectiveLocation) unitCompatible @@ -478,9 +477,7 @@ findSupplierAcrossDatabases :: -- | Unit of the exchange Text -> CrossDBLinkResult -findSupplierAcrossDatabases ctx productName location unit = - -- Just delegate to the indexed version - findSupplierInIndexedDBs ctx productName location unit +findSupplierAcrossDatabases = findSupplierInIndexedDBs {- | Match product names (simplified - just for scoring display) Actual matching is done via index lookup diff --git a/src/Database/Loader.hs b/src/Database/Loader.hs index fc7367c..f8dce3c 100644 --- a/src/Database/Loader.hs +++ b/src/Database/Loader.hs @@ -81,7 +81,8 @@ import Data.Bits (xor) import qualified Data.ByteString as BS import Data.Char (toLower) import Data.Either (partitionEithers) -import Data.List (group, sort, sortBy, sortOn, unzip7) +import Data.List (sort, sortBy, sortOn, unzip7) +import qualified Data.List.NonEmpty as NE import qualified Data.Map as M import Data.Maybe (fromMaybe) import Data.Ord (Down (..)) @@ -277,7 +278,7 @@ reportUnlinkedSummary summary printf " ... and %d more activities" remainingCount where sortOn' f = sortBy (\a b -> compare (f a) (f b)) - nub = map head . group . sort + nub = map NE.head . NE.group . sort -- | Normalize text for matching: lowercase, strip whitespace, normalize Unicode normalizeText :: T.Text -> T.Text diff --git a/src/Database/Manager.hs b/src/Database/Manager.hs index 16bc0e0..56b375b 100644 --- a/src/Database/Manager.hs +++ b/src/Database/Manager.hs @@ -102,7 +102,8 @@ import Data.Aeson (FromJSON (..), ToJSON (..), (.:), (.:?), (.=)) import qualified Data.Aeson as A import Data.Bifunctor (first) import Data.Char (toLower) -import Data.List (isPrefixOf, nub, sortOn) +import Data.Either (lefts, rights) +import Data.List (isPrefixOf, nub, sortOn, unsnoc) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Maybe (catMaybes, fromMaybe, isJust) @@ -1144,13 +1145,13 @@ detectDirectoryFormat path = do files <- listDirectory path let extensions = map (map toLower . takeExtension) files -- Check for different formats (in order of preference) - if elem ".spold" extensions + if ".spold" `elem` extensions then return FormatSpold else - if elem ".csv" extensions + if ".csv" `elem` extensions then return FormatCSV else - if elem ".xml" extensions + if ".xml" `elem` extensions then return FormatXML else return FormatUnknown else return FormatUnknown @@ -2345,10 +2346,10 @@ loadMethodCollectionFromConfig mc = do else return Nothing let (xmlErrs, xmlMethods) = partitionEithers xmlResults (csvErrs, csvOks) = partitionEithers csvParsed - (jsonErrs, jsonMethods) = partitionEithers [r | Just r <- jsonResults] + (jsonErrs, jsonMethods) = partitionEithers (catMaybes jsonResults) -- Merge: SimaPro CSVs are MethodCollections, tabular CSVs are [Method] - spCollections = [sp | Left sp <- csvOks] - tabularMethods = concat [ms | Right ms <- csvOks] + spCollections = lefts csvOks + tabularMethods = concat (rights csvOks) allMethods = xmlMethods ++ tabularMethods @@ -2359,21 +2360,22 @@ loadMethodCollectionFromConfig mc = do allNWSets = concatMap mcNormWeightSets spCollections collection = MethodCollection allMethods allDamageCats allNWSets [] errs = xmlErrs ++ csvErrs ++ jsonErrs - if null allMethods && not (null errs) - then return $ Left $ "All method files failed to parse: " <> T.pack (head errs) - else do + case (null allMethods, errs) of + (True, firstErr : _) -> + return $ Left $ "All method files failed to parse: " <> T.pack firstErr + _ -> do let xmlOk = length xmlMethods csvOk = length csvOks jsonOk = length jsonMethods reportProgress Info $ " Parsed " <> show xmlOk <> " XML, " <> show csvOk <> " CSV, " <> show jsonOk <> " JSON file(s)" - when (not (null allDamageCats)) $ + unless (null allDamageCats) $ reportProgress Info $ " " <> show (length allDamageCats) <> " damage categories, " <> show (length allNWSets) <> " normalization-weighting set(s)" - when (not (null errs)) $ + unless (null errs) $ reportProgress Warning $ " " <> show (length errs) <> " method file(s) failed to parse" return $ Right (collection, flowInfo) @@ -2897,13 +2899,15 @@ parseGeographiesCSV path = do | otherwise = case T.splitOn "," line of [] -> [] [_] -> [] - parts -> - let code = T.strip (head parts) - parentsStr = T.strip (last parts) - displayRaw = T.intercalate "," (init (tail parts)) - displayName = let d = T.strip displayRaw in if T.null d then code else d - parents = if T.null parentsStr then [] else T.splitOn "|" parentsStr - in [(code, (displayName, parents))] + parts@(codeRaw : _) -> case unsnoc parts of + Nothing -> [] -- unreachable: parts is non-empty by pattern + Just (initPart, parentsRaw) -> + let code = T.strip codeRaw + parentsStr = T.strip parentsRaw + displayRaw = T.intercalate "," (drop 1 initPart) + displayName = let d = T.strip displayRaw in if T.null d then code else d + parents = if T.null parentsStr then [] else T.splitOn "|" parentsStr + in [(code, (displayName, parents))] -- | Load CSV file content from path. loadRefDataCSV :: FilePath -> IO (Either Text BL.ByteString) diff --git a/src/Database/Upload.hs b/src/Database/Upload.hs index c3c3791..fea911d 100644 --- a/src/Database/Upload.hs +++ b/src/Database/Upload.hs @@ -429,8 +429,9 @@ findDataDirectory dir = do where pickByFileCount dirs = do counts <- mapM (\d -> (,) d <$> countDataFilesIn d) dirs - let sorted = sortOn (Down . snd) counts - return $ fst (head sorted) + case sortOn (Down . snd) counts of + (best : _) -> return (fst best) + [] -> return dir -- Recursively find the first directory containing a processes/ subdirectory findILCDRoot d = do @@ -451,7 +452,7 @@ findDataDirectory dir = do -- | Find all directories containing recognized data files under a root. findAllDataDirectories :: FilePath -> IO [FilePath] -findAllDataDirectories root = go root +findAllDataDirectories = go where go dir = do result <- try @SomeException $ do @@ -489,7 +490,7 @@ anyDataFilesIn d = do else do let fullPaths = map (d ) fs extensions = map (map toLower . takeExtension) fs - if any (== ".spold") extensions + if ".spold" `elem` extensions then return True else do let xmlFiles = [p | p <- fullPaths, map toLower (takeExtension p) == ".xml"] @@ -511,11 +512,13 @@ findMethodDirectory dir = do [one] -> return one many -> do counts <- mapM (\d -> (,) d <$> countMethodFilesIn d) many - return $ fst $ head $ sortOn (Down . snd) counts + case sortOn (Down . snd) counts of + (best : _) -> return (fst best) + [] -> return dir -- | Find all directories containing ILCD method XML files under a root. findAllMethodDirectories :: FilePath -> IO [FilePath] -findAllMethodDirectories root = go root +findAllMethodDirectories = go where go dir = do hasMethod <- anyMethodFilesIn dir @@ -612,9 +615,9 @@ detectDatabaseFormat path = do else do fs <- listDirectoryRecursive path let extensions = map (map toLower . takeExtension) fs - let hasSpold = any (== ".spold") extensions - hasXml = any (== ".xml") extensions - hasCsv = any (== ".csv") extensions + let hasSpold = ".spold" `elem` extensions + hasXml = ".xml" `elem` extensions + hasCsv = ".csv" `elem` extensions if hasSpold then return EcoSpold2 else diff --git a/src/EcoSpold/Parser1.hs b/src/EcoSpold/Parser1.hs index 3a4642c..9382403 100644 --- a/src/EcoSpold/Parser1.hs +++ b/src/EcoSpold/Parser1.hs @@ -1,5 +1,4 @@ {-# LANGUAGE BangPatterns #-} -{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -Wno-name-shadowing #-} @@ -27,7 +26,9 @@ module EcoSpold.Parser1 ( import Control.Monad (forM_) import qualified Data.ByteString as BS +import Data.Either (lefts, rights) import qualified Data.Map as M +import Data.Maybe (fromMaybe) import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Encoding as TE @@ -244,19 +245,19 @@ parseWithXeno xmlContent = in case psContext state of InInputGroup edata -> -- Restore parent exchange context with updated inputGroup - state{psContext = InExchange edata{exInputGroup = txt}, psPath = tail (psPath state), psTextAccum = []} + state{psContext = InExchange edata{exInputGroup = txt}, psPath = drop 1 (psPath state), psTextAccum = []} InExchange edata -> - state{psContext = InExchange edata{exInputGroup = txt}, psPath = tail (psPath state), psTextAccum = []} - _ -> state{psPath = tail (psPath state), psTextAccum = []} + state{psContext = InExchange edata{exInputGroup = txt}, psPath = drop 1 (psPath state), psTextAccum = []} + _ -> state{psPath = drop 1 (psPath state), psTextAccum = []} | isElement tagName "outputGroup" = let txt = T.strip $ T.concat $ reverse $ map bsToText (psTextAccum state) in case psContext state of InOutputGroup edata -> -- Restore parent exchange context with updated outputGroup - state{psContext = InExchange edata{exOutputGroup = txt}, psPath = tail (psPath state), psTextAccum = []} + state{psContext = InExchange edata{exOutputGroup = txt}, psPath = drop 1 (psPath state), psTextAccum = []} InExchange edata -> - state{psContext = InExchange edata{exOutputGroup = txt}, psPath = tail (psPath state), psTextAccum = []} - _ -> state{psPath = tail (psPath state), psTextAccum = []} + state{psContext = InExchange edata{exOutputGroup = txt}, psPath = drop 1 (psPath state), psTextAccum = []} + _ -> state{psPath = drop 1 (psPath state), psTextAccum = []} | isElement tagName "exchange" = case psContext state of InExchange edata -> @@ -272,14 +273,14 @@ parseWithXeno xmlContent = , psUnits = unit : psUnits state , psSupplierLinks = supplierLinks , psContext = Other - , psPath = tail (psPath state) + , psPath = drop 1 (psPath state) , psTextAccum = [] } - _ -> state{psPath = tail (psPath state)} + _ -> state{psPath = drop 1 (psPath state)} | isElement tagName "referenceFunction" = - state{psContext = Other, psPath = tail (psPath state), psTextAccum = []} + state{psContext = Other, psPath = drop 1 (psPath state), psTextAccum = []} | isElement tagName "geography" = - state{psContext = Other, psPath = tail (psPath state), psTextAccum = []} + state{psContext = Other, psPath = drop 1 (psPath state), psTextAccum = []} -- Handle dataset close tag: accumulate completed activity for multi-dataset files | isElement tagName "dataset" = let !result = buildResult state @@ -302,12 +303,12 @@ parseWithXeno xmlContent = , psTextAccum = [] , psSupplierLinks = M.empty } - in resetState{psPath = tail (psPath state)} + in resetState{psPath = drop 1 (psPath state)} | otherwise = - state{psPath = if null (psPath state) then [] else tail (psPath state)} + state{psPath = drop 1 (psPath state)} -- CDATA handler - cdata state content = text state content + cdata = text -- Build exchange, flow, and unit from exchange data -- activityLoc is the activity's location for fallback @@ -342,9 +343,7 @@ parseWithXeno xmlContent = if T.null (exLocation edata) then if isBiosphere - then case activityLoc of - Just loc -> loc - Nothing -> "" + then fromMaybe "" activityLoc else "" -- Technosphere: leave empty for name-only lookup in Loader else exLocation edata @@ -385,15 +384,9 @@ parseWithXeno xmlContent = -- Build final result buildResult :: ParseState -> Either String (Activity, [Flow], [Unit], Int, M.Map UUID Int) buildResult st = - let name = case psActivityName st of - Just n -> n - Nothing -> "Unknown Activity" - location = case psLocation st of - Just loc -> loc - Nothing -> "GLO" - refUnit = case psRefUnit st of - Just u -> u - Nothing -> "UNKNOWN_UNIT" + let name = fromMaybe "Unknown Activity" (psActivityName st) + location = fromMaybe "GLO" (psLocation st) + refUnit = fromMaybe "UNKNOWN_UNIT" (psRefUnit st) description = reverse (psDescription st) classifications = M.fromList $ @@ -432,7 +425,7 @@ hasReferenceProduct act = any exchangeIsReference (exchanges act) -- | Remove production exchanges with zero amounts removeZeroAmountCoproducts :: [Exchange] -> [Exchange] -removeZeroAmountCoproducts exs = filter keepExchange exs +removeZeroAmountCoproducts = filter keepExchange where keepExchange TechnosphereExchange{techIsInput = False, techIsReference = True} = True keepExchange TechnosphereExchange{techIsInput = False, techIsReference = False, techAmount = amount} = amount /= 0.0 @@ -577,18 +570,18 @@ parseAllWithXeno xmlContent = let txt = T.strip $ T.concat $ reverse $ map bsToText (psTextAccum state) in case psContext state of InInputGroup edata -> - state{psContext = InExchange edata{exInputGroup = txt}, psPath = tail (psPath state), psTextAccum = []} + state{psContext = InExchange edata{exInputGroup = txt}, psPath = drop 1 (psPath state), psTextAccum = []} InExchange edata -> - state{psContext = InExchange edata{exInputGroup = txt}, psPath = tail (psPath state), psTextAccum = []} - _ -> state{psPath = tail (psPath state), psTextAccum = []} + state{psContext = InExchange edata{exInputGroup = txt}, psPath = drop 1 (psPath state), psTextAccum = []} + _ -> state{psPath = drop 1 (psPath state), psTextAccum = []} | isElement tagName "outputGroup" = let txt = T.strip $ T.concat $ reverse $ map bsToText (psTextAccum state) in case psContext state of InOutputGroup edata -> - state{psContext = InExchange edata{exOutputGroup = txt}, psPath = tail (psPath state), psTextAccum = []} + state{psContext = InExchange edata{exOutputGroup = txt}, psPath = drop 1 (psPath state), psTextAccum = []} InExchange edata -> - state{psContext = InExchange edata{exOutputGroup = txt}, psPath = tail (psPath state), psTextAccum = []} - _ -> state{psPath = tail (psPath state), psTextAccum = []} + state{psContext = InExchange edata{exOutputGroup = txt}, psPath = drop 1 (psPath state), psTextAccum = []} + _ -> state{psPath = drop 1 (psPath state), psTextAccum = []} | isElement tagName "exchange" = case psContext state of InExchange edata -> @@ -604,14 +597,14 @@ parseAllWithXeno xmlContent = , psUnits = unit : psUnits state , psSupplierLinks = supplierLinks , psContext = Other - , psPath = tail (psPath state) + , psPath = drop 1 (psPath state) , psTextAccum = [] } - _ -> state{psPath = tail (psPath state)} + _ -> state{psPath = drop 1 (psPath state)} | isElement tagName "referenceFunction" = - state{psContext = Other, psPath = tail (psPath state), psTextAccum = []} + state{psContext = Other, psPath = drop 1 (psPath state), psTextAccum = []} | isElement tagName "geography" = - state{psContext = Other, psPath = tail (psPath state), psTextAccum = []} + state{psContext = Other, psPath = drop 1 (psPath state), psTextAccum = []} -- Handle dataset close tag: accumulate completed activity | isElement tagName "dataset" = let !result = buildResultForAll state @@ -632,12 +625,12 @@ parseAllWithXeno xmlContent = , psTextAccum = [] , psSupplierLinks = M.empty } - in resetState{psPath = tail (psPath state)} + in resetState{psPath = drop 1 (psPath state)} | otherwise = - state{psPath = if null (psPath state) then [] else tail (psPath state)} + state{psPath = drop 1 (psPath state)} -- CDATA handler - cdata state content = text state content + cdata = text -- Build exchange, flow, and unit from exchange data (same logic as parseWithXeno) buildExchangeForAll :: Int -> Maybe Text -> ExchangeData -> (Exchange, Flow, Unit) @@ -658,9 +651,7 @@ parseAllWithXeno xmlContent = if T.null (exLocation edata) then if isBiosphere - then case activityLoc of - Just loc -> loc - Nothing -> "" + then fromMaybe "" activityLoc else "" else exLocation edata exchange = @@ -696,15 +687,9 @@ parseAllWithXeno xmlContent = -- Build final result for a single dataset buildResultForAll :: ParseState -> Either String (Activity, [Flow], [Unit], Int, M.Map UUID Int) buildResultForAll st = - let name = case psActivityName st of - Just n -> n - Nothing -> "Unknown Activity" - location = case psLocation st of - Just loc -> loc - Nothing -> "GLO" - refUnit = case psRefUnit st of - Just u -> u - Nothing -> "UNKNOWN_UNIT" + let name = fromMaybe "Unknown Activity" (psActivityName st) + location = fromMaybe "GLO" (psLocation st) + refUnit = fromMaybe "UNKNOWN_UNIT" (psRefUnit st) description = reverse (psDescription st) classifications = M.fromList $ @@ -727,9 +712,9 @@ streamParseAllDatasetsFromFile1 path = do !xmlContent <- BS.readFile path case parseAllWithXeno xmlContent of Right results -> do - forM_ [e | Left e <- results] $ \e -> + forM_ (lefts results) $ \e -> reportProgress Warning $ "Skipping dataset in " ++ path ++ ": " ++ e - return [r | Right r <- results] + return (rights results) Left err -> do reportProgress Warning $ "Failed to parse " ++ path ++ ": " ++ err return [] diff --git a/src/EcoSpold/Parser2.hs b/src/EcoSpold/Parser2.hs index f975194..0936fb7 100644 --- a/src/EcoSpold/Parser2.hs +++ b/src/EcoSpold/Parser2.hs @@ -1,11 +1,11 @@ {-# LANGUAGE BangPatterns #-} -{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} module EcoSpold.Parser2 (streamParseActivityAndFlowsFromFile, normalizeCAS) where import qualified Data.ByteString as BS import qualified Data.Map as M +import Data.Maybe (catMaybes, fromMaybe) import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T @@ -253,7 +253,7 @@ parseWithXeno xmlContent processId = closeTag state tagName | isElement tagName "activityName" = let txt = T.concat $ reverse $ map bsToText (psTextAccum state) - in state{psActivityName = Just txt, psContext = Other, psPath = tail (psPath state), psTextAccum = []} + in state{psActivityName = Just txt, psContext = Other, psPath = drop 1 (psPath state), psTextAccum = []} | isElement tagName "comment" = -- Capture text only when the immediate parent is the -- exchange itself, not a nested . Property comments @@ -263,7 +263,7 @@ parseWithXeno xmlContent processId = [] -> "" txt = T.concat $ reverse $ map bsToText (psTextAccum state) lang = psPendingCommentLang state - popPath = state{psPath = tail (psPath state), psTextAccum = [], psPendingCommentLang = ""} + popPath = state{psPath = drop 1 (psPath state), psTextAccum = [], psPendingCommentLang = ""} in case psContext state of InIntermediateExchange idata | isElement parent "intermediateExchange" -> @@ -274,7 +274,7 @@ parseWithXeno xmlContent processId = _ -> popPath | isElement tagName "shortname" && psContext state == InGeographyShortname = let txt = T.concat $ reverse $ map bsToText (psTextAccum state) - in state{psLocation = Just txt, psContext = Other, psPath = tail (psPath state), psTextAccum = []} + in state{psLocation = Just txt, psContext = Other, psPath = drop 1 (psPath state), psTextAccum = []} | isElement tagName "intermediateExchange" = case psContext state of InIntermediateExchange idata -> @@ -296,7 +296,7 @@ parseWithXeno xmlContent processId = if T.null (idActivityLinkId idata) then (UUID.nil, Nothing) else parseUUID (idActivityLinkId idata) - uuidWarnings = [w | Just w <- [flowWarn, unitWarn, linkWarn]] + uuidWarnings = catMaybes [flowWarn, unitWarn, linkWarn] exchange = TechnosphereExchange { techFlowId = flowUUID @@ -322,13 +322,11 @@ parseWithXeno xmlContent processId = Nothing -- CAS Nothing -- substanceId unitNameWarning = - if T.null (idUnitName idata) - then - [ "[WARNING] Missing unit name for intermediate exchange with flow ID: " - ++ T.unpack (idFlowId idata) - ++ " - using 'UNKNOWN_UNIT' placeholder" - ] - else [] + [ "[WARNING] Missing unit name for intermediate exchange with flow ID: " + ++ T.unpack (idFlowId idata) + ++ " - using 'UNKNOWN_UNIT' placeholder" + | T.null (idUnitName idata) + ] unit = Unit unitUUID @@ -345,14 +343,14 @@ parseWithXeno xmlContent processId = , psFlows = flow : psFlows state , psUnits = unit : psUnits state , psContext = Other - , psPath = tail (psPath state) + , psPath = drop 1 (psPath state) , psTextAccum = [] , psPendingInputGroup = "" , psPendingOutputGroup = "" , psRefUnit = newRefUnit , psWarnings = uuidWarnings ++ unitNameWarning ++ psWarnings state } - _ -> state{psPath = tail (psPath state)} + _ -> state{psPath = drop 1 (psPath state)} | isElement tagName "elementaryExchange" = case psContext state of InElementaryExchange edata -> @@ -367,20 +365,19 @@ parseWithXeno xmlContent processId = -- Determine if exchange is input (resource extraction) -- Primary: use inputGroup/outputGroup if present -- Fallback: use compartment heuristic (natural resource = input, others = output) - isInput = - if not (T.null finalInputGroup) - then True -- Has explicit inputGroup - else - if not (T.null finalOutputGroup) - then False -- Has explicit outputGroup - else -- Fallback to compartment-based heuristic - case edCompartments edata of - (comp : _) | T.toLower comp == "natural resource" -> True - _ -> False -- Default to output (emissions) - -- Parse UUIDs and collect warnings + -- Determine input/output: + -- explicit inputGroup wins, then explicit outputGroup, else + -- compartment-based heuristic (natural resource = input). + isInput + | not (T.null finalInputGroup) = True + | not (T.null finalOutputGroup) = False + | otherwise = case edCompartments edata of + (comp : _) | T.toLower comp == "natural resource" -> True + _ -> False + -- Parse UUIDs and collect warnings (flowUUID, flowWarn) = parseUUID (edFlowId edata) (unitUUID, unitWarn) = parseUUID (edUnitId edata) - uuidWarnings = [w | Just w <- [flowWarn, unitWarn]] + uuidWarnings = catMaybes [flowWarn, unitWarn] exchange = BiosphereExchange { bioFlowId = flowUUID @@ -407,13 +404,11 @@ parseWithXeno xmlContent processId = (edCAS edata) Nothing -- substanceId - to be filled later unitNameWarning = - if T.null (edUnitName edata) - then - [ "[WARNING] Missing unit name for elementary exchange with flow ID: " - ++ T.unpack (edFlowId edata) - ++ " - using 'UNKNOWN_UNIT' placeholder" - ] - else [] + [ "[WARNING] Missing unit name for elementary exchange with flow ID: " + ++ T.unpack (edFlowId edata) + ++ " - using 'UNKNOWN_UNIT' placeholder" + | T.null (edUnitName edata) + ] unit = Unit unitUUID @@ -425,13 +420,13 @@ parseWithXeno xmlContent processId = , psFlows = flow : psFlows state , psUnits = unit : psUnits state , psContext = Other - , psPath = tail (psPath state) + , psPath = drop 1 (psPath state) , psTextAccum = [] , psPendingInputGroup = "" , psPendingOutputGroup = "" , psWarnings = uuidWarnings ++ unitNameWarning ++ psWarnings state } - _ -> state{psPath = tail (psPath state)} + _ -> state{psPath = drop 1 (psPath state)} | isElement tagName "text" = case psContext state of InGeneralCommentText _idx -> @@ -440,7 +435,7 @@ parseWithXeno xmlContent processId = if T.null txt then state{psContext = Other, psTextAccum = []} else state{psDescription = txt : psDescription state, psContext = Other, psTextAccum = []} - _ -> state{psPath = tail (psPath state), psTextAccum = []} + _ -> state{psPath = drop 1 (psPath state), psTextAccum = []} | isElement tagName "name" = let txt = T.concat $ reverse $ map bsToText (psTextAccum state) isInsideProperty = case psPath state of @@ -449,11 +444,11 @@ parseWithXeno xmlContent processId = in case psContext state of InIntermediateExchange idata | not isInsideProperty -> - state{psContext = InIntermediateExchange idata{idFlowName = txt}, psPath = tail (psPath state), psTextAccum = []} + state{psContext = InIntermediateExchange idata{idFlowName = txt}, psPath = drop 1 (psPath state), psTextAccum = []} InElementaryExchange edata | not isInsideProperty -> - state{psContext = InElementaryExchange edata{edFlowName = txt}, psPath = tail (psPath state), psTextAccum = []} - _ -> state{psPath = tail (psPath state), psTextAccum = []} + state{psContext = InElementaryExchange edata{edFlowName = txt}, psPath = drop 1 (psPath state), psTextAccum = []} + _ -> state{psPath = drop 1 (psPath state), psTextAccum = []} | isElement tagName "unitName" = let txt = T.concat $ reverse $ map bsToText (psTextAccum state) isInsideProperty = case psPath state of @@ -462,50 +457,50 @@ parseWithXeno xmlContent processId = in case psContext state of InIntermediateExchange idata | not isInsideProperty -> - state{psContext = InIntermediateExchange idata{idUnitName = txt}, psPath = tail (psPath state), psTextAccum = []} + state{psContext = InIntermediateExchange idata{idUnitName = txt}, psPath = drop 1 (psPath state), psTextAccum = []} InElementaryExchange edata | not isInsideProperty -> - state{psContext = InElementaryExchange edata{edUnitName = txt}, psPath = tail (psPath state), psTextAccum = []} - _ -> state{psPath = tail (psPath state), psTextAccum = []} + state{psContext = InElementaryExchange edata{edUnitName = txt}, psPath = drop 1 (psPath state), psTextAccum = []} + _ -> state{psPath = drop 1 (psPath state), psTextAccum = []} | isElement tagName "synonym" = let txt = T.strip $ T.concat $ reverse $ map bsToText (psTextAccum state) in case psContext state of InIntermediateExchange idata | not (T.null txt) -> let syns = M.insertWith S.union "en" (S.singleton txt) (idSynonyms idata) - in state{psContext = InIntermediateExchange idata{idSynonyms = syns}, psPath = tail (psPath state), psTextAccum = []} + in state{psContext = InIntermediateExchange idata{idSynonyms = syns}, psPath = drop 1 (psPath state), psTextAccum = []} InElementaryExchange edata | not (T.null txt) -> let syns = M.insertWith S.union "en" (S.singleton txt) (edSynonyms edata) - in state{psContext = InElementaryExchange edata{edSynonyms = syns}, psPath = tail (psPath state), psTextAccum = []} - _ -> state{psPath = tail (psPath state), psTextAccum = []} + in state{psContext = InElementaryExchange edata{edSynonyms = syns}, psPath = drop 1 (psPath state), psTextAccum = []} + _ -> state{psPath = drop 1 (psPath state), psTextAccum = []} | isElement tagName "inputGroup" = let txt = T.strip $ T.concat $ reverse $ map bsToText (psTextAccum state) in -- DON'T change psContext - preserve the parent exchange context - state{psPendingInputGroup = txt, psPath = tail (psPath state), psTextAccum = []} + state{psPendingInputGroup = txt, psPath = drop 1 (psPath state), psTextAccum = []} | isElement tagName "outputGroup" = let txt = T.strip $ T.concat $ reverse $ map bsToText (psTextAccum state) in -- DON'T change psContext - preserve the parent exchange context - state{psPendingOutputGroup = txt, psPath = tail (psPath state), psTextAccum = []} + state{psPendingOutputGroup = txt, psPath = drop 1 (psPath state), psTextAccum = []} | isElement tagName "compartment" = let txt = T.strip $ T.concat $ reverse $ map bsToText (psTextAccum state) in case psContext state of InElementaryExchange edata | not (T.null txt) -> - state{psContext = InElementaryExchange edata{edCompartments = txt : edCompartments edata}, psPath = tail (psPath state), psTextAccum = []} + state{psContext = InElementaryExchange edata{edCompartments = txt : edCompartments edata}, psPath = drop 1 (psPath state), psTextAccum = []} _ -> - state{psPath = tail (psPath state), psTextAccum = []} + state{psPath = drop 1 (psPath state), psTextAccum = []} | isElement tagName "subcompartment" = let txt = T.strip $ T.concat $ reverse $ map bsToText (psTextAccum state) in case psContext state of InElementaryExchange edata | not (T.null txt) -> - state{psContext = InElementaryExchange edata{edSubcompartments = txt : edSubcompartments edata}, psPath = tail (psPath state), psTextAccum = []} + state{psContext = InElementaryExchange edata{edSubcompartments = txt : edSubcompartments edata}, psPath = drop 1 (psPath state), psTextAccum = []} _ -> - state{psPath = tail (psPath state), psTextAccum = []} + state{psPath = drop 1 (psPath state), psTextAccum = []} | isElement tagName "classificationSystem" = let txt = T.strip $ T.concat $ reverse $ map bsToText (psTextAccum state) - in state{psPendingClassSystem = txt, psPath = tail (psPath state), psTextAccum = []} + in state{psPendingClassSystem = txt, psPath = drop 1 (psPath state), psTextAccum = []} | isElement tagName "classificationValue" = let txt = T.strip $ T.concat $ reverse $ map bsToText (psTextAccum state) sys = psPendingClassSystem state @@ -514,28 +509,22 @@ parseWithXeno xmlContent processId = if T.null sys || T.null txt then psClassifications state else M.insert sys txt (psClassifications state) - , psPath = tail (psPath state) + , psPath = drop 1 (psPath state) , psTextAccum = [] } | otherwise = - state{psPath = if null (psPath state) then [] else tail (psPath state)} + state{psPath = drop 1 (psPath state)} -- CDATA handler - treat as text - cdata state content = text state content + cdata = text -- Build final result from parse state buildResult :: ParseState -> ProcessId -> Either String (Activity, [Flow], [Unit]) buildResult st _pid = - let name = case psActivityName st of - Just n -> n - Nothing -> "Unknown Activity" - location = case psLocation st of - Just loc -> loc - Nothing -> "GLO" + let name = fromMaybe "Unknown Activity" (psActivityName st) + location = fromMaybe "GLO" (psLocation st) description = reverse (psDescription st) -- Reverse to get correct order - refUnit = case psRefUnit st of - Just u -> u - Nothing -> "UNKNOWN_UNIT" + refUnit = fromMaybe "UNKNOWN_UNIT" (psRefUnit st) -- Apply cutoff strategy to exchanges activity = Activity name description M.empty (psClassifications st) location refUnit (reverse $ psExchanges st) M.empty M.empty Nothing Nothing flows = reverse (psFlows st) @@ -581,7 +570,7 @@ hasReferenceProduct activity = any exchangeIsReference (exchanges activity) -- | Remove production exchanges with zero amounts removeZeroAmountCoproducts :: [Exchange] -> [Exchange] -removeZeroAmountCoproducts exs = filter keepExchange exs +removeZeroAmountCoproducts = filter keepExchange where keepExchange TechnosphereExchange{techIsInput = False, techIsReference = True} = True keepExchange TechnosphereExchange{techIsInput = False, techIsReference = False, techAmount = amount} = amount /= 0.0 diff --git a/src/Method/ChemSynonyms.hs b/src/Method/ChemSynonyms.hs index c05d2c5..8f95a78 100644 --- a/src/Method/ChemSynonyms.hs +++ b/src/Method/ChemSynonyms.hs @@ -33,7 +33,6 @@ module Method.ChemSynonyms ( ) where import qualified Data.ByteString as BS -import Data.List (foldl') import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Set (Set) diff --git a/src/Method/FlowResolver.hs b/src/Method/FlowResolver.hs index bdcd504..df491bd 100644 --- a/src/Method/FlowResolver.hs +++ b/src/Method/FlowResolver.hs @@ -300,12 +300,12 @@ Level 2: "Emissions to air, indoor" → subcompartment = "indoor" -} parseCompartment :: [Text] -> Maybe Compartment parseCompartment [] = Nothing -parseCompartment cats = +parseCompartment cats@(firstCat : _) = let -- Level 1 typically contains medium: "Emissions to air", "Emissions to water", etc. medium = case drop 1 cats of (lvl1 : _) -> extractMedium lvl1 - [] -> extractMedium (head cats) -- cats is non-empty ([] case handled above) + [] -> extractMedium firstCat -- Level 2 is the subcompartment subcomp = case drop 2 cats of (lvl2 : _) -> extractSubcompartment lvl2 diff --git a/src/Method/Mapping.hs b/src/Method/Mapping.hs index aef3764..d2fe6e2 100644 --- a/src/Method/Mapping.hs +++ b/src/Method/Mapping.hs @@ -70,6 +70,7 @@ module Method.Mapping ( import Control.DeepSeq (NFData) import Control.Monad.ST (runST) import Data.Aeson (ToJSON) +import Data.Either (lefts, rights) import Data.List (find, sortOn) import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as NE @@ -87,10 +88,8 @@ import qualified Data.Vector.Unboxed.Mutable as MU import Data.Word (Word8) import GHC.Generics (Generic) -import qualified Data.Set as S import qualified Data.Set as Set import Matrix (Inventory, Vector) -import qualified Matrix import Method.ChemSynonyms (ChemSynonyms, expandedTokens) import Method.Types import Plugin.Types (MapContext (..), MapQuery (..), MapResult (..), MapperHandle (..)) @@ -1029,50 +1028,11 @@ sumRegionalizedLCIAScoreCrossDB :: Either Text Double sumRegionalizedLCIAScoreCrossDB unitCfg unitDB flowDB hier triples = let results = [computeRegionalizedLCIAScore unitCfg unitDB flowDB db sv hier t | (db, sv, t) <- triples] - rights = [s | Right s <- results] - lefts = [e | Left e <- results] - in case (rights, lefts) of - ([], errs) | not (null errs) -> Left (T.intercalate "; " errs) - _ -> Right (sum rights) - -{- | Resolve a CF for a (flow, location) pair through the hierarchy + broadcast -fallback. See 'computeRegionalizedLCIAScore' for the rules. - -* @Right Nothing@: the flow is not covered by this method (silent OK). -* @Right (Just v)@: a CF was found. -* @Left err@: the flow IS regionalized in this method but no CF could be - resolved for the given location even after walking parents — surfacing the - gap prevents silent under-counting. --} -resolveRegionalCF :: - MethodTables -> - FlowDB -> - Set.Set UUID -> - M.Map Text [Text] -> - UUID -> - Text -> - Either Text (Maybe (Double, Text)) -resolveRegionalCF tables flowDB regionalizedFlows hier flowUUID loc = - case M.lookup (flowUUID, loc) (mtRegionalizedCF tables) of - Just v -> Right (Just v) - Nothing -> - let parents = M.findWithDefault [] loc hier - fromParents = firstJust [M.lookup (flowUUID, p) (mtRegionalizedCF tables) | p <- parents] - in case fromParents of - Just v -> Right (Just v) - Nothing -> case lookupCascadeCF tables flowDB flowUUID of - Just v -> Right (Just v) - Nothing - | Set.member flowUUID regionalizedFlows -> - Left $ - "Regionalized CF lookup failed: flow " - <> T.pack (show flowUUID) - <> " has regional CFs in this method but none for location '" - <> loc - <> "' (after walking " - <> T.pack (show (length parents)) - <> " parent regions) and no universal broadcast." - | otherwise -> Right Nothing + oks = rights results + errs = lefts results + in case (oks, errs) of + ([], es) | not (null es) -> Left (T.intercalate "; " es) + _ -> Right (sum oks) {- | Cascade CF lookup: UUID → exact (name, medium, subcomp) → fallback (name, medium). The same logic is baked into 'mtBroadcast' once unit conversion is available; @@ -1140,11 +1100,6 @@ convertAndMultiply unitConfig unitDB mflow (cfVal, cfUnit) qty = converted = convertForCharacterization unitConfig flowUnit cfUnit qty in converted * cfVal -firstJust :: [Maybe a] -> Maybe a -firstJust [] = Nothing -firstJust (Just x : _) = Just x -firstJust (Nothing : xs) = firstJust xs - {- | Per-flow contributions over an 'Inventory', keyed by flow UUID (possibly cross-DB-merged). Walks the inventory directly (not the mappings) so any flow with a matchable CF contributes — including flows from dep DBs that @@ -1205,7 +1160,7 @@ processContributionsFromTables :: UnitDB -> FlowDB -> Database -> - Matrix.Vector -> + Vector -> MethodTables -> M.Map ProcessId Double processContributionsFromTables unitConfig unitDB flowDB db scalingVec tables = diff --git a/src/Method/ParserSimaPro.hs b/src/Method/ParserSimaPro.hs index afeb0fe..4ccccc9 100644 --- a/src/Method/ParserSimaPro.hs +++ b/src/Method/ParserSimaPro.hs @@ -17,7 +17,6 @@ module Method.ParserSimaPro ( import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as BS8 import Data.Char (toLower) -import Data.List (foldl') import qualified Data.Map.Strict as M import Data.Text (Text) import qualified Data.Text as T diff --git a/src/Search/BM25.hs b/src/Search/BM25.hs index b2a9a62..4cbd443 100644 --- a/src/Search/BM25.hs +++ b/src/Search/BM25.hs @@ -15,7 +15,6 @@ module Search.BM25 ( import Control.Monad (forM_) import Control.Monad.ST (runST) -import Data.Foldable (foldl') import Data.Int (Int32) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M diff --git a/src/Service.hs b/src/Service.hs index 22b259a..dae5cd7 100644 --- a/src/Service.hs +++ b/src/Service.hs @@ -1323,7 +1323,11 @@ getPathTo db solver pidText target = do Left $ ActivityNotFound $ "No upstream node matching '" <> target <> "' reachable from " <> pidText - Just pids -> + Just [] -> + Left $ + ActivityNotFound $ + "BFS returned empty path from " <> pidText + Just pids@(firstPid : restPids) -> let scalingOf i = supplyVec U.! i mkStep i mRatio = let act = dbActivities db V.! i @@ -1340,9 +1344,9 @@ getPathTo db solver pidText target = do Nothing -> base Just r -> base ++ ["local_step_ratio" .= r] steps = - mkStep (head pids) (Nothing :: Maybe Double) + mkStep firstPid (Nothing :: Maybe Double) : [ mkStep c (Just ratio) - | (p, c) <- zip pids (tail pids) + | (p, c) <- zip pids restPids , let ratio = if scalingOf p == 0 then 0 @@ -1351,7 +1355,7 @@ getPathTo db solver pidText target = do totalRatio = product [ scalingOf c / scalingOf p - | (p, c) <- zip pids (tail pids) + | (p, c) <- zip pids restPids , scalingOf p /= 0 ] in Right $ diff --git a/src/SimaPro/Parser.hs b/src/SimaPro/Parser.hs index 379d0dc..b07ff89 100644 --- a/src/SimaPro/Parser.hs +++ b/src/SimaPro/Parser.hs @@ -38,7 +38,7 @@ import qualified Data.ByteString.Char8 as BS8 import qualified Data.ByteString.Lazy as BL import Data.Char (isUpper, toLower) import qualified Data.Csv as Csv -import Data.List (dropWhileEnd, foldl') +import Data.List (dropWhileEnd) import qualified Data.Map.Strict as M import qualified Data.Set as S import Data.Text (Text) diff --git a/src/Types.hs b/src/Types.hs index d080104..f8cdaf9 100644 --- a/src/Types.hs +++ b/src/Types.hs @@ -31,7 +31,7 @@ import qualified Data.Vector.Generic.Mutable as VGM import qualified Data.Vector.Unboxed as VU import GHC.Generics (Generic) -import Data.List (foldl', nub) +import Data.List (nub) import Search.BM25.Types (BM25Index) import SynonymDB (normalizeName) import SynonymDB.Types (SynonymDB) diff --git a/src/UnitConversion.hs b/src/UnitConversion.hs index 5a417ee..1492391 100644 --- a/src/UnitConversion.hs +++ b/src/UnitConversion.hs @@ -1,7 +1,5 @@ -{-# LANGUAGE BangPatterns #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} {- | Dimensional unit conversion system. @@ -52,6 +50,7 @@ import Data.Csv (HasHeader (..), decode) import Data.IORef (IORef, modifyIORef', newIORef, readIORef) import Data.List (elemIndex) import qualified Data.Map.Strict as M +import Data.Maybe (fromMaybe) import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T @@ -177,7 +176,7 @@ parseDimension dimOrder expr <> ")" modifyAt :: Int -> (Int -> Int) -> [Int] -> [Int] - modifyAt idx f xs = zipWith (\i x -> if i == idx then f x else x) [0 ..] xs + modifyAt idx f = zipWith (\i x -> if i == idx then f x else x) [0 ..] foldlM :: (b -> a -> Either e b) -> b -> [a] -> Either e b foldlM _ acc [] = Right acc @@ -201,7 +200,7 @@ buildFromCSV csvData = UnitConfig { ucDimensionOrder = dimOrder , ucUnits = units - , ucOriginalKeys = M.mapWithKey (\k _ -> k) units + , ucOriginalKeys = M.mapWithKey const units } parseRow dimOrder (name, dimExpr, factor) = do @@ -213,9 +212,9 @@ buildFromCSV csvData = -} mergeUnitConfigs :: [UnitConfig] -> UnitConfig mergeUnitConfigs [] = defaultUnitConfig -mergeUnitConfigs cfgs = +mergeUnitConfigs cfgs@(first : _) = UnitConfig - { ucDimensionOrder = ucDimensionOrder (head cfgs) + { ucDimensionOrder = ucDimensionOrder first , ucUnits = M.unions (reverse $ map ucUnits cfgs) , ucOriginalKeys = M.unions (reverse $ map ucOriginalKeys cfgs) } @@ -273,6 +272,4 @@ Returns the original amount if conversion fails. -} convertExchangeAmount :: UnitConfig -> Text -> Text -> Double -> Double convertExchangeAmount cfg fromUnit toUnit amount = - case convertUnit cfg fromUnit toUnit amount of - Just converted -> converted - Nothing -> amount + fromMaybe amount (convertUnit cfg fromUnit toUnit amount) diff --git a/test/BM25Spec.hs b/test/BM25Spec.hs index 967b36a..5e36652 100644 --- a/test/BM25Spec.hs +++ b/test/BM25Spec.hs @@ -6,7 +6,7 @@ import Data.List (sortOn) import qualified Data.Map.Strict as M import Data.Ord (Down (Down)) import Data.Text (Text) -import Data.UUID (UUID, nil) +import Data.UUID (nil) import qualified Data.Vector as V import qualified Data.Vector.Unboxed as VU import Test.Hspec @@ -66,10 +66,8 @@ mkFlow fid name = , flowSubstanceId = Nothing } -f1, f2, f3 :: UUID +f1 :: UUID f1 = read "11111111-1111-1111-1111-111111111111" -f2 = read "22222222-2222-2222-2222-222222222222" -f3 = read "33333333-3333-3333-3333-333333333333" -- | Return docIds sorted by descending score, filtering out zeros. ranking :: VU.Vector Double -> [Int] diff --git a/test/CrossDBInventorySpec.hs b/test/CrossDBInventorySpec.hs index d6b2ed9..ed9004e 100644 --- a/test/CrossDBInventorySpec.hs +++ b/test/CrossDBInventorySpec.hs @@ -19,7 +19,6 @@ import Data.Maybe (listToMaybe) import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T -import Data.UUID (UUID) import qualified Data.UUID as UUID import qualified Data.Vector as V import qualified Data.Vector.Unboxed as U diff --git a/test/CrossDBRegionalLCIAFixture.hs b/test/CrossDBRegionalLCIAFixture.hs index 43f77d5..d14b67c 100644 --- a/test/CrossDBRegionalLCIAFixture.hs +++ b/test/CrossDBRegionalLCIAFixture.hs @@ -47,7 +47,6 @@ module CrossDBRegionalLCIAFixture ( import Data.Int (Int32) import qualified Data.Map.Strict as M import Data.Text (Text) -import Data.UUID (UUID) import qualified Data.UUID as UUID import qualified Data.Vector as V import qualified Data.Vector.Unboxed as U diff --git a/test/CrossDBRegionalLCIASensitivitySpec.hs b/test/CrossDBRegionalLCIASensitivitySpec.hs index 5d8912c..db5e90a 100644 --- a/test/CrossDBRegionalLCIASensitivitySpec.hs +++ b/test/CrossDBRegionalLCIASensitivitySpec.hs @@ -53,7 +53,7 @@ spec = describe "cross-DB regional LCIA via sensitivity propagation" $ do -- must equal the plain-path baseline (5.0), proving the -- perturbed-scaling path picks up dep-DB emissions instead of -- silently zeroing them. - rootSolver <- mkSolverFromDb rootDb "root" + _rootSolver <- mkSolverFromDb rootDb "root" depSolver <- mkSolverFromDb depDb "dep" let depLookup name = pure $ @@ -103,7 +103,7 @@ spec = describe "cross-DB regional LCIA via sensitivity propagation" $ do -- depends on: if Sherman-Morrison produces a perturbed scaling -- that differs from baseline by Δ, the downstream regional -- score must reflect that Δ, not silently flatten it. - rootSolver <- mkSolverFromDb rootDb "root" + _rootSolver <- mkSolverFromDb rootDb "root" depSolver <- mkSolverFromDb depDb "dep" let depLookup name = pure $ diff --git a/test/CrossLinkingSpec.hs b/test/CrossLinkingSpec.hs index dd0e8c5..ab22fc0 100644 --- a/test/CrossLinkingSpec.hs +++ b/test/CrossLinkingSpec.hs @@ -2,7 +2,6 @@ module CrossLinkingSpec (spec) where -import qualified Data.Map.Strict as M import Database.CrossLinking import Database.Loader (loadDatabase) import SynonymDB (buildFromPairs, emptySynonymDB) diff --git a/test/EcoSpold1Spec.hs b/test/EcoSpold1Spec.hs index beb2861..e31e20e 100644 --- a/test/EcoSpold1Spec.hs +++ b/test/EcoSpold1Spec.hs @@ -68,10 +68,9 @@ mkBio fid amt = , bioPedigree = Nothing } -fid1, fid2, fid3 :: UUID +fid1, fid2 :: UUID fid1 = read "11111111-1111-1111-1111-111111111111" fid2 = read "22222222-2222-2222-2222-222222222222" -fid3 = read "33333333-3333-3333-3333-333333333333" -- | Minimal valid EcoSpold1 XML with one reference product and one air emission minimalXml :: BC.ByteString diff --git a/test/ILCDParserSpec.hs b/test/ILCDParserSpec.hs index e2d259d..6d652fe 100644 --- a/test/ILCDParserSpec.hs +++ b/test/ILCDParserSpec.hs @@ -8,15 +8,9 @@ import qualified Data.Map.Strict as M import Data.Text (Text) import qualified Data.UUID as UUID import ILCD.Parser (ILCDExchangeRaw (..), ILCDProcessRaw (..), buildSupplierIndex, fixActivityExchanges, parseILCDDirectory, parseProcessXML) -import System.IO (hClose) -import System.IO.Temp (withSystemTempFile) import Test.Hspec import Types -isRight :: Either a b -> Bool -isRight (Right _) = True -isRight _ = False - classOf :: BS.ByteString -> M.Map Text Text classOf = maybe M.empty iprClassifications . parseProcessXML diff --git a/test/LoaderSpec.hs b/test/LoaderSpec.hs index 865b07d..7a05414 100644 --- a/test/LoaderSpec.hs +++ b/test/LoaderSpec.hs @@ -16,11 +16,10 @@ import Types -- Minimal fixtures -- --------------------------------------------------------------------------- -flowUUID1, flowUUID2, actUUID1, actUUID2 :: UUID.UUID +flowUUID1, flowUUID2, actUUID1 :: UUID.UUID flowUUID1 = read "aaaaaaaa-0000-0000-0000-000000000001" flowUUID2 = read "bbbbbbbb-0000-0000-0000-000000000002" actUUID1 = read "cccccccc-0000-0000-0000-000000000001" -actUUID2 = read "cccccccc-0000-0000-0000-000000000002" minimalFlow :: UUID.UUID -> Text -> Flow minimalFlow fid name = diff --git a/test/MappingSpec.hs b/test/MappingSpec.hs index a35b851..0c13e86 100644 --- a/test/MappingSpec.hs +++ b/test/MappingSpec.hs @@ -15,7 +15,6 @@ import Method.Types (Compartment (..), FlowDirection (..), Method (..), MethodCF import SynonymDB (buildFromPairs, emptySynonymDB) import Types (Flow (..), FlowType (..), Unit (..)) import UnitConversion (UnitConfig (..), UnitDef (..), defaultUnitConfig) -import qualified UnitConversion -- --------------------------------------------------------------------------- -- Helpers diff --git a/test/MatrixConstructionSpec.hs b/test/MatrixConstructionSpec.hs index 446b0c0..3f75296 100644 --- a/test/MatrixConstructionSpec.hs +++ b/test/MatrixConstructionSpec.hs @@ -2,10 +2,8 @@ module MatrixConstructionSpec (spec) where -import qualified Data.Map as M import qualified Data.Vector as V import qualified Data.Vector.Unboxed as VU -import GoldenData import Test.Hspec import TestHelpers import Types diff --git a/test/MatrixExportSpec.hs b/test/MatrixExportSpec.hs index 054f773..7b0c628 100644 --- a/test/MatrixExportSpec.hs +++ b/test/MatrixExportSpec.hs @@ -5,7 +5,6 @@ module MatrixExportSpec (spec) where import qualified Data.Text as T import qualified Data.Text.IO as TIO import qualified Data.UUID as UUID -import GoldenData import Matrix.Export ( MatrixDebugInfo (..), escapeCsvField, @@ -45,11 +44,7 @@ spec = do let diagonalLines = filter (T.isInfixOf "0;0;1.0") lines length diagonalLines `shouldSatisfy` (>= 1) - -- Check off-diagonal entries (should be NEGATIVE for (I-A) format) - let offDiagonalLines = tail lines -- Skip header - let offDiagonalEntries = filter (\l -> not (T.isInfixOf ";1.0;" l)) offDiagonalLines - - -- For SAMPLE.min3: Expected -0.6 and -0.4 + -- For SAMPLE.min3: Expected -0.6 and -0.4 off-diagonal entries. let hasNegative = any (T.isInfixOf "-0.") lines hasNegative `shouldBe` True diff --git a/test/MethodSpec.hs b/test/MethodSpec.hs index b586963..7738cf1 100644 --- a/test/MethodSpec.hs +++ b/test/MethodSpec.hs @@ -4,7 +4,6 @@ module MethodSpec (spec) where import qualified Data.Map.Strict as M -import qualified Data.Set as S import qualified Data.Text as T import qualified Data.Text.Encoding as TE import qualified Data.UUID as UUID @@ -20,7 +19,6 @@ import Method.ParserCSV (parseMethodCSVBytes) import Method.ParserNW (parseNormWeightCSVBytes) import Method.ParserSimaPro (isSimaProMethodCSV, parseSimaProMethodCSVBytes) import Method.Types -import Method.Types (Compartment (..)) import SynonymDB import Types (Flow (..), FlowType (..)) import UnitConversion (defaultUnitConfig) diff --git a/test/ParserSpec.hs b/test/ParserSpec.hs index 07eec8d..a377af4 100644 --- a/test/ParserSpec.hs +++ b/test/ParserSpec.hs @@ -5,7 +5,6 @@ module ParserSpec (spec) where import qualified Data.Map as M import qualified Data.Text as T import qualified Data.Vector as V -import GoldenData import Test.Hspec import TestHelpers import Types diff --git a/test/RegionalLCIASpec.hs b/test/RegionalLCIASpec.hs index 98838ee..7693ad9 100644 --- a/test/RegionalLCIASpec.hs +++ b/test/RegionalLCIASpec.hs @@ -12,7 +12,7 @@ module RegionalLCIASpec (spec) where import Data.Int (Int32) import qualified Data.Map.Strict as M import Data.Text (Text) -import Data.UUID (UUID, nil) +import Data.UUID (UUID) import qualified Data.UUID as UUID import qualified Data.Vector as V import qualified Data.Vector.Unboxed as U diff --git a/test/ServerSpec.hs b/test/ServerSpec.hs index db5225b..e41c004 100644 --- a/test/ServerSpec.hs +++ b/test/ServerSpec.hs @@ -167,7 +167,7 @@ serverSpecs = do it "POST /api/v1/idle-timeout/0 cancels timeout" $ do withMinimalConfig $ \cfgPath -> - withServer cfgPath $ \ph mgr -> do + withServer cfgPath $ \_ph mgr -> do -- Activate 2s timeout then immediately cancel _ <- postEndpoint mgr "/api/v1/idle-timeout/2" _ <- postEndpoint mgr "/api/v1/idle-timeout/0" diff --git a/test/ServiceSpec.hs b/test/ServiceSpec.hs index b6ce720..5b6de83 100644 --- a/test/ServiceSpec.hs +++ b/test/ServiceSpec.hs @@ -15,8 +15,6 @@ import qualified Data.Map.Strict as M import qualified Data.Set as S import Data.Text (Text) import Data.UUID (nil) -import qualified Data.UUID as UUID -import GoldenData import Service ( ServiceError (..), buildUnitGroups, diff --git a/test/SharedSolverSpec.hs b/test/SharedSolverSpec.hs index 4dae754..634a021 100644 --- a/test/SharedSolverSpec.hs +++ b/test/SharedSolverSpec.hs @@ -4,7 +4,7 @@ module SharedSolverSpec (spec) where import qualified Data.Vector.Unboxed as U import GoldenData -import Matrix (buildDemandVectorFromIndex, computeScalingVector) +import Matrix (buildDemandVectorFromIndex) import SharedSolver (SharedSolver, createSharedSolver, getFactorization, solveWithSharedSolver) import Test.Hspec import TestHelpers (assertVectorNear, loadSampleDatabase) diff --git a/test/SimaProParserSpec.hs b/test/SimaProParserSpec.hs index 303ba10..438cb07 100644 --- a/test/SimaProParserSpec.hs +++ b/test/SimaProParserSpec.hs @@ -6,7 +6,6 @@ module SimaProParserSpec (spec) where import qualified Data.ByteString as BS import qualified Data.Map.Strict as M import qualified Data.Set as S -import qualified Data.Text as T import Expr (evaluate, normalizeExpr) import SimaPro.Parser ( BioExchangeRow (..), @@ -324,19 +323,6 @@ parseYieldChainCSV = withSystemTempFile "yield-test.csv" $ \path handle -> do hClose handle parseSimaProCSV defaultUnitConfig path --- Helper: find technosphere input by name -findInput :: Activity -> T.Text -> Maybe Exchange -findInput act query = case [ e - | e@TechnosphereExchange{} <- exchanges act - , techIsInput e - , not (techIsReference e) - ] of - exs -> case filter (matchesName query) exs of - (e : _) -> Just e - _ -> Nothing - where - matchesName _ _ = True -- We check by position since we can't easily get flow names here - -- Helper: get all tech input amounts techInputAmounts :: Activity -> [Double] techInputAmounts act = @@ -1085,12 +1071,6 @@ isLeft :: Either a b -> Bool isLeft (Left _) = True isLeft _ = False --- | Two Maybe Double values, considered equal within 0.01. -approxEqAlloc :: Maybe Double -> Maybe Double -> Bool -approxEqAlloc (Just a) (Just b) = abs (a - b) < 0.01 -approxEqAlloc Nothing Nothing = True -approxEqAlloc _ _ = False - {- | Generic 5-coproduct fixture: one Process block emits 5 fictional outputs with five mass-allocation formulas. Percentages are chosen so they sum to exactly 100 and use round numbers (50/20/15/10/5), keeping the test diff --git a/test/TestHelpers.hs b/test/TestHelpers.hs index 7073044..6a83498 100644 --- a/test/TestHelpers.hs +++ b/test/TestHelpers.hs @@ -48,7 +48,7 @@ withinTolerance tolerance expected actual = abs (expected - actual) < tolerance -- | Assert that a vector is near expected values assertVectorNear :: String -> Double -> U.Vector Double -> [Double] -> Expectation -assertVectorNear label tolerance actualVec expectedList = do +assertVectorNear _label tolerance actualVec expectedList = do let actual = U.toList actualVec length actual `shouldBe` length expectedList zipWithM_ (\a e -> withinTolerance tolerance e a `shouldBe` True) actual expectedList diff --git a/test/UnitConversionSpec.hs b/test/UnitConversionSpec.hs index 84d524c..2fa9f8c 100644 --- a/test/UnitConversionSpec.hs +++ b/test/UnitConversionSpec.hs @@ -3,7 +3,6 @@ module UnitConversionSpec (spec) where import qualified Data.ByteString.Lazy as BL -import qualified Data.Map.Strict as M import qualified Data.Text as T import Test.Hspec import UnitConversion @@ -13,11 +12,6 @@ isLeft :: Either a b -> Bool isLeft (Left _) = True isLeft _ = False --- Helper for testing Right results -isRight :: Either a b -> Bool -isRight (Right _) = True -isRight _ = False - -- | Load the full unit config from data/units.csv loadFullUnitConfig :: IO UnitConfig loadFullUnitConfig = do diff --git a/test/UploadedDatabaseSpec.hs b/test/UploadedDatabaseSpec.hs index 2f6d0c1..29225c1 100644 --- a/test/UploadedDatabaseSpec.hs +++ b/test/UploadedDatabaseSpec.hs @@ -2,12 +2,10 @@ module UploadedDatabaseSpec (spec) where -import Data.Text (Text) import qualified Data.Text as T import System.IO.Temp (withSystemTempDirectory) import Test.Hspec -import Database.Upload (DatabaseFormat (..)) import Database.UploadedDatabase -- | Minimal UploadMeta without description diff --git a/volca.cabal b/volca.cabal index b481bad..459c62e 100644 --- a/volca.cabal +++ b/volca.cabal @@ -1,6 +1,6 @@ cabal-version: 2.4 name: volca -version: 0.6.1-dev +version: 0.6.1.0 build-type: Simple license: Apache-2.0 @@ -159,6 +159,13 @@ executable volca test-suite lca-tests import: warnings, rtsdefaults + -- Test code idiomatically destructures well-formed fixtures with partial + -- patterns; a fixture mismatch should crash the test with the relevant + -- 'head'/'let Just x' source location rather than be silenced. + ghc-options: -Wno-x-partial + -Wno-incomplete-uni-patterns + -Wno-incomplete-record-updates + -Wno-name-shadowing type: exitcode-stdio-1.0 hs-source-dirs: test main-is: Spec.hs