diff --git a/src/Types/Transaction/TransactionBuySell.hs b/src/Types/Transaction/TransactionBuySell.hs index aedb047..e34d636 100644 --- a/src/Types/Transaction/TransactionBuySell.hs +++ b/src/Types/Transaction/TransactionBuySell.hs @@ -17,8 +17,8 @@ import Types.Transaction.GenericTransaction ) import Types.UtilTypes ( parseInt, - parseMoney, - parseMoneyDefault0, + parseSvMoney, + parseSvMoneyDefault0, ) import Util (SortableByDate (..)) @@ -35,11 +35,11 @@ transformToBuySell (GenericTransaction date account action company quantity rate "Sälj" -> Just Sell _ -> Nothing quantity' <- parseInt quantity - rate' <- parseMoney rate - amount' <- parseMoney amount + rate' <- parseSvMoney rate + amount' <- parseSvMoney amount -- I've noticed that courtage = 0 is sometimes indicated with "-" rather than -- "0", therefor the default - let courtage' = parseMoneyDefault0 courtage + let courtage' = parseSvMoneyDefault0 courtage return $ GenericTransaction diff --git a/src/Types/Transaction/TransactionDividend.hs b/src/Types/Transaction/TransactionDividend.hs index ac408aa..519fb6f 100644 --- a/src/Types/Transaction/TransactionDividend.hs +++ b/src/Types/Transaction/TransactionDividend.hs @@ -13,8 +13,8 @@ import Types.Transaction.GenericTransaction ) import Types.UtilTypes ( parseInt, - parseMoney, - parseMoneyDefault0, + parseSvMoney, + parseSvMoneyDefault0, ) type TransactionDividend = GenericTransaction Action Int Money @@ -29,11 +29,11 @@ transformToDividend (GenericTransaction date account action company quantity rat "Utdelning" -> Just Dividend _ -> Nothing quantity' <- parseInt quantity - rate' <- parseMoney rate - amount' <- parseMoney amount + rate' <- parseSvMoney rate + amount' <- parseSvMoney amount -- I've noticed that courtage = 0 is sometimes indicated with "-" rather than -- "0", therefor the default - let courtage' = parseMoneyDefault0 courtage + let courtage' = parseSvMoneyDefault0 courtage return $ GenericTransaction diff --git a/src/Types/UtilTypes.hs b/src/Types/UtilTypes.hs index c424551..704e86f 100644 --- a/src/Types/UtilTypes.hs +++ b/src/Types/UtilTypes.hs @@ -12,6 +12,7 @@ module Types.UtilTypes normalizeSvNumber, parseSvDouble, parseSvMoney, + parseSvMoneyDefault0, SortedByDateList (..), ) where @@ -44,11 +45,12 @@ parseMoneyDefault0 t = case parseMoney t of Just m -> m Nothing -> Money 0.0 --- Avanza's "consolidated holdings" export formats numbers in the Swedish locale: --- a comma as the decimal separator and (for large values) spaces as thousands --- separators, e.g. "1 234 567,89". This normalizes such a value into something --- 'parseDouble' understands by dropping the (possibly non-breaking) spaces and --- swapping the decimal comma for a dot. +-- Avanza's CSV exports format numbers in the Swedish locale: a comma as the +-- decimal separator and (for large values) spaces as thousands separators, e.g. +-- "1 234 567,89". 'parseDouble' (Data.Text.Read) only understands a dot and +-- stops at the comma, silently truncating "114,3" to 114. This normalizes such a +-- value into something 'parseDouble' reads in full by dropping the (possibly +-- non-breaking) spaces and swapping the decimal comma for a dot. normalizeSvNumber :: Text -> Text normalizeSvNumber = T.replace (T.pack ",") (T.pack ".") @@ -60,6 +62,11 @@ parseSvDouble = parseDouble . normalizeSvNumber parseSvMoney :: Text -> Maybe Money parseSvMoney t = Money <$> parseSvDouble t +parseSvMoneyDefault0 :: Text -> Money +parseSvMoneyDefault0 t = case parseSvMoney t of + Just m -> m + Nothing -> Money 0.0 + newtype SortedByDateList a = SortedByDateList {getSortedByDateList :: [a]} deriving (Show, Eq, Ord, Functor, Foldable) diff --git a/test/ParseSpec.hs b/test/ParseSpec.hs index d2b1629..fa0fdb1 100644 --- a/test/ParseSpec.hs +++ b/test/ParseSpec.hs @@ -13,7 +13,12 @@ import Test.Hspec import TestUtils (shouldThrowErrorContaining) import Types.Money (Money (..)) import Types.Position (Position (..)) -import Types.Transaction.GenericTransaction (GenericTransaction (..)) +import Types.Transaction.GenericTransaction + ( GenericTransaction (..), + getAmount, + getCourtage, + getRate, + ) import Types.Transaction.TransactionBuySell (transformToBuySell) spec :: Spec @@ -21,6 +26,7 @@ spec = do describe "test buy transaction" testParseBuy describe "test basic parsing" testBasicParsing describe "test position parsing" testParsePosition + describe "test swedish decimal parsing" testSwedishDecimals testBasicParsing :: Spec testBasicParsing = do @@ -126,6 +132,21 @@ testParsePosition = do evaluate (P.parsePositionCsvData csv) `shouldThrowErrorContaining` "Missing: Kontonummer" +testSwedishDecimals :: Spec +testSwedishDecimals = do + it "parses comma-decimal rate/amount/courtage without truncating" $ do + -- Real Avanza exports format money with a decimal comma, e.g. "114,3". + -- The values below must survive in full rather than being cut at the comma. + let withBuyRow = + stringsToCsvByteString + [ csvHeader2025, + "2024-01-01;1111111;Köp;Company A;17;114,3;-1947,96;SEK;4,86;;SEK;some-isin;" + ] + let buySell = transformToBuySell . head $ P.parseCsvData withBuyRow + fmap getRate buySell `shouldBe` Just (Money 114.3) + fmap getAmount buySell `shouldBe` Just (Money (-1947.96)) + fmap getCourtage buySell `shouldBe` Just (Money 4.86) + positionHeader :: String positionHeader = "Kontonummer;Namn;Kortnamn;Volym;Marknadsvärde;GAV (SEK);GAV;Valuta;Land;ISIN;Marknad;Typ"