Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 2e580ba

Browse files
committed
Import: Add support for milliseconds in UTC timestamps
- Add support for more names for created UTC field - Ensure date / datetime parsing is correct
1 parent 8ef0bcf commit 2e580ba

File tree

7 files changed

+122
-25
lines changed

7 files changed

+122
-25
lines changed

stack.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ packages:
66
# - tasklite-app
77

88
extra-deps:
9+
- aeson-2.2.1.0
910
# TODO: Upgrade after 0.7.x includes missing megaparsec dependency
1011
- simple-sql-parser-0.6.0
1112

stack.yaml.lock

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44
# https://docs.haskellstack.org/en/stable/lock_files
55

66
packages:
7+
- completed:
8+
hackage: aeson-2.2.1.0@sha256:a23a61aada8233e10573e1612c0b2efe5a1aba0d59b05dbe2f63301822f136cb,6582
9+
pantry-tree:
10+
sha256: 8a50c54b9ecba80ecc3df3ea67faa4d155d6f6a6b3e342c74f3e6b0dcdc87e13
11+
size: 83518
12+
original:
13+
hackage: aeson-2.2.1.0
714
- completed:
815
hackage: simple-sql-parser-0.6.0@sha256:ce8f602fa81001287deb25af7b711fc45e1cdf36ff7702edde5dee2358f19a37,5580
916
pantry-tree:

tasklite-core/app/Main.hs

+4-13
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,7 @@ import Protolude qualified as P
5454
import Control.Monad.Catch (catchAll)
5555
import Data.Aeson as Aeson (KeyValue ((.=)), encode, object)
5656
import Data.FileEmbed (embedStringFile, makeRelativeToProject)
57-
import Data.Hourglass (
58-
DateTime,
59-
Time (timeFromElapsedP),
60-
TimeFormat (toFormat),
61-
timePrint,
62-
)
57+
import Data.Hourglass (DateTime, Time (timeFromElapsedP))
6358
import Data.String (fromString)
6459
import Data.Text qualified as T
6560
import Data.Text.Lazy qualified as TL
@@ -231,7 +226,7 @@ import Utils (
231226
TagText,
232227
executeHooks,
233228
parseUtc,
234-
ulidTextToDateTime,
229+
ulid2utc,
235230
)
236231

237232

@@ -1095,11 +1090,7 @@ executeCLiCommand :: Config -> DateTime -> Connection -> IO (Doc AnsiStyle)
10951090
executeCLiCommand conf now connection = do
10961091
let
10971092
addTaskC = addTask conf connection
1098-
prettyUlid ulid =
1099-
pretty $
1100-
fmap
1101-
(T.pack . timePrint (toFormat ("YYYY-MM-DD H:MI:S.ms" :: [Char])))
1102-
(ulidTextToDateTime ulid)
1093+
11031094
days3 = Iso.DurationDate (Iso.DurDateDay (Iso.DurDay 3) Nothing)
11041095

11051096
cliArgs <- execParser (parserInfo conf)
@@ -1195,7 +1186,7 @@ executeCLiCommand conf now connection = do
11951186
Help -> pure $ extendHelp $ parserHelp defaultPrefs $ cliArgsParser conf
11961187
PrintConfig -> pure $ pretty conf
11971188
Alias alias _ -> pure $ aliasWarning alias
1198-
UlidToUtc ulid -> pure $ prettyUlid ulid
1189+
UlidToUtc ulid -> pure $ pretty $ ulid2utc ulid
11991190
ExternalCommand cmd argsMb -> do
12001191
let
12011192
args =

tasklite-core/source/ImportExport.hs

+17-1
Original file line numberDiff line numberDiff line change
@@ -208,12 +208,28 @@ instance FromJSON ImportTask where
208208
utc <- o .:? "utc"
209209
entry <- o .:? "entry"
210210
creation <- o .:? "creation"
211+
creation_utc <- o .:? "creation_utc"
212+
creationUtc <- o .:? "creationUtc"
213+
created <- o .:? "created"
211214
created_at <- o .:? "created_at"
215+
createdAt <- o .:? "createdAt"
216+
created_utc <- o .:? "created_utc"
217+
createdUtc_ <- o .:? "createdUtc"
212218

213219
let
214220
parsedCreatedUtc =
215221
parseUtc
216-
=<< (utc <|> entry <|> creation <|> created_at)
222+
=<< ( utc
223+
<|> entry
224+
<|> creation
225+
<|> creation_utc
226+
<|> creationUtc
227+
<|> created
228+
<|> created_at
229+
<|> createdAt
230+
<|> created_utc
231+
<|> createdUtc_
232+
)
217233
createdUtc = fromMaybe zeroTime parsedCreatedUtc
218234

219235
o_body <- o .:? "body"

tasklite-core/source/Task.hs

+2-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import Protolude as P (
1919
Semigroup ((<>)),
2020
Show,
2121
decodeUtf8,
22-
encodeUtf8,
2322
fst,
2423
otherwise,
2524
show,
@@ -36,8 +35,8 @@ import Data.Aeson as Aeson (
3635
FromJSON,
3736
ToJSON,
3837
Value (Object),
39-
eitherDecode,
4038
encode,
39+
eitherDecodeStrictText,
4140
)
4241
import Data.Aeson.Key as Key (fromText)
4342
import Data.Aeson.KeyMap as KeyMap (fromList, insert)
@@ -341,7 +340,7 @@ instance Hashable Task
341340

342341
instance Sql.FromField.FromField Value where
343342
fromField aField@(Field (SQLText txt) _) =
344-
case Aeson.eitherDecode $ BSL.fromStrict $ encodeUtf8 txt of
343+
case eitherDecodeStrictText txt of
345344
Left error -> returnError ConversionFailed aField error
346345
Right value -> Ok value
347346
fromField f = returnError ConversionFailed f "expecting SQLText column type"

tasklite-core/source/Utils.hs

+25-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1+
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
2+
3+
{-# HLINT ignore "Use maybe" #-}
4+
15
{-|
26
Several utility functions (e.g for parsing & serializing UTC stamps)
37
-}
48
module Utils where
59

6-
import Protolude as P (
10+
import Protolude (
711
Alternative ((<|>)),
812
Applicative (pure),
913
Char,
1014
Double,
1115
Eq,
12-
Foldable (elem),
1316
Fractional (fromRational, (/)),
1417
Functor (fmap),
1518
IO,
@@ -41,6 +44,7 @@ import Protolude as P (
4144
(.),
4245
(<&>),
4346
)
47+
import Protolude qualified as P
4448

4549
import Control.Monad.Catch (catchAll)
4650
import Data.Colour.RGBSpace (RGB (..))
@@ -57,6 +61,7 @@ import Data.Hourglass (
5761
Timeable (timeGetElapsedP),
5862
timeGetDateTimeOfDay,
5963
timeParse,
64+
timePrint,
6065
)
6166
import Data.Text as T (
6267
Text,
@@ -88,6 +93,7 @@ import Config (
8893
Config (bodyStyle),
8994
Hook (body, filePath, interpreter),
9095
)
96+
import Control.Arrow ((>>>))
9197

9298

9399
type IdText = Text
@@ -138,6 +144,10 @@ parseUtc utcText =
138144
in
139145
-- From long (specific) to short (unspecific)
140146
timeParse ISO8601_DateAndTime utcString
147+
-- <|> tParse "YYYY-MM-DDtH:MI:S.ns"
148+
<|> tParse "YYYY-MM-DDtH:MI:S.msusns"
149+
<|> tParse "YYYY-MM-DDtH:MI:S.msus"
150+
<|> tParse "YYYY-MM-DDtH:MI:S.ms"
141151
<|> tParse "YYYY-MM-DDtH:MI:S"
142152
<|> tParse "YYYY-MM-DDtH:MI"
143153
<|> tParse "YYYYMMDDtHMIS"
@@ -167,7 +177,19 @@ parseUlidUtcSection encodedUtc = do
167177

168178
ulidTextToDateTime :: Text -> Maybe DateTime
169179
ulidTextToDateTime =
170-
parseUlidUtcSection . T.take 10
180+
T.take 10 >>> parseUlidUtcSection
181+
182+
183+
{-| `ulid2utc` converts a ULID to a UTC timestamp
184+
185+
>>> ulid2utc "01hq68smfe0r9entg3x4rb9441"
186+
Just "2024-02-21 16:43:17.358"
187+
-}
188+
ulid2utc :: Text -> Maybe Text
189+
ulid2utc ulid =
190+
fmap
191+
(T.pack . timePrint (toFormat ("YYYY-MM-DD H:MI:S.ms" :: [Char])))
192+
(ulidTextToDateTime ulid)
171193

172194

173195
parseUlidText :: Text -> Maybe ULID

tasklite-core/test/Spec.hs

+66-5
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,23 @@ import Protolude (
1111
($),
1212
(&),
1313
(/=),
14+
(<&>),
1415
(<>),
1516
)
1617
import Protolude qualified as P
1718

18-
import Data.Aeson (decode, eitherDecode)
19+
import Data.Aeson (decode, eitherDecode, eitherDecodeStrictText)
1920
import Data.Hourglass (
2021
DateTime,
2122
Elapsed (Elapsed),
2223
ElapsedP (ElapsedP),
2324
Time (timeFromElapsedP),
2425
timeGetDateTimeOfDay,
26+
timePrint,
27+
toFormat,
2528
)
26-
import Data.Text as T (unpack)
29+
import Data.Text (unpack)
30+
import Data.Text qualified as T
2731
import Database.SQLite.Simple (query_)
2832
import Database.SQLite.Simple qualified as Sql
2933
import Test.Hspec (
@@ -82,7 +86,7 @@ import TaskToNote (TaskToNote)
8286
import TaskToNote qualified
8387
import TaskToTag (TaskToTag)
8488
import TaskToTag qualified
85-
import Utils (parseUlidText, parseUlidUtcSection, parseUtc)
89+
import Utils (parseUlidText, parseUlidUtcSection, parseUtc, ulid2utc)
8690

8791

8892
withMemoryDb :: Config -> (Sql.Connection -> IO a) -> IO a
@@ -341,6 +345,43 @@ testSuite conf now = do
341345
runFilter conf now memConn [" "] `shouldThrow` (== ExitFailure 1)
342346

343347
describe "Import & Export" $ do
348+
it "parses any sensible datetime string" $ do
349+
-- TODO: Maybe keep microseconds and nanoseconds
350+
-- , ("YYYY-MM-DDTH:MI:S.msusZ", "2024-03-15T22:20:05.637913Z")
351+
-- , ("YYYY-MM-DDTH:MI:S.msusnsZ", "2024-03-15T22:20:05.637913438Z")
352+
353+
let dateMap :: [(Text, Text)] =
354+
[ ("YYYY-MM-DD", "2024-03-15")
355+
, ("YYYY-MM-DD H:MI", "2024-03-15 22:20")
356+
, ("YYYY-MM-DDTH:MIZ", "2024-03-15T22:20Z")
357+
, ("YYYY-MM-DD H:MI:S", "2024-03-15 22:20:05")
358+
, ("YYYY-MM-DDTH:MI:SZ", "2024-03-15T22:20:05Z")
359+
, ("YYYYMMDDTHMIS", "20240315T222005")
360+
, ("YYYY-MM-DDTH:MI:S.msZ", "2024-03-15T22:20:05.637Z")
361+
, ("YYYY-MM-DDTH:MI:S.msZ", "2024-03-15T22:20:05.637123Z")
362+
, ("YYYY-MM-DDTH:MI:S.msZ", "2024-03-15T22:20:05.637123456Z")
363+
]
364+
365+
P.forM_ dateMap $ \(formatTxt, utcTxt) -> do
366+
case parseUtc utcTxt of
367+
Nothing -> P.die "Invalid UTC string"
368+
Just utcStamp ->
369+
let timeFmt = formatTxt & T.unpack & toFormat
370+
in (utcStamp & timePrint timeFmt)
371+
`shouldBe` T.unpack
372+
( utcTxt
373+
& T.replace "123" ""
374+
& T.replace "456" ""
375+
)
376+
377+
let
378+
utcTxt = "2024-03-15T22:20:05.386777444Z"
379+
printFmt = "YYYY-MM-DDTH:MI:S.ms" & T.unpack & toFormat
380+
-- Truncates microseconds and nanoseconds
381+
expected = "2024-03-15T22:20:05.386"
382+
383+
(utcTxt & parseUtc <&> timePrint printFmt) `shouldBe` Just expected
384+
344385
it "imports a JSON task" $ do
345386
withMemoryDb conf $ \memConn -> do
346387
let jsonTask = "{\"body\":\"Just a test\", \"notes\":[\"A note\"]}"
@@ -363,8 +404,7 @@ testSuite conf now = do
363404
taskToNote `shouldSatisfy` (\task -> task.note == "A note")
364405
_ -> P.die "More than one task_to_note row found"
365406

366-
tasks :: [FullTask] <-
367-
query_ memConn "SELECT * FROM tasks_view"
407+
tasks :: [FullTask] <- query_ memConn "SELECT * FROM tasks_view"
368408

369409
case tasks of
370410
[updatedTask] -> do
@@ -385,6 +425,27 @@ testSuite conf now = do
385425
}
386426
_ -> P.die "More than one task found"
387427

428+
it "imports a JSON task with an ISO8601 created_at field" $ do
429+
withMemoryDb conf $ \memConn -> do
430+
let
431+
utc = "2024-03-15T10:32:51.386777444Z"
432+
-- ULID only has millisecond precision:
433+
utcFromUlid = "2024-03-15 10:32:51.387"
434+
jsonTask =
435+
"{\"body\":\"Just a test\",\"created_at\":\"{{utc}}\"}"
436+
& T.replace "{{utc}}" utc
437+
438+
case eitherDecodeStrictText jsonTask of
439+
Left error ->
440+
P.die $ "Error decoding JSON: " <> show error
441+
Right importTaskRecord -> do
442+
_ <- insertImportTask memConn importTaskRecord
443+
tasks :: [FullTask] <- query_ memConn "SELECT * FROM tasks_view"
444+
case tasks of
445+
[updatedTask] ->
446+
ulid2utc updatedTask.ulid `shouldBe` Just utcFromUlid
447+
_ -> P.die "More than one task found"
448+
388449

389450
main :: IO ()
390451
main = do

0 commit comments

Comments
 (0)