diff --git a/TransactionQL.Application/TransactionQL.Application.fsproj b/TransactionQL.Application/TransactionQL.Application.fsproj
index 7877175..3f3d858 100644
--- a/TransactionQL.Application/TransactionQL.Application.fsproj
+++ b/TransactionQL.Application/TransactionQL.Application.fsproj
@@ -4,7 +4,7 @@
net8.0
true
TransactionQL.CsharpApi
- 2.1.3
+ 2.2.0
diff --git a/TransactionQL.Console/TransactionQL.Console.fsproj b/TransactionQL.Console/TransactionQL.Console.fsproj
index c5943c1..88de186 100644
--- a/TransactionQL.Console/TransactionQL.Console.fsproj
+++ b/TransactionQL.Console/TransactionQL.Console.fsproj
@@ -6,7 +6,7 @@
./nupkg
Exe
net8.0
- 2.1.3
+ 2.2.0
diff --git a/TransactionQL.DesktopApp/TransactionQL.DesktopApp.csproj b/TransactionQL.DesktopApp/TransactionQL.DesktopApp.csproj
index 3f6e8dc..6d827d7 100644
--- a/TransactionQL.DesktopApp/TransactionQL.DesktopApp.csproj
+++ b/TransactionQL.DesktopApp/TransactionQL.DesktopApp.csproj
@@ -7,7 +7,7 @@
app.manifest
true
Assets\lion.ico
- 2.1.3
+ 2.2.0
diff --git a/TransactionQL.Input/TransactionQL.Input.fsproj b/TransactionQL.Input/TransactionQL.Input.fsproj
index 52428c9..49f1e68 100644
--- a/TransactionQL.Input/TransactionQL.Input.fsproj
+++ b/TransactionQL.Input/TransactionQL.Input.fsproj
@@ -2,7 +2,7 @@
net8.0
- 2.1.3
+ 2.2.0
diff --git a/TransactionQL.Parser.Tests/QLInterpreterTests.fs b/TransactionQL.Parser.Tests/QLInterpreterTests.fs
index 35b150f..352f117 100644
--- a/TransactionQL.Parser.Tests/QLInterpreterTests.fs
+++ b/TransactionQL.Parser.Tests/QLInterpreterTests.fs
@@ -232,6 +232,32 @@ let ``Inference: number column - notequalto`` () =
Assert.True(evalFilter' (env', Filter(Column "Amount", NotEqualTo, (Number 2.0))))
+[]
+let ``Payee: words`` () =
+ let payeeParts = Interpolation [ Word "American"; Word "Express"]
+ let payee = evalPayee env payeeParts
+ Assert.Equal("American Express", payee)
+
+[]
+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)
+
+[]
+let ``Payee: variable (undefined)`` () =
+ let payeeParts = Interpolation [ ColumnToken (Column "Name") ]
+ let payee = evalPayee env payeeParts
+ Assert.Equal("@Name", payee)
+
+[]
+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)
+
[]
let ``Posting lines: No amount`` () =
let env' =
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
diff --git a/TransactionQL.Parser.Tests/QLParserTests.fs b/TransactionQL.Parser.Tests/QLParserTests.fs
index 0e17461..1dd0843 100644
--- a/TransactionQL.Parser.Tests/QLParserTests.fs
+++ b/TransactionQL.Parser.Tests/QLParserTests.fs
@@ -181,27 +181,54 @@ let ``Filters: or groups`` () =
[]
let ``Payee: # `` () =
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)
[]
-let ``Query: `` () =
- let query =
- """# Full description test
- Creditor = "NL"
- Amount >= 50.00
-
- posting {
- Assets:TestAccount EUR (total / 2)
- Assets:TestSavings EUR (remainder)
- Expenses:Development
- }
- """
+let ``Payee: # `` () =
+ let variable = "Name"
+ test QLParser.qpayee $"# @%s{variable}\n" (Interpolation [ColumnToken (Column variable)])
+
+[]
+let ``Payee: # `` () =
+ let variable = "Name"
+ test QLParser.qpayee $"# Some @%s{variable} string\n" (Interpolation [ (Word "Some"); (ColumnToken (Column variable)); (Word "string") ])
+[]
+let ``Payee: # `` () =
+ let variable = "Name"
+ test
+ QLParser.qpayee
+ $"# Some & Sons (@%s{variable}) string\n"
+ (Interpolation [
+ Word "Some"
+ Word ""
+ Word "&"
+ Word "Sons"
+ Word "("
+ ColumnToken (Column "Name")
+ Word ")"
+ Word "string"
+ ])
+
+[]
+let ``Query: `` () =
+ 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(
@@ -220,38 +247,38 @@ let ``Query: `` () =
[]
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)
diff --git a/TransactionQL.Parser/AST.fs b/TransactionQL.Parser/AST.fs
index 5d15c78..d6ac57e 100644
--- a/TransactionQL.Parser/AST.fs
+++ b/TransactionQL.Parser/AST.fs
@@ -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
diff --git a/TransactionQL.Parser/Interpretation.fs b/TransactionQL.Parser/Interpretation.fs
index 2fef8b2..3470ae0 100644
--- a/TransactionQL.Parser/Interpretation.fs
+++ b/TransactionQL.Parser/Interpretation.fs
@@ -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))
diff --git a/TransactionQL.Parser/QLInterpreter.fs b/TransactionQL.Parser/QLInterpreter.fs
index f69c678..34c9d72 100644
--- a/TransactionQL.Parser/QLInterpreter.fs
+++ b/TransactionQL.Parser/QLInterpreter.fs
@@ -38,7 +38,6 @@ module QLInterpreter =
eval' expr |> fun n -> Interpretation(env, n)
-
let generatePostingLine
env
({ Account = accounts
@@ -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
@@ -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
@@ -178,7 +187,6 @@ module QLInterpreter =
Comments = newComments }
)
-
let rec evalProgram env queries =
match queries with
| (q :: qs) ->
diff --git a/TransactionQL.Parser/QLParser.fs b/TransactionQL.Parser/QLParser.fs
index 9408e63..b7a1c32 100644
--- a/TransactionQL.Parser/QLParser.fs
+++ b/TransactionQL.Parser/QLParser.fs
@@ -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 =
- 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)
diff --git a/TransactionQL.Parser/TransactionQL.Parser.fsproj b/TransactionQL.Parser/TransactionQL.Parser.fsproj
index 0585831..c2f033a 100644
--- a/TransactionQL.Parser/TransactionQL.Parser.fsproj
+++ b/TransactionQL.Parser/TransactionQL.Parser.fsproj
@@ -2,7 +2,7 @@
net8.0
- 2.1.3
+ 2.2.0
diff --git a/readme.md b/readme.md
index cb3d4e8..3e17d62 100644
--- a/readme.md
+++ b/readme.md
@@ -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