dataframe-json (empty) → 1.0.0.0
raw patch · 3 files changed
+192/−0 lines, 3 filesdep +aesondep +basedep +bytestring
Dependencies added: aeson, base, bytestring, containers, dataframe-core, dataframe-operations, scientific, text, vector
Files
- LICENSE +20/−0
- dataframe-json.cabal +39/−0
- src/DataFrame/IO/JSON.hs +133/−0
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2026 Michael Chavinda++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ dataframe-json.cabal view
@@ -0,0 +1,39 @@+cabal-version: 2.4+name: dataframe-json+version: 1.0.0.0++synopsis: JSON reader and writer for the dataframe ecosystem.+description:+ @DataFrame.IO.JSON@ — read/write arrays of JSON objects as+ 'DataFrame's. Built on @aeson@.++bug-reports: https://github.com/mchav/dataframe/issues+license: MIT+license-file: LICENSE+author: Michael Chavinda+maintainer: mschavinda@gmail.com+copyright: (c) 2024-2025 Michael Chavinda+category: Data+tested-with: GHC ==9.4.8 || ==9.6.7 || ==9.8.4 || ==9.10.3 || ==9.12.2++common warnings+ ghc-options:+ -Wincomplete-patterns+ -Wincomplete-uni-patterns+ -Wunused-imports+ -Wunused-local-binds++library+ import: warnings+ exposed-modules: DataFrame.IO.JSON+ build-depends: base >= 4 && < 5,+ aeson >= 0.11.0.0 && < 3,+ bytestring >= 0.11 && < 0.13,+ containers >= 0.6.7 && < 0.9,+ dataframe-core ^>= 1.0,+ dataframe-operations ^>= 1.0,+ scientific >= 0.3.1 && < 0.4,+ text >= 2.0 && < 3,+ vector ^>= 0.13+ hs-source-dirs: src+ default-language: Haskell2010
+ src/DataFrame/IO/JSON.hs view
@@ -0,0 +1,133 @@+{-# LANGUAGE TypeApplications #-}++module DataFrame.IO.JSON (+ readJSON,+ readJSONEither,+) where++import Control.Monad (forM)+import Data.Aeson+import qualified Data.Aeson.Key as K+import qualified Data.Aeson.KeyMap as KM+import qualified Data.ByteString.Lazy as LBS+import Data.Maybe (catMaybes)+import Data.Scientific (toRealFloat)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Vector as V++import qualified DataFrame.Internal.Column as D+import qualified DataFrame.Internal.DataFrame as D+import qualified DataFrame.Operations.Core as D++readJSONEither :: LBS.ByteString -> Either String D.DataFrame+readJSONEither bs = do+ v <- note "Could not decode JSON" (decode @Value bs)+ rows <- toArrayOfObjects v+ let cols :: [Text]+ cols =+ uniq+ . concatMap (map K.toText . KM.keys)+ . V.toList+ $ rows++ columns <- forM cols $ \c -> do+ let col = buildColumn rows c+ pure (c, col)++ pure $ D.fromNamedColumns columns++readJSON :: FilePath -> IO D.DataFrame+readJSON path = do+ contents <- LBS.readFile path+ case readJSONEither contents of+ Left err -> fail $ "readJSON: " <> err+ Right df -> pure df++toArrayOfObjects :: Value -> Either String (V.Vector Object)+toArrayOfObjects (Array xs)+ | V.null xs = Left "Top-level JSON array is empty"+ | otherwise = traverse asObject xs+toArrayOfObjects _ =+ Left "Top-level JSON value must be a JSON array of objects"++asObject :: Value -> Either String Object+asObject (Object o) = Right o+asObject _ = Left "Expected each element of the array to be an object"++uniq :: (Ord a) => [a] -> [a]+uniq = go mempty+ where+ go _ [] = []+ go seen (x : xs)+ | x `elem` seen = go seen xs+ | otherwise = x : go (x : seen) xs++note :: e -> Maybe a -> Either e a+note e = maybe (Left e) Right++data ColType+ = CTString+ | CTNumber+ | CTBool+ | CTArray+ | CTMixed++buildColumn :: V.Vector Object -> Text -> D.Column+buildColumn rows colName =+ let key = K.fromText colName+ values :: V.Vector (Maybe Value)+ values = V.map (KM.lookup key) rows+ colType = detectColType values+ in case colType of+ CTString ->+ D.fromVector (fmap (fmap asText) values)+ CTNumber ->+ D.fromVector (fmap (fmap asDouble) values)+ CTBool ->+ D.fromVector (fmap (fmap asBool) values)+ CTArray ->+ D.fromVector (fmap (fmap asArray) values)+ CTMixed ->+ D.fromVector values++detectColType :: V.Vector (Maybe Value) -> ColType+detectColType vals =+ case nonMissing of+ [] -> CTMixed+ vs+ | all isString vs -> CTString+ | all isNumber vs -> CTNumber+ | all isBool vs -> CTBool+ | all isArray vs -> CTArray+ | otherwise -> CTMixed+ where+ nonMissing = catMaybes (V.toList vals)++ isString (String _) = True+ isString _ = False++ isNumber (Number _) = True+ isNumber _ = False++ isBool (Bool _) = True+ isBool _ = False++ isArray (Array _) = True+ isArray _ = False++asText :: Value -> Text+asText (String s) = s+asText v = T.pack (show v)++asDouble :: Value -> Double+asDouble (Number s) = toRealFloat @Double s+asDouble v = error $ "asDouble: non-number value: " <> show v++asBool :: Value -> Bool+asBool (Bool b) = b+asBool v = error $ "asBool: non-bool value: " <> show v++asArray :: Value -> V.Vector Value+asArray (Array a) = a+asArray v = error $ "asArray: non-array value: " <> show v