commit 56029c7ceffa84a528fbdab8066a37fe35a57eba Author: chaos Date: Thu Nov 7 08:58:38 2024 +0000 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b469f3b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +export.json \ No newline at end of file diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..f111a9c --- /dev/null +++ b/run.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -eu + +SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)" +cd "$SCRIPT_DIR" + +${JQ:-jq} ${JQ_ARGS:-} -L "tool" -f tool/main.jq -Cr ${EXPORT_FILE:-export.json} --args -- "$@" \ No newline at end of file diff --git a/runTests.sh b/runTests.sh new file mode 100755 index 0000000..d67269a --- /dev/null +++ b/runTests.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -eu + +SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)" +cd "$SCRIPT_DIR/tool" + +jq -n -r -L . "include \"testLib\"; testLibMain" +jq -n -r -L . "include \"tests\"; testsMain" \ + --slurpfile "file:testdata/tests_export.json" testdata/tests_export.json \ No newline at end of file diff --git a/testdata/tests_export.json b/testdata/tests_export.json new file mode 100644 index 0000000..eae5d2c --- /dev/null +++ b/testdata/tests_export.json @@ -0,0 +1,205 @@ +{ + "substanceCompanions": [ + { + "substanceName": "Caffeine", + "color": "BLUE" + }, + { + "color": "BROWN", + "substanceName": "N-Acetylcysteine" + }, + { + "substanceName": "Custom", + "color": "CYAN" + } + ], + "customUnits": [ + { + "substanceName": "Caffeine", + "isEstimate": false, + "isArchived": false, + "creationDate": 1730892759000, + "administrationRoute": "ORAL", + "unit": "pill", + "originalUnit": "mg", + "id": 1251361475, + "estimatedDoseStandardDeviation": null, + "name": "Tablet", + "dose": 200, + "note": "" + } + ], + "experiences": [ + { + "isFavorite": false, + "text": "", + "creationDate": 1730892508000, + "title": "Experience 1", + "ratings": [], + "sortDate": 1730892507000, + "timedNotes": [], + "ingestions": [ + { + "estimatedDoseStandardDeviation": null, + "dose": 20, + "stomachFullness": null, + "administrationRoute": "INSUFFLATED", + "substanceName": "Caffeine", + "creationDate": 1730892508000, + "units": "mg", + "time": 1730892507000, + "isDoseAnEstimate": false, + "notes": "", + "consumerName": null, + "customUnitId": null + }, + { + "customUnitId": null, + "estimatedDoseStandardDeviation": null, + "time": 1730892510000, + "stomachFullness": null, + "substanceName": "Caffeine", + "dose": 20, + "notes": "", + "creationDate": 1730892511000, + "isDoseAnEstimate": true, + "units": "mg", + "consumerName": null, + "administrationRoute": "INSUFFLATED" + }, + { + "estimatedDoseStandardDeviation": null, + "dose": 100, + "units": "mg", + "creationDate": 1730892529000, + "isDoseAnEstimate": false, + "consumerName": null, + "notes": "", + "administrationRoute": "ORAL", + "time": 1730892528000, + "substanceName": "Caffeine", + "customUnitId": null, + "stomachFullness": "EMPTY" + }, + { + "substanceName": "Caffeine", + "notes": "", + "customUnitId": null, + "time": 1730892533000, + "isDoseAnEstimate": true, + "administrationRoute": "ORAL", + "units": "mg", + "estimatedDoseStandardDeviation": null, + "dose": 100, + "creationDate": 1730892534000, + "stomachFullness": "EMPTY", + "consumerName": null + }, + { + "estimatedDoseStandardDeviation": 20, + "isDoseAnEstimate": true, + "administrationRoute": "ORAL", + "dose": 100, + "time": 1730892549000, + "units": "mg", + "consumerName": null, + "creationDate": 1730892554000, + "notes": "", + "substanceName": "Caffeine", + "stomachFullness": "EMPTY", + "customUnitId": null + }, + { + "stomachFullness": "EMPTY", + "notes": "", + "dose": 250, + "isDoseAnEstimate": false, + "time": 1730892600000, + "administrationRoute": "ORAL", + "creationDate": 1730892603000, + "estimatedDoseStandardDeviation": null, + "units": "mg", + "consumerName": null, + "customUnitId": null, + "substanceName": "N-Acetylcysteine" + } + ], + "location": null + }, + { + "text": "", + "sortDate": 1730892605000, + "isFavorite": false, + "ingestions": [ + { + "stomachFullness": "EMPTY", + "dose": 250, + "isDoseAnEstimate": false, + "substanceName": "N-Acetylcysteine", + "notes": "", + "consumerName": null, + "customUnitId": null, + "creationDate": 1730892610000, + "estimatedDoseStandardDeviation": null, + "time": 1730892605000, + "units": "mg", + "administrationRoute": "ORAL" + }, + { + "substanceName": "Custom", + "stomachFullness": null, + "customUnitId": null, + "time": 1730892696000, + "isDoseAnEstimate": false, + "units": "mg", + "notes": "", + "consumerName": null, + "estimatedDoseStandardDeviation": null, + "administrationRoute": "SMOKED", + "creationDate": 1730892698000, + "dose": 100 + }, + { + "dose": null, + "estimatedDoseStandardDeviation": null, + "substanceName": "Custom", + "units": "mg", + "administrationRoute": "SMOKED", + "creationDate": 1730892704000, + "notes": "", + "consumerName": null, + "customUnitId": null, + "time": 1730892702000, + "isDoseAnEstimate": false, + "stomachFullness": null + }, + { + "stomachFullness": "EMPTY", + "time": 1730892770000, + "consumerName": null, + "dose": 1, + "isDoseAnEstimate": false, + "customUnitId": 1251361475, + "substanceName": "Caffeine", + "notes": "", + "estimatedDoseStandardDeviation": null, + "administrationRoute": "ORAL", + "units": "mg", + "creationDate": 1730892771000 + } + ], + "title": "Experience 2", + "creationDate": 1730892610000, + "location": null, + "ratings": [], + "timedNotes": [] + } + ], + "customSubstances": [ + { + "description": "", + "name": "Custom", + "units": "mg" + } + ] +} diff --git a/tool/args.jq b/tool/args.jq new file mode 100644 index 0000000..ee43e6f --- /dev/null +++ b/tool/args.jq @@ -0,0 +1,42 @@ +def parseArgBool: + . as $value | + if ($value | type) == "boolean" then + $value + elif + $value == "true" or $value == "on" or $value == "no" + then + true + elif + $value == "false" or $value == "off" or $value == "yes" + then + false + else + error("invalid bool value") + end; + +def parseArgs: + . as $args | + + reduce $args.positional[] as $arg ({ + shortArgs: [], + longArgs: {}, + nonArgs: [] + }; ( + if ($arg | test("^-[\\-a-zA-Z]") | not) then + .nonArgs += [$arg] + else + if $arg | startswith("--") then + $arg[2:] as $arg | + ($arg | contains("=")) as $containsValue | + if $containsValue then + ($arg | split("=")) as $argSplit | + $argSplit[0] as $arg | $argSplit[1] as $value | + .longArgs[$arg] |= $value + else + .longArgs[$arg] |= null + end + else + .shortArgs += [$arg[1:]] + end + end + )); \ No newline at end of file diff --git a/tool/journalUtils.jq b/tool/journalUtils.jq new file mode 100644 index 0000000..79f44bc --- /dev/null +++ b/tool/journalUtils.jq @@ -0,0 +1,126 @@ +include "utils"; + +def formatExperienceTitle: + . as $experience | + "\"\(.title)\": \(.creationDate / 1000 | strftime("%d-%m-%Y"))"; + +def calculateIngestionDose($customUnits): + . as $ingestion | + if .customUnitId != null then + ($customUnits | map(select(.id == $ingestion.customUnitId))[0]) as $customUnit | + .dose * $customUnit.dose | . as $dose | + $dose * 100 | round / 100 + else + .dose + end; + +def ingestionUnit($customUnits): + . as $ingestion | + if .customUnitId != null then + ($customUnits | map(select(.id == $ingestion.customUnitId))[0]) as $customUnit | + $customUnit.originalUnit + else + .units + end; + +def formatIngestionDose($customUnits): + . as $ingestion | + . | calculateIngestionDose($customUnits) as $dose | + . | ingestionUnit($customUnits) as $unit | + $customUnits | map(select(.id == $ingestion.customUnitId))[0] as $customUnit | + if $ingestion.dose == null then + "Unknown" + elif $customUnit == null then + "\($dose) \($unit)" + else + "\($dose) \($unit) (\($ingestion.dose) \($unit) * \($customUnit.dose) \($customUnit.unit))" + end; + +def formatIngestionTime: + . as $ingestion | + $ingestion.time / 1000 | strftime("%a %I:%M %p"); + +def formatIngestionROA($customUnits; $substitutions): + . as $ingestion | + $ingestion.administrationRoute as $roa | + + (if + $substitutions | has($roa) + then + $substitutions.[$roa] + else + $roa | titleCase + end) as $roaText | + + $ingestion.customUnitId as $customUnitId | + + if + $customUnitId == null + then + $roaText + else + $customUnits | map(select(.id == $customUnitId))[0] as $customUnit | + "\($roaText) (\($customUnit.name))" + end; +def formatIngestionROA($customUnits): formatIngestionROA($customUnits; {}); + +def filterIngestions($substanceFilter; $consumerFilter): + . as $ingestions | + $ingestions | if (($substanceFilter // [] | length) > 0) then + (reduce .[] as $ingestion ([]; + if + ([$substanceFilter[] | . == $ingestion.substanceName] | any) + and + ([$consumerFilter[] | . == ifNullDefault($ingestion.consumerName; "default")] | any) + then . += [$ingestion] + else . end) + ) + end; + +def ingestionsByConsumer: + . as $ingestions | + (reduce $ingestions[] as $ingestion ({}; + ifNullDefault($ingestion.consumerName; "default") as $consumerName | + if .[$consumerName] == null then .[$consumerName] |= [] end | + .[$consumerName] += [$ingestion] + )); + +def ingestionsSubstanceNames: + . as $ingestions | + [$ingestions[].substanceName] | orderedUnique; + +def ingestionsConsumerNames: + . as $ingestions | + [$ingestions[] as $ingestion | ifNullDefault($ingestion.consumerName; "default")] | orderedUnique; + +def experienceStats($customUnits): + . as $experience | + $experience.ingestions as $ingestions | + (reduce $ingestions[] as $ingestion ({}; . as $stats | + $ingestion | + .substanceName as $name | + .administrationRoute as $administrationRoute | + . | calculateIngestionDose($customUnits) as $dose | + . | ingestionUnit($customUnits) as $unit | + (.consumerName // "default") as $consumerName | + + $stats | + .[$consumerName].[$name].[$administrationRoute]|= + ($stats.[$consumerName].[$name].[$administrationRoute] // { + unit: "", + # null because null+null = null for ingestions with unknown dose which .dose is null + dose: null + }) | + .[$consumerName].[$name].[$administrationRoute].unit |= $unit | + .[$consumerName].[$name].[$administrationRoute].dose += $dose + )); + +def calculateCombinedDose($substanceName; $consumerName): + . as $stats | + ($stats.[$consumerName].[$substanceName] | [to_entries[] | .value.dose] | add)| . as $combinedDose | + ($stats.[$consumerName].[$substanceName] | to_entries[0] | .value.unit) as $combinedDoseUnit | + {dose: $combinedDose, unit: $combinedDoseUnit}; + +def experienceByTitle($name): + assert((. | type) == "array"; "experienceByTitle takes a array of experiences as input") | + map(select(.title == $name))[0]; \ No newline at end of file diff --git a/tool/main.jq b/tool/main.jq new file mode 100644 index 0000000..b8d924a --- /dev/null +++ b/tool/main.jq @@ -0,0 +1,391 @@ +# run as: jq export.json -f tool.jq -Cr --args -- + +include "utils"; +include "args"; +include "journalUtils"; + +def printExperienceStats($stats; $substanceFilter; $consumerFilter; $withTitle): + . as $experience | + + ($consumerFilter // ["default"]) as $consumerFilter | + + $experience.ingestions | filterIngestions($substanceFilter; $consumerFilter) as $ingestions | + ($ingestions | ingestionsByConsumer) as $ingestionsByConsumer | + + ($ingestionsByConsumer | keys) as $consumerNames | + + "" as $experienceStatsText | + $experienceStatsText | + if $withTitle then + . += ($experience | formatExperienceTitle | . + "\n") + end | . as $experienceStatsText | + + reduce $consumerNames[] as $consumerName ($experienceStatsText; + . as $experienceStatsText | + $experienceStatsText | + if ($consumerNames != ["default"]) + then . += "Consumer: \($consumerName)\n" + end | . as $experienceStatsText | + + ($stats.[$consumerName] | keys) as $substanceNames | + + + $experienceStatsText | reduce $substanceNames[] as $substanceName (.; + . as $experienceStatsText | + + ($stats.[$consumerName].[$substanceName] | keys) as $ingestionMethods | + + ($experienceStatsText | . += "Substance: \($substanceName)\n") as $experienceStatsText | + + reduce ($stats.[$consumerName].[$substanceName] | to_entries)[] as $substanceStats ($experienceStatsText; + . as $experienceStatsText | + + $substanceStats | + .key as $ingestionMethod | + ifNullDefault(.value.dose; "Unknown") as $dose | + .value.unit as $unit | + + ($experienceStatsText | . += "Dose (\($ingestionMethod | titleCase)): \($dose) \($unit)\n") as $experienceStatsText | + $experienceStatsText + ) | . as $experienceStatsText | + + $experienceStatsText | if ($ingestionMethods | length > 1) then + ($stats | calculateCombinedDose($substanceName; $consumerName)) as $combinedDose | + . += "Combined Dose: \(ifNullDefault($combinedDose.dose; "Unknown")) \($combinedDose.unit)\n" + end | . as $experienceStatsText | + + $experienceStatsText | . += "\n" + ) + ) | rtrimstr("\n\n"); + +def printExperienceLog($customUnits; $substanceFilter; $consumerFilter; $pretty; $withTitle): + . as $experience | + ($consumerFilter // ["default"]) as $consumerFilter | + + $experience.ingestions | sort_by(.sortDate) | filterIngestions($substanceFilter; $consumerFilter) as $ingestions | + + $ingestions | ingestionsConsumerNames as $consumerNames | + + if ($consumerNames == ["default"]) + then + ["Substance", "Dose", "ROA", "Consumer", "Time"] + else + ["Substance", "Dose", "ROA", "Time"] + end | . as $columnTitles | + + (reduce $ingestions[] as $ingestion ([]; . as $rows | + $ingestion | + .substanceName as $substanceName | + ifNullDefault(.consumerName; "default") as $consumerName | + formatIngestionDose($customUnits) as $doseText | + formatIngestionROA($customUnits) as $roaText | + formatIngestionTime as $timeText | + $rows | . += [ + if ($consumerNames != ["default"]) + then [$substanceName, $doseText, $roaText, $consumerName, $timeText] + else [$substanceName, $doseText, $roaText, $timeText] end + ] + )) as $rows | + + if $pretty then + printPrettyTable( + if $withTitle then ($experience | formatExperienceTitle) else null end; + $columnTitles; + $rows + ) + else + $rows | map(join(" | ")) | rtrimstr(" ") as $rows | + (if $withTitle then [$experience | formatExperienceTitle] else [] end) + $rows | join("\n") + end | . as $ingestionLog | + + $ingestionLog; + +def printExperiencesAdvanced($customUnits; $substanceFilter; $consumerFilter; $sortMethod; $sortOptions; $sortFilterString; sortFilter): + . as $experiences | + ($consumerFilter // ["default"]) as $consumerFilter | + $sortOptions | + # If filtering results by substances but no sortOptions.substanceName is defined, use the first one as a default + (.substanceName |= + if + $sortOptions.substanceName == null + and + (($substanceFilter | length) >= 1) + then + $substanceFilter[0] + else + null + end + ) | . as $sortOptions | + $sortOptions | (.ingestionMethod |= ($sortOptions.ingestionMethod // "ORAL")) | . as $sortOptions | + $sortOptions | (.consumerName |= ($sortOptions.consumerName // "default")) | . as $sortOptions | + + def sortFilterExperiences: + def oldToNewSort: .experience.sortDate; + def highestCombinedDoseSort: .stats.[$sortOptions.consumerName].[$sortOptions.substanceName] | [to_entries[] | .value.dose] | add; + def highestMethodDoseSort: .stats.[$sortOptions.consumerName].[$sortOptions.substanceName].[$sortOptions.ingestionMethod // "ORAL"].dose; + + def filterBySubstanceFilter: + . as $experiencesData | + (reduce $experiencesData[] as $experienceData ([]; + ($experienceData.experience.ingestions) as $ingestions | + . += + if ($experienceData.experience.ingestions | + any( + .substanceName as $substanceName | + $substanceFilter | + any(index($substanceName)) + ) + ) then [$experienceData] else [] end + )); + + def filterByConsumerFilter: + . as $experiencesData | + (reduce $experiencesData[] as $experienceData ([]; + ($experienceData.experience.ingestions) as $ingestions | + . += + if ($experienceData.experience.ingestions | + any( + ifNullDefault(.consumerName; "default") as $consumerName | + $consumerFilter | + any(index($consumerName)) + ) + ) then [$experienceData] else [] end + )); + + def filterBySubstanceAndConsumer: + . as $experiencesData | + (reduce $experiencesData[] as $experienceData ([]; + ($experienceData.experience.ingestions) as $ingestions | + . += + if + ($experienceData.experience.ingestions | + any( + .substanceName == $sortOptions.substanceName + and + ifNullDefault(.consumerName; "default") == $sortOptions.consumerName + )) + then [$experienceData] else [] end + )); + + def filterByIngestionMethodForSubstance: + . as $experiencesData | filterBySubstanceAndConsumer as $experiencesData | + (reduce $experiencesData[] as $experienceData ([]; + ($experienceData.experience.ingestions) as $ingestions | + . += if ($experienceData.experience.ingestions | any(.substanceName == $sortOptions.substanceName and .administrationRoute == $sortOptions.ingestionMethod)) + then [$experienceData] else [] end + )); + + # speeds up by excluding everything not containing substances & consumers not in filters, wouldn't show any data anyway + if $substanceFilter != null then filterBySubstanceFilter end | + filterByConsumerFilter | + + if + $sortMethod == "old-to-new" or $sortMethod == null + then + sort_by(oldToNewSort) + elif + $sortMethod == "highest-combined-dose" + then + assert($sortOptions.substanceName != null; "substanceName not provided as sortOption") | + filterBySubstanceAndConsumer | + sort_by(highestCombinedDoseSort, oldToNewSort) | + reverse + elif + $sortMethod == "highest-dose-for-method" + then + assert($sortOptions.substanceName != null; "substanceName not provided as sortOption") | + filterByIngestionMethodForSubstance | + sort_by(highestMethodDoseSort, oldToNewSort) | + reverse + end; + + def sortFilterFromString($filterString): + (reduce ($filterString | split("|"))[] as $filter (.; + if + $filter == "reverse" then . | reverse + elif ($filter | startswith("firstN")) then + ($filter | ltrimstr("firstN(") | rtrimstr(")")) as $arg | + . | firstN($arg | try fromjson catch error("invalid number passed to firstN")) + end + )); + + def experiencesWithExtraData($customUnits): + . as $experiences | + (reduce $experiences[] as $experience ([]; + . += [{ + stats: $experience | experienceStats($customUnits), + $experience + }] + )); + + $experiences | + experiencesWithExtraData($customUnits) | + sortFilterExperiences | + if ($sortFilterString != null) then . | sortFilterFromString($sortFilterString) end | + if (sortFilter != null) then . | sortFilter end | + .[] as $entry | + $entry.experience as $experience | + $entry.stats as $stats | + ($experience | printExperienceLog( + $customUnits; + $substanceFilter; + $consumerFilter; + true; + true + )) + + "\nCumulative Doses:\n" + + ($experience | printExperienceStats( + $stats; + $substanceFilter; + $consumerFilter; + false + )) + "\n"; + + +def main($ARGS): + def usage: + [ + "psychonaut_journal_stats {printExperience,printExperiencesAdvanced}", + "" + ] | join("\n") | halt_error(1); + + . as $exportData | + + ($ARGS | parseArgs) as $parsedArgs | + + $parsedArgs.nonArgs[0] as $program | + ($parsedArgs | .nonArgs |= $parsedArgs.nonArgs[1:]) as $parsedArgs | + + if $program == null then + if any($parsedArgs.shortArgs[]; . == "h") then usage end | + if ($parsedArgs.longArgs | has("help")) then usage end | + + usage + elif $program == "printExperience" then + def printExperienceUsage($reason): + [ + $reason, + "Usage: printExperience [experienceTitle] --pretty=bool --title=bool --stats=bool --substance-filter=[substanceNames,] --consumer-filter=[consumerNames,]", + "" + ] | map(select(. != null)) | join("\n") | halt_error(1); + + if any($parsedArgs.shortArgs[]; . == "h") then printExperienceUsage(null) end | + if ($parsedArgs.longArgs | has("help")) then printExperienceUsage(null) end | + + $parsedArgs.nonArgs[0] as $experienceTitle | + if $experienceTitle == null then printExperienceUsage("experienceTitle not provided") end | + + { + substanceFilter: null, + consumerFilter: null, + pretty: true, + withTitle: true, + withStats: true, + } as $defaultOptions | + $defaultOptions as $options | + + reduce ($parsedArgs.longArgs | to_entries[]) as $longArg ($options; ( + $longArg.key as $arg | + $longArg.value as $value | + + if $arg == "pretty" then + .pretty |= (ifNullDefault($value; $defaultOptions.pretty) | parseArgBool) + end | + if $arg == "title" then + .withTitle |= (ifNullDefault($value; $defaultOptions.withTitle) | parseArgBool) + end | + if $arg == "stats" then + .withStats |= (ifNullDefault($value; $defaultOptions.withStats) | parseArgBool) + end | + if $arg == "substance-filter" then + .substanceFilter |= ($parsedArgs.longArgs.["substance-filter"] | split(",")) + end | + if $arg == "consumer-filter" then + .consumerFilter |= ($parsedArgs.longArgs.["consumer-filter"] | split(",")) + end + )) | . as $options | + + $exportData.experiences | experienceByTitle($experienceTitle) as $experience | + if $experience == null then error("Experience not found") end | + + ($experience | printExperienceLog( + $exportData.customUnits; + $options.substanceFilter; + $options.consumerFilter; + $options.pretty; + $options.withTitle + )) as $ingestionLog | + + if $options.withStats then + ($experience | experienceStats($exportData.customUnits)) as $stats | + $ingestionLog + + "\n\nCumulative Doses:\n" + + ($experience | printExperienceStats( + $stats; + $options.substanceFilter; + $options.consumerFilter; + false + )) + else + $ingestionLog + end + elif $program == "printExperiencesAdvanced" then + def printExperiencesAdvancedUsage($reason): + [ + $reason, + "Usage: printExperiencesAdvanced --substance-filter=[substanceNames,] --consumer-filter=[consumerNames,] --sort-method={old-to-new,highest-combined-dose,highest-dose-for-method} --sort-option-substance-name=[string] --sort-options-ingestion-method=[string] --sort-options-consumer-name=[string] --sort-filter={firstN(x),reverse}", + "" + ] | map(select(. != null)) | join("\n") | halt_error(1); + + if any($parsedArgs.shortArgs[]; . == "h") then printExperiencesAdvancedUsage(null) end | + if ($parsedArgs.longArgs | has("help")) then printExperiencesAdvancedUsage(null) end | + + { + substanceFilter: null, + consumerFilter: null, + sortMethod: "old-to-new", + sortOptions: {}, + sortFilter: null + } as $defaultOptions | + $defaultOptions as $options | + + reduce ($parsedArgs.longArgs | to_entries[]) as $longArg ($options; ( + $longArg.key as $arg | + $longArg.value as $value | + if $arg == "substance-filter" then + .substanceFilter |= ($parsedArgs.longArgs.["substance-filter"] | split(",")) + end | + if $arg == "consumer-filter" then + .consumerFilter |= ($parsedArgs.longArgs.["consumer-filter"] | split(",")) + end | + if $arg == "sort-method" then + .sortMethod |= $parsedArgs.longArgs.["sort-method"] + end | + if $arg == "sort-option-substance-name" then + .sortOptions.substanceName |= $parsedArgs.longArgs.["sort-option-substance-name"] + end | + if $arg == "sort-option-ingestion-method" then + .sortOptions.ingestionMethod |= $parsedArgs.longArgs.["sort-option-ingestion-method"] + end | + if $arg == "sort-option-consumer-name" then + .sortOptions.consumerName |= $parsedArgs.longArgs.["sort-option-consumer-name"] + end | + if $arg == "sort-filter" then + .sortFilter |= $parsedArgs.longArgs.["sort-filter"] + end + )) | . as $options | + + $exportData.experiences | + printExperiencesAdvanced( + $exportData.customUnits; + $options.substanceFilter; + $options.consumerFilter; + $options.sortMethod; + $options.sortOptions; + $options.sortFilter; null + ) + else + usage + end; + +main($ARGS) \ No newline at end of file diff --git a/tool/testLib.jq b/tool/testLib.jq new file mode 100644 index 0000000..7d928e1 --- /dev/null +++ b/tool/testLib.jq @@ -0,0 +1,127 @@ +def printTestResult: + . as $result | $result | + if .passed then + "Test \"\(.name)\" Passed" + else + (if (.loc != null) then " at \(.loc.file):\(.loc.line)" else "" end) as $fileLocation | + "Test \"\(.name)\"\($fileLocation) Failed\nReason: \(.reason)\nOutput: \(.output | tojson)" + end; + +def runTest($loc; $name; testExpr; checkResult; $expectError): + try ( + (null | testExpr) as $output | + if $expectError then + { + $name, + passed: false, + reason: "Expected error but no error was raised", + $output, + $loc + } + elif ($output | checkResult) then + { + $name, + passed: true, + $output, + $loc + } + else + { + $name, + passed: false, + reason: "Result was different from expected", + $output, + $loc + } + end + ) catch ( + . as $error | + if ($expectError | not) then + { + $name, + passed: false, + reason: "Error caught when no error was expected", + output: $error, + $loc + } + elif ($expectError and ($error | checkResult)) then + { + $name, + passed: true, + output: $error, + $loc + } + elif ($expectError and ($error | checkResult | not)) then + { + $name, + passed: false, + reason: "Expected error but received different error", + output: $error, + $loc + } + else + error("unknown error") + end + ); + +def expectPassed: + if (.passed | not) then + error(. | printTestResult) + end; +def expectPassed($result): $result | expectPassed; + +def expectFailed: + if (.passed) then + error(. | printTestResult) + end; +def expectFailed($result): $result | expectFailed; + +def testTests: + expectPassed(runTest( + $__loc__; + "passing test"; + true; + . == true; + false + )) | + expectPassed(runTest( + $__loc__; + "error expected and equal"; + error("error"); + . == "error"; + true + )) | + expectFailed(runTest( + $__loc__; + "failing test"; + true; + . == false; + false + )) | + expectFailed(runTest( + $__loc__; + "error expected but no error"; + null; + . == null; + true + )) | + expectFailed(runTest( + $__loc__; + "error not expected"; + error("error"); + . == null; + false + )) | + expectFailed(runTest( + $__loc__; + "error different"; + error("error"); + . == "different error"; + true + )); + +def testLibMain: + testTests | + empty | + halt_error(0); + diff --git a/tool/tests.jq b/tool/tests.jq new file mode 100644 index 0000000..887415a --- /dev/null +++ b/tool/tests.jq @@ -0,0 +1,17 @@ +#import "./testdata/tests_export" as $exportData; +include "journalUtils"; +include "testLib"; + +def testsMain: + $ARGS.named["file:testdata/tests_export.json"][0] as $exportData | + + expectPassed(runTest( + $__loc__; + "invalid input to experienceByTitle"; + ( + $exportData.experiences | + experienceByTitle("Test") + ); + . == "error"; + false + )); diff --git a/tool/utils.jq b/tool/utils.jq new file mode 100644 index 0000000..2791dfe --- /dev/null +++ b/tool/utils.jq @@ -0,0 +1,121 @@ +def debugLog($target; $value): + if + ($ENV["JQ_DEBUG"] == $target) + or + ($target == "*") + or + ($ENV["JQ_DEBUG"] | split(",") | any(. as $debugTarget | $target | startswith($debugTarget | split("*")[0]))) + then + debug({$target, $value}) + end; + +def assert(cond; $msg): if cond then . else error($msg) end; +def assert($loc; cond; $msg): assert(cond; "\($loc.file):\($loc.line): " + $msg); + +def titleCase: + [splits("\\b") | select(length>0)] + | map((.[:1]|ascii_upcase) + (.[1:] |ascii_downcase)) + | join(""); + +def lpad(string;len;fill): + if len == 0 then string else (fill * len)[0:len] + string end; + +def rpad(string;len;fill): + if len == 0 then string else string + (fill * len)[0:len] end; + +def firstN($n): .[:$n]; + +# only use when // does not suffice +def ifNullDefault($value; $default): + if $value != null then + $value + else + $default + end; + +def orderedUnique: + (reduce .[] as $value ([]; + if + (. | any(index($value)) | not) + then . += [$value] + else . end + )); + +def printPrettyTable($title; $columnTitles; $rows): + if [$rows[] | length] | unique | length > 1 then error("non-even number of columns") end | + $title | if . == null then "" end | . as $title | + + " | " as $columnSeperator | + ([$rows[] | length] | unique[0] // 0) as $columns | + [$rows[] as $columns | [$columns[] | length]] as $columnLengths | + + (reduce $columnLengths[] as $lengths ([range($columns) | 0]; + [range($columns) as $columnNumber | [.[$columnNumber], $lengths[$columnNumber]] | max] + )) as $maxColumnLengths | + + (reduce $columnLengths[] as $lengths ($maxColumnLengths; + [range($columns) as $columnNumber | [.[$columnNumber], ($columnTitles[$columnNumber] | length)] | max] + )) as $maxColumnLengths | + + (reduce range($columns) as $columnNumber ([]; + $columnTitles[$columnNumber] as $title | + . += [ + if $columnNumber == 0 then + lpad($title; $maxColumnLengths[$columnNumber] - ($title | length) ; " ") + else + rpad($title; $maxColumnLengths[$columnNumber] - ($title | length) ; " ") + end + ] + )) | join($columnSeperator) as $columnTitleRow | + + (reduce $rows[] as $row ([]; + . += [(reduce range($columns) as $columnNumber ([]; . += [ + $row[$columnNumber] as $column | + + if $columnNumber == 0 then + lpad($column; $maxColumnLengths[$columnNumber] - ($column | length) ; " ") + else + rpad($column; $maxColumnLengths[$columnNumber] - ($column | length) ; " ") + end + ]))] + )) as $rows | + + $title | length as $titleLength | + + ((($columns - 1) * ($columnSeperator | length)) + ($maxColumnLengths | add)) as $maxRowLength | + [ + $maxRowLength, + $titleLength + ] | max as $maxLength | + + # Pad title to be centered to maximum row length + $title | if $titleLength < $maxRowLength then + lpad($title; ($maxLength - $titleLength) / 2; " ") + end | . as $title | + + # if title is longer than maximum row length, pad the first row the difference between them + $rows | if $titleLength > $maxRowLength then + (reduce .[] as $row ([]; + . += [ + [ + lpad($row[0]; $titleLength - $maxRowLength; " ") + ] + $row[1:] + ] + )) + end | . as $rows | + + ($rows | map(join($columnSeperator))) as $rows | + if $title != null then + [ + $title, + ([range($maxLength) | "-"] | join("")), + $columnTitleRow, + ([range($maxLength) | "-"] | join("")), + $rows + ] | flatten | join("\n") + "\n" + else + [ + $columnTitleRow, + $rows + ] | flatten | join("\n") + "\n" + end; \ No newline at end of file