Skip to content

Commit

Permalink
Allow variables in Payee line (#4) (#9)
Browse files Browse the repository at this point in the history
* Update AST and parser to allow for expressions in Payee

* Payee parser unit tests

* Fix unit tests where payee is used in context

* Add unit tests for parser

* chore: update assembly versions

* fix(interpreter): no substitution for undefined payee variable

* docs: Added variables to payee line in readme
  • Loading branch information
janssen-io authored Aug 19, 2024
1 parent d5f20d7 commit 57e4208
Show file tree
Hide file tree
Showing 12 changed files with 142 additions and 62 deletions.
2 changes: 1 addition & 1 deletion TransactionQL.Application/TransactionQL.Application.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<RootNamespace>TransactionQL.CsharpApi</RootNamespace>
<AssemblyVersion>2.1.3</AssemblyVersion>
<AssemblyVersion>2.2.0</AssemblyVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion TransactionQL.Console/TransactionQL.Console.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<PackageOutputPath>./nupkg</PackageOutputPath>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<AssemblyVersion>2.1.3</AssemblyVersion>
<AssemblyVersion>2.2.0</AssemblyVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion TransactionQL.DesktopApp/TransactionQL.DesktopApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<ApplicationIcon>Assets\lion.ico</ApplicationIcon>
<AssemblyVersion>2.1.3</AssemblyVersion>
<AssemblyVersion>2.2.0</AssemblyVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion TransactionQL.Input/TransactionQL.Input.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyVersion>2.1.3</AssemblyVersion>
<AssemblyVersion>2.2.0</AssemblyVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
42 changes: 34 additions & 8 deletions TransactionQL.Parser.Tests/QLInterpreterTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,32 @@ let ``Inference: number column - notequalto`` () =

Assert.True(evalFilter' (env', Filter(Column "Amount", NotEqualTo, (Number 2.0))))

[<Fact>]
let ``Payee: words`` () =
let payeeParts = Interpolation [ Word "American"; Word "Express"]
let payee = evalPayee env payeeParts
Assert.Equal("American Express", payee)

[<Fact>]
let ``Payee: variable`` () =
let payeeParts = Interpolation [ ColumnToken (Column "Name") ]
let env' = { env with Row = Map.ofList [ ("Name", "American Express") ]}
let payee = evalPayee env' payeeParts
Assert.Equal("American Express", payee)

[<Fact>]
let ``Payee: variable (undefined)`` () =
let payeeParts = Interpolation [ ColumnToken (Column "Name") ]
let payee = evalPayee env payeeParts
Assert.Equal("@Name", payee)

[<Fact>]
let ``Payee: interpolation`` () =
let payeeParts = Interpolation [ Word "Monthly:"; ColumnToken (Column "Name") ]
let env' = { env with Row = Map.ofList [ ("Name", "American Express") ]}
let payee = evalPayee env' payeeParts
Assert.Equal("Monthly: American Express", payee)

[<Fact>]
let ``Posting lines: No amount`` () =
let env' =
Expand Down Expand Up @@ -326,7 +352,7 @@ let ``Posting: updates remainder between lines`` () =
let ``Query: given a matching row, a posting is generated`` () =
let ql =
Query(
Payee "a payee",
Word "a payee",
[ Filter(Column "Amount", GreaterThan, Number 0.00) ],
Posting(
None,
Expand All @@ -351,7 +377,7 @@ let ``Query: given a matching row, a posting is generated`` () =
let ``Query: given a row that does not match, no posting is generated`` () =
let ql =
Query(
Payee "a payee",
Word "a payee",
[ Filter(Column "Amount", GreaterThan, Number 0.00) ],
Posting(
None,
Expand All @@ -369,7 +395,7 @@ let ``Query: given a row that does not match, no posting is generated`` () =
let ``Queries: multiple matching queries only applies the first match`` () =
let queries =
[ Query(
Payee "first payee",
Word "first payee",
[ Filter(Column "Amount", GreaterThan, Number 0.00) ],
Posting(
None,
Expand All @@ -382,7 +408,7 @@ let ``Queries: multiple matching queries only applies the first match`` () =
)
)
Query(
Payee "second payee",
Word "second payee",
[ Filter(Column "Amount", GreaterThan, Number 0.00) ],
Posting(
None,
Expand Down Expand Up @@ -411,7 +437,7 @@ let ``Queries: multiple matching queries only applies the first match`` () =
let ``Queries: multiple queries only applies the match`` () =
let queries =
[ Query(
Payee "first payee",
Word "first payee",
[ Filter(Column "Amount", LessThan, Number 0.00) ],
Posting(
None,
Expand All @@ -424,7 +450,7 @@ let ``Queries: multiple queries only applies the match`` () =
)
)
Query(
Payee "second payee",
Word "second payee",
[ Filter(Column "Amount", GreaterThan, Number 0.00) ],
Posting(
None,
Expand Down Expand Up @@ -459,7 +485,7 @@ let ``Queries: no matches`` () =
let ``Queries: notes are added to the comments`` () =
let queries =
[ Query(
Payee "second payee",
Word "second payee",
[ Filter(Column "Amount", GreaterThan, Number 0.00) ],
Posting(
"this is a note" |> Some,
Expand All @@ -486,7 +512,7 @@ let ``Queries: notes are added to the comments`` () =
let ``Queries: tags are added to the posting line`` () =
let queries =
[ Query(
Payee "second payee",
Word "second payee",
[ Filter(Column "Amount", GreaterThan, Number 0.00) ],
Posting(
"this is a note" |> Some,
Expand Down
103 changes: 65 additions & 38 deletions TransactionQL.Parser.Tests/QLParserTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -181,27 +181,54 @@ let ``Filters: or groups`` () =
[<Fact>]
let ``Payee: # <words>`` () =
let payee = "Some long string"
test QLParser.qpayee $"# %s{payee}" (Payee payee)
let words = payee.Split () |> (Array.map Word) |> List.ofArray
test QLParser.qpayee $"# %s{payee}\n" (Interpolation words)

[<Fact>]
let ``Query: <payee> <filters> <posting>`` () =
let query =
"""# Full description test
Creditor = "NL"
Amount >= 50.00
posting {
Assets:TestAccount EUR (total / 2)
Assets:TestSavings EUR (remainder)
Expenses:Development
}
"""
let ``Payee: # <variable>`` () =
let variable = "Name"
test QLParser.qpayee $"# @%s{variable}\n" (Interpolation [ColumnToken (Column variable)])

[<Fact>]
let ``Payee: # <interpolation>`` () =
let variable = "Name"
test QLParser.qpayee $"# Some @%s{variable} string\n" (Interpolation [ (Word "Some"); (ColumnToken (Column variable)); (Word "string") ])

[<Fact>]
let ``Payee: # <odd chars>`` () =
let variable = "Name"
test
QLParser.qpayee
$"# Some <Test> & Sons (@%s{variable}) string\n"
(Interpolation [
Word "Some"
Word "<Test>"
Word "&"
Word "Sons"
Word "("
ColumnToken (Column "Name")
Word ")"
Word "string"
])

[<Fact>]
let ``Query: <payee> <filters> <posting>`` () =
let query = [
"# Full description @Creditor"
" Creditor = \"NL\""
" Amount >= 50.00"
""
" posting {"
" Assets:TestAccount EUR (total / 2)"
" Assets:TestSavings EUR (remainder)"
" Expenses:Development"
" }"
]
test
QLParser.qquery
query
(String.concat Environment.NewLine query)
(Query(
Payee "Full description test",
Interpolation [ Word "Full"; Word "description"; ColumnToken (Column "Creditor") ],
[ Filter(Column "Creditor", EqualTo, String "NL")
Filter(Column "Amount", GreaterThanOrEqualTo, Number 50.0) ],
Posting(
Expand All @@ -220,38 +247,38 @@ let ``Query: <payee> <filters> <posting>`` () =

[<Fact>]
let ``Queries: multiple queries`` () =
let queries =
"""# First query
Creditor = "NL"
posting {
Test:Account
}
# Second query
Creditor = "BE"
A = 5.0
or B = 2.0
C = 1.0
posting {
Assets:Checking
}
"""
let queries = [
"# First query"
" Creditor = \"NL\""
""
" posting {"
" Test:Account"
" }"
""
"# Second query"
" Creditor = \"BE\""
""
" A = 5.0"
" or B = 2.0"
""
" C = 1.0"
""
" posting {"
" Assets:Checking"
" }"
]

test
QLParser.qprogram
queries
(String.concat Environment.NewLine queries)
([ Query(
Payee "First query",
Interpolation [ Word "First"; Word "query" ],
[ Filter(Column "Creditor", EqualTo, String "NL") ],
Posting(None, [ trx (Account [ "Test"; "Account" ], None) ])
)

Query(
Payee "Second query",
Interpolation [ Word "Second"; Word "query" ],
[ Filter(Column "Creditor", EqualTo, String "BE")
OrGroup
[ Filter(Column "A", EqualTo, Number 5.0)
Expand Down
5 changes: 4 additions & 1 deletion TransactionQL.Parser/AST.fs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ module AST =
| Filter of Column * FilterOperator * FilterAtom
| OrGroup of Filter list

type Payee = Payee of string
type Payee =
| Word of string
| ColumnToken of Column
| Interpolation of Payee list

type Query = Query of Payee * Filter list * Posting
5 changes: 5 additions & 0 deletions TransactionQL.Parser/Interpretation.fs
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,8 @@ module Interpretation =
Interpretation(newEnv, folder currentResult newResult))
seed
list

let map
(f: ('a -> 'b))
(Interpretation(e, r): Interpretation<'a>)
: Interpretation<'b> = Interpretation(e, f(r))
18 changes: 13 additions & 5 deletions TransactionQL.Parser/QLInterpreter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ module QLInterpreter =

eval' expr |> fun n -> Interpretation(env, n)


let generatePostingLine
env
({ Account = accounts
Expand Down Expand Up @@ -135,7 +134,17 @@ module QLInterpreter =

| OrGroup filters -> Interpretation.fold evalFilter (||) (Interpretation(env, false)) filters

let evalQuery env (Query(Payee payee, filters, posting)) =
let rec evalPayee env payee =
match payee with
| Word p -> p
| ColumnToken (Column col) ->
Map.tryFind col env.Row
|> Option.defaultValue $"@{col}"
| Interpolation xs ->
List.map (evalPayee env) xs
|> (String.concat " ")

let evalQuery env (Query(payee, filters, posting)) =
let (Interpretation(envFilter, isMatch)) =
Interpretation.fold evalFilter (&&) (Interpretation(env, true)) filters

Expand All @@ -153,8 +162,8 @@ module QLInterpreter =
let date =
System.DateTime.ParseExact(Map.find "Date" env.Row, env.DateFormat, CultureInfo.InvariantCulture)

// TODO: (20240818) Check if Payee contains variables (for example 'Recipient', 'Name' or 'Receiver')
let header = Header(date, payee)
let payeeString = evalPayee env payee
let header = Header(date, payeeString)

let comments =
[ if Map.containsKey "Description" envFilter.Row then
Expand All @@ -178,7 +187,6 @@ module QLInterpreter =
Comments = newComments }
)


let rec evalProgram env queries =
match queries with
| (q :: qs) ->
Expand Down
15 changes: 11 additions & 4 deletions TransactionQL.Parser/QLParser.fs
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,22 @@ module QLParser =

orGroup

/// Skip zero or more literal spaces
let pspace = skipMany (pchar ' ')

let qcolumnToken = pchar '@' >>. qcolumnIdentifier |>> ColumnToken

let qpayee: Parser<Payee, unit> =
let isNewline = fun c -> List.contains c [ "\n"; "\r" ]
(pchar '#' .>> spaces1) >>. manySatisfy (not << isNewline << string) |>> Payee
let isWhitespaceOrVar = fun c -> List.contains c [ "\n"; "\r"; "\t"; " "; "@" ]
let ptext = manySatisfy (not << isWhitespaceOrVar << string)
let payeeParts = ((either qcolumnToken (ptext |>> Word)) .>>? pspace)
let pinterpolation = (many1Till payeeParts newline) |>> Interpolation
(pchar '#' .>> spaces1) >>. pinterpolation

let qquery =
let payee = qpayee .>> newline
let filters = many (between spaces spaces1 qfilter)
let posting = between spaces spaces qposting
pipe3 payee filters posting (curry3 Query)
pipe3 qpayee filters posting (curry3 Query)

let qprogram = many (qquery .>> spaces)

Expand Down
2 changes: 1 addition & 1 deletion TransactionQL.Parser/TransactionQL.Parser.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyVersion>2.1.3</AssemblyVersion>
<AssemblyVersion>2.2.0</AssemblyVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
6 changes: 5 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ filter, it will generate a posting. Filters consist of three parts:
### Payee line

Every filter starts with a payee line. This line will be used on the generated
posting.
posting. Variables can be used by prefixing them with the `@`-symbol.
For example, `# Rent (@Name)`. If the variable does not exist, the text is
printed as is.

Individual `@`-symbols are not supported.

### Conditions

Expand Down

0 comments on commit 57e4208

Please sign in to comment.