diff --git a/run.sh b/run.sh index 84ff932..090f198 100755 --- a/run.sh +++ b/run.sh @@ -9,7 +9,7 @@ JQ=${JQ:-jq} export JQ_FLAVOR=${JQ_FLAVOR:-${JQ}} run() { - ${JQ} -rn -L "$(realpath .)" -L "$(realpath lib)" -L "$(realpath dropins)/${JQ_FLAVOR}" \ + ${JQ} -nr -L "$(realpath .)" -L "$(realpath ./lib)" -L "$(realpath ./dropins)/${JQ_FLAVOR}" \ --slurpfile exportFile "${EXPORT_FILE:-export.json}" \ 'include "main"; main' \ --args -- "$@" diff --git a/tool/journalLib.jq b/tool/journalLib.jq new file mode 100644 index 0000000..e893fc8 --- /dev/null +++ b/tool/journalLib.jq @@ -0,0 +1,203 @@ +import "utils" as utils; + +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 ingestionConsumerName: + . as $ingestion | + $ingestion.consumerName // "default"; + +def filterIngestions($substanceFilter; $consumerFilter): + . as $ingestions | + if (($substanceFilter // [] | length) > 0) then + [ + $ingestions[] as $ingestion | + if + ([$substanceFilter[] | . == $ingestion.substanceName] | any) + and + ([$consumerFilter[] | . == ($ingestion | ingestionConsumerName)] | any) + then $ingestion + else null end + ] | map(select(. != null)) + else $ingestions end; + +def ingestionsSubstanceNames: + . as $ingestions | + [$ingestions[].substanceName] | utils::orderedUnique; + +def ingestionsConsumerNames: + . as $ingestions | + [$ingestions[] | ingestionConsumerName] | utils::orderedUnique; + +def ingestionsByConsumer: + . as $ingestions | + ($ingestions | ingestionsConsumerNames) as $consumerNames | + [ + $consumerNames[] as $consumerName | + { + key: $consumerName, + value: [$ingestions | map(select( + . | ingestionConsumerName == $consumerName + ))], + } + ] | from_entries; + +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 | + . | ingestionConsumerName 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 statsCalculateCombinedDose($substanceName; $consumerName): + . as $stats | + (.[$consumerName].[$substanceName] | [to_entries[] | .value.dose] | add) as $combinedDose | + (.[$consumerName].[$substanceName] | to_entries[0] | .value.unit) as $combinedDoseUnit | + {dose: $combinedDose, unit: $combinedDoseUnit}; + +def experiencesWithExtraData($customUnits): + . as $experiences | + (reduce $experiences[] as $experience ([]; + ($experience | experienceStats($customUnits)) as $stats | + . += [{ + $stats, + $experience + }] + )); + +def filterSortExperiences($customUnits; $substanceFilter; $consumerFilter; $sortMethod; $sortOptions): + . 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 sortFilter: + 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 | + [ + $experiencesData[] as $experienceData | + ($experienceData.experience.ingestions) as $ingestions | + if ($experienceData.experience.ingestions | + any( + . as $ingestion | + $substanceFilter | + any(index($ingestion.substanceName)) + ) + ) then $experienceData else null end + ] | map(select(. != null)); + + def filterByConsumerFilter: + . as $experiencesData | + (reduce $experiencesData[] as $experienceData ([]; + ($experienceData.experience.ingestions) as $ingestions | + . += + if ($experienceData.experience.ingestions | + any( + (. | ingestionConsumerName) 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 + (. | ingestionConsumerName) == $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 + utils::assert($sortOptions.substanceName != null; "substanceName not provided as sortOption") | + filterBySubstanceAndConsumer | + sort_by(highestCombinedDoseSort, oldToNewSort) | + reverse + elif + $sortMethod == "highest-dose-for-method" + then + utils::assert($sortOptions.substanceName != null; "substanceName not provided as sortOption") | + filterByIngestionMethodForSubstance | + sort_by(highestMethodDoseSort, oldToNewSort) | + reverse + end; + + $experiences | + experiencesWithExtraData($customUnits) | + sortFilter; + +def experienceByTitle($name): + utils::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/journalUtils.jq b/tool/journalUtils.jq index 6418202..02c8ceb 100644 --- a/tool/journalUtils.jq +++ b/tool/journalUtils.jq @@ -1,33 +1,18 @@ include "dropins"; include "utils"; +import "lib/stringLib" as stringLib; + +import "journalLib" as journalLib; + 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 | + . | journalLib::calculateIngestionDose($customUnits) as $dose | + . | journalLib::ingestionUnit($customUnits) as $unit | $customUnits | map(select(.id == $ingestion.customUnitId))[0] as $customUnit | if $ingestion.dose == null then "Unknown" @@ -50,7 +35,7 @@ def formatIngestionROA($customUnits; $substitutions): then $substitutions | .[$roa] else - $roa | titleCase + $roa | stringLib::titleCase end) as $roaText | $ingestion.customUnitId as $customUnitId | @@ -63,64 +48,5 @@ def formatIngestionROA($customUnits; $substitutions): $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 | - (.[$consumerName].[$substanceName] | [to_entries[] | .value.dose] | add) as $combinedDose | - (.[$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 +def formatIngestionROA($customUnits): formatIngestionROA($customUnits; {}); \ No newline at end of file diff --git a/tool/lib/stringLib.jq b/tool/lib/stringLib.jq new file mode 100644 index 0000000..d18526e --- /dev/null +++ b/tool/lib/stringLib.jq @@ -0,0 +1,10 @@ +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; \ No newline at end of file diff --git a/tool/lib/tableLib.jq b/tool/lib/tableLib.jq new file mode 100644 index 0000000..a1f9aad --- /dev/null +++ b/tool/lib/tableLib.jq @@ -0,0 +1,84 @@ +import "stringLib" as stringLib; + +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 $numColumnseperator | + + def padRow($numColumns; $maxColumnLengths): + . as $row | + [ + range($numColumns) as $columnNumber | + $row[$columnNumber] as $content | + if $columnNumber == 0 then + stringLib::lpad($content; $maxColumnLengths[$columnNumber] - ($content | length) ; " ") + else + stringLib::rpad($content; $maxColumnLengths[$columnNumber] - ($content | length) ; " ") + end + ]; + + ([$rows[] | length] | unique[0]) as $numColumns | + ([$columnTitles | length] | unique[0]) as $numColumnTitles | + if $numColumns != $numColumnTitles then error("unequal title rows and columns") end | + + [$columnTitles[] | length] as $maxColumnLengths | + (reduce $rows[] as $column ($maxColumnLengths; + [ + range($numColumns) as $columnNumber | + [ + .[$columnNumber], + ($column[$columnNumber] | length) + ] | max + ] + )) as $maxColumnLengths | + + $columnTitles | + padRow($numColumns; $maxColumnLengths) | + join($numColumnseperator) as $columnTitleRow | + + [$rows[] | padRow($numColumns; $maxColumnLengths)] as $rows | + + $title | length as $titleLength | + + ((($numColumns - 1) * ($numColumnseperator | length)) + ($maxColumnLengths | add)) as $maxRowLength | + [ + $maxRowLength, + $titleLength + ] | max as $maxLength | + + + # Pad title to be centered to maximum row length + $title | if $titleLength < $maxRowLength then + stringLib::lpad($title; ($maxLength - $titleLength) / 2; " ") + end | . as $title | + + # if title is longer than maximum row length, pad the first row the difference between them + if $titleLength > $maxRowLength then + [ + $rows[] as $row | + [ + stringLib::lpad($row[0]; $titleLength - $maxRowLength; " ") + ] + $row[1:] + ] + else $rows end | . as $rows | + + ($rows | map(join($numColumnseperator))) as $rows | + if $title != "" then + + [ + $title, + ([range($maxLength) | "-"] | join("")), + + $columnTitleRow, + ([range($maxLength) | "-"] | join("")), + $rows + ] | flatten | join("\n") + "\n" + else + + [ + $columnTitleRow, + $rows + ] | flatten | join("\n") + "\n" + end; diff --git a/tool/lib/typeLib.jq b/tool/lib/typeLib.jq index 9fa3741..4776d01 100644 --- a/tool/lib/typeLib.jq +++ b/tool/lib/typeLib.jq @@ -57,6 +57,8 @@ def ensureNullOr(ensureType): def ensureKey($type; $key): if (. | has($key) | not) then typeError("\($type):\($key)") end; def ensureKey($value; $type; $key): $value | ensureKey($type; $key); +def ensureIfKey($key; ensureType): if (. | has($key)) then .[$key] | ensureType end; + def ensureWrapError($newType; ensureType): . as $value | try (. | ensureType) @@ -69,7 +71,6 @@ def ensureWrapError($newType; ensureType): def ensureWrapError($value; $newType; ensureType): $value | ensureWrapError($newType; ensureType); - # JQ_TYPECHECKING=true required for tests def typeLibTests: testLib::expectPassed(testLib::runTest( diff --git a/tool/main.jq b/tool/main.jq index c3726b8..9827e66 100644 --- a/tool/main.jq +++ b/tool/main.jq @@ -2,25 +2,30 @@ include "dropins"; import "lib/typeLib" as typeLib; import "lib/argsLib" as argsLib; +import "lib/stringLib" as stringLib; +import "lib/tableLib" as tableLib; -include "utils"; -include "journalUtils"; +import "journalLib" as journalLib; +import "journalUtils" as journalUtils; import "types" as types; +import "utils" as utils; + def printExperienceStats($stats; $substanceFilter; $consumerFilter; $withTitle): . as $experience | ($consumerFilter // ["default"]) as $consumerFilter | - $experience.ingestions | filterIngestions($substanceFilter; $consumerFilter) as $ingestions | - ($ingestions | ingestionsByConsumer) as $ingestionsByConsumer | + $experience.ingestions | + journalLib::filterIngestions($substanceFilter; $consumerFilter) as $ingestions | + ($ingestions | journalLib::ingestionsByConsumer) as $ingestionsByConsumer | ($ingestionsByConsumer | keys) as $consumerNames | "" as $experienceStatsText | $experienceStatsText | if $withTitle then - . += ($experience | formatExperienceTitle | . + "\n") + . += ($experience | journalUtils::formatExperienceTitle | . + "\n") end | . as $experienceStatsText | reduce $consumerNames[] as $consumerName ($experienceStatsText; @@ -45,16 +50,16 @@ def printExperienceStats($stats; $substanceFilter; $consumerFilter; $withTitle): $substanceStats | .key as $ingestionMethod | - ifNullDefault(.value.dose; "Unknown") as $dose | + (.value.dose // "Unknown") as $dose | .value.unit as $unit | - ($experienceStatsText | . += "Dose (\($ingestionMethod | titleCase)): \($dose) \($unit)\n") as $experienceStatsText | + ($experienceStatsText | . += "Dose (\($ingestionMethod | stringLib::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" + ($stats | journalLib::statsCalculateCombinedDose($substanceName; $consumerName)) as $combinedDose | + . += "Combined Dose: \($combinedDose.dose // "Unknown") \($combinedDose.unit)\n" end | . as $experienceStatsText | $experienceStatsText | . += "\n" @@ -65,167 +70,41 @@ def printExperienceLog($customUnits; $substanceFilter; $consumerFilter; $pretty; . as $experience | ($consumerFilter // ["default"]) as $consumerFilter | - $experience.ingestions | sort_by(.sortDate) | filterIngestions($substanceFilter; $consumerFilter) as $ingestions | + $experience.ingestions | + sort_by(.sortDate) | + journalLib::filterIngestions($substanceFilter; $consumerFilter) as $ingestions | - $ingestions | ingestionsConsumerNames as $consumerNames | + $ingestions | journalLib::ingestionsConsumerNames as $consumerNames | - if ($consumerNames == ["default"]) - then + 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 | . += [ + [ + $ingestions[] as $ingestion | + $ingestion | + .substanceName as $substanceName | + journalLib::ingestionConsumerName as $consumerName | + journalUtils::formatIngestionDose($customUnits) as $doseText | + journalUtils::formatIngestionROA($customUnits) as $roaText | + journalUtils::formatIngestionTime as $timeText | if ($consumerNames != ["default"]) then [$substanceName, $doseText, $roaText, $consumerName, $timeText] else [$substanceName, $doseText, $roaText, $timeText] end - ] - )) as $rows | + ] 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; + tableLib::printPrettyTable( + if $withTitle then ($experience | journalUtils::formatExperienceTitle) else null end; + $columnTitles; + $rows + ); -def printExperiencesAdvanced($customUnits; $substanceFilter; $consumerFilter; $sortMethod; $sortOptions; $sortFilterString; sortFilter): +def printExperiences($customUnits; $substanceFilter; $consumerFilter): . 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 | @@ -248,7 +127,7 @@ def printExperiencesAdvanced($customUnits; $substanceFilter; $consumerFilter; $s def main: def usage: [ - "psychonaut_journal_stats {printExperience,printExperiencesAdvanced}", + "psychonaut_journal_stats {printExperience,printExperiences}", "" ] | join("\n") | halt_error(1); @@ -269,7 +148,7 @@ def main: def printExperienceUsage($reason): [ $reason, - "Usage: printExperience [experienceTitle] --pretty=bool --title=bool --stats=bool --substance-filter=[substanceNames,] --consumer-filter=[consumerNames,]", + "Usage: printExperience [experienceTitle] --title=bool --stats=bool --substance-filter=[substanceNames,] --consumer-filter=[consumerNames,]", "" ] | map(select(. != null)) | join("\n") | halt_error(1); @@ -282,7 +161,6 @@ def main: { substanceFilter: null, consumerFilter: null, - pretty: true, withTitle: true, withStats: true, } as $defaultOptions | @@ -292,14 +170,11 @@ def main: $longArg.key as $arg | $longArg.value as $value | - if $arg == "pretty" then - .pretty |= (ifNullDefault($value; $defaultOptions.pretty) | argsLib::parseArgBool) - end | if $arg == "title" then - .withTitle |= (ifNullDefault($value; $defaultOptions.withTitle) | argsLib::parseArgBool) + .withTitle |= ($value // $defaultOptions.withTitle | argsLib::parseArgBool) end | if $arg == "stats" then - .withStats |= (ifNullDefault($value; $defaultOptions.withStats) | argsLib::parseArgBool) + .withStats |= ($value // $defaultOptions.withStats | argsLib::parseArgBool) end | if $arg == "substance-filter" then .substanceFilter |= ($parsedArgs.longArgs.["substance-filter"] | split(",")) @@ -309,7 +184,7 @@ def main: end )) | . as $options | - $exportData.experiences | experienceByTitle($experienceTitle) as $experience | + $exportData.experiences | journalLib::experienceByTitle($experienceTitle) as $experience | if $experience == null then error("Experience not found") end | ($experience | printExperienceLog( @@ -321,7 +196,7 @@ def main: )) as $ingestionLog | if $options.withStats then - ($experience | experienceStats($exportData.customUnits)) as $stats | + ($experience | journalLib::experienceStats($exportData.customUnits)) as $stats | $ingestionLog + "\n\nCumulative Doses:\n" + ($experience | printExperienceStats( @@ -333,16 +208,16 @@ def main: else $ingestionLog end - elif $program == "printExperiencesAdvanced" then - def printExperiencesAdvancedUsage($reason): + elif $program == "printExperiences" then + def printExperiencesUsage($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}", + "Usage: printExperiences --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 | + if any($parsedArgs.shortArgs[]; . == "h") then printExperiencesUsage(null) end | + if ($parsedArgs.longArgs | has("help")) then printExperiencesUsage(null) end | { substanceFilter: null, @@ -380,13 +255,18 @@ def main: )) | . as $options | $exportData.experiences | - printExperiencesAdvanced( + journalLib::filterSortExperiences( $exportData.customUnits; $options.substanceFilter; $options.consumerFilter; - $options.sortMethod; - $options.sortOptions; - $options.sortFilter; null + $options.sortMethod; + $options.sortOptions + ) | + if ($options.sortFilter != null) then utils::sortFilterFromString($options.sortFilter) end | + printExperiences( + $exportData.customUnits; + $options.substanceFilter; + $options.consumerFilter ) else usage diff --git a/tool/types.jq b/tool/types.jq index e2e7ffb..8af3169 100644 --- a/tool/types.jq +++ b/tool/types.jq @@ -4,27 +4,71 @@ import "lib/typeLib" as typeLib; #["DEBUG:",{"creationDate":"number","ingestions":[{"administrationRoute":"string","consumerName":"null","creationDate":"number","customUnitId":"null","dose":"number","estimatedDoseStandardDeviation":"null","isDoseAnEstimate":"boolean","notes":"string","stomachFullness":"null","substanceName":"string","time":"number","units":"string"}],"isFavorite":"boolean","location":"null","ratings":"array:empty/unknown","sortDate":"number","text":"string","timedNotes":"array:empty/unknown","title":"string"}] +def ensureAdministrationRoute: + . as $administrationRoute | + if typeLib::typecheckingEnabled then + [ + "ORAL", + "SUBLINGUAL", + "BUCCAL", + "INSUFFLATED", + "RECTAL", + "TRANSDERMAL", + "SUBCUTANEOUS", + "INTRAMUSCULAR", + "INTRAVENOUS", + "SMOKED", + "INHALED" + ] | any(index($administrationRoute)) + end; + def ensureIngestion: . as $ingestion | - typeLib::ensureObject | - - $ingestion | typeLib::ensureKey("ingestion"; "substanceName") | - .substanceName | typeLib::ensureWrapError("experience:substanceName"; typeLib::ensureString); - -def ensureTimedNote: - . as $timedNote | if typeLib::typecheckingEnabled then - $timedNote | typeLib::ensureObject | - $timedNote | typeLib::ensureKey("timedNote"; "color") | - $timedNote.color | typeLib::ensureWrapError("timedNote:color"; typeLib::ensureString) | - $timedNote | typeLib::ensureKey("timedNote"; "creationDate") | - $timedNote.creationDate | typeLib::ensureWrapError("timedNote:creationDate"; typeLib::ensureNumber) | - $timedNote | typeLib::ensureKey("timedNote"; "isPartOfTimeline") | - $timedNote.isPartOfTimeline | typeLib::ensureWrapError("timedNote:isPartOfTimeline"; typeLib::ensureBool) | - $timedNote | typeLib::ensureKey("timedNote"; "note") | - $timedNote.note | typeLib::ensureWrapError("timedNote:note"; typeLib::ensureString) | - $timedNote | typeLib::ensureKey("timedNote"; "time") | - $timedNote.time | typeLib::ensureWrapError("timedNote:time"; typeLib::ensureNumber) + typeLib::ensureObject | + + $ingestion | typeLib::ensureKey("ingestion"; "substanceName") | + .substanceName | typeLib::ensureWrapError("experience:substanceName"; typeLib::ensureString) | + + $ingestion | typeLib::ensureKey("ingestion"; "time") | + .time | typeLib::ensureWrapError("experience:time"; typeLib::ensureNumber) | + + $ingestion | typeLib::ensureKey("ingestion"; "creationDate") | + .creationDate | typeLib::ensureWrapError("experience:creationDate"; typeLib::ensureNumber) | + + # TODO: check routes + $ingestion | + typeLib::ensureKey("ingestion"; "administrationRoute") | + $ingestion.administrationRoute | + typeLib::ensureWrapError("experience:administrationRoute"; typeLib::ensureString) | + $ingestion.administrationRoute | + ensureAdministrationRoute | + + typeLib::ensureWrapError( + "experience:dose"; + $ingestion | typeLib::ensureIfKey("dose"; typeLib::ensureNullOr(typeLib::ensureNumber)) + ) | + + $ingestion | typeLib::ensureKey("ingestion"; "isDoseAnEstimate") | + .isDoseAnEstimate | typeLib::ensureWrapError("experience:isDoseAnEstimate"; typeLib::ensureBool) | + + $ingestion | typeLib::ensureKey("ingestion"; "units") | + .units | typeLib::ensureWrapError("experience:units"; typeLib::ensureString) | + + $ingestion | typeLib::ensureKey("ingestion"; "notes") | + .notes | typeLib::ensureWrapError("experience:notes"; typeLib::ensureString) | + + typeLib::ensureWrapError( + "experience:stomachFullness"; + $ingestion | typeLib::ensureIfKey("stomachFullness"; typeLib::ensureNullOr(typeLib::ensureString)) + ) | + + typeLib::ensureWrapError( + "experience:customUnitId"; + $ingestion | typeLib::ensureIfKey("customUnitId"; typeLib::ensureNullOr(typeLib::ensureNumber)) + ) | + + $ingestion end; def ensureExperience: @@ -44,17 +88,11 @@ def ensureExperience: $experience | typeLib::ensureKey("experience"; "sortDate") | $experience.sortDate | typeLib::ensureWrapError("experience:sortDate"; typeLib::ensureNumber) | - $experience | typeLib::ensureKey("experience"; "isFavorite") | - $experience.isFavorite | typeLib::ensureWrapError("experience:isFavorite"; typeLib::ensureBool) | - - $experience | typeLib::ensureKey("experience"; "timedNotes") | - $experience.timedNotes | typeLib::ensureWrapError("experience:timedNotes"; typeLib::ensureArray) | - (reduce $experience.timedNotes[] as $timedNote (null; $timedNote | ensureTimedNote)) | - $experience | typeLib::ensureKey("experience"; "ingestions") | $experience.ingestions | typeLib::ensureWrapError("experience:ingestions"; typeLib::ensureArray) | - (reduce $experience.ingestions[] as $ingestion (null; $ingestion | ensureIngestion)) - + (reduce $experience.ingestions[] as $ingestion (null; $ingestion | ensureIngestion)) | + + $experience end; def ensureExportData: diff --git a/tool/utils.jq b/tool/utils.jq index 9f81038..79a5612 100644 --- a/tool/utils.jq +++ b/tool/utils.jq @@ -1,5 +1,7 @@ include "dropins"; +import "stringLib" as stringLib; + def debugLog($target; $value): if ($ENV["JQ_DEBUG"] == $target) @@ -14,27 +16,8 @@ def debugLog($target; $value): 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 @@ -43,81 +26,11 @@ def orderedUnique: 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 +def sortFilterFromString($filterString): + ($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; \ No newline at end of file