commit 16ed12c0750f417facc9e2b5c2d096240016af4f
parent 4981c3dbef0b5a7e92806ea2c3c37f64098fe5e2
Author: Bharatvaj Hemanth <bharatvaj@yahoo.com>
Date: Mon, 18 Dec 2023 01:45:23 +0530
Use separate book.c and account.c for definitions
Fix Makefile test setup
Rename to payredu
Update README
Update .gitigonre
Diffstat:
M | .gitignore | | | 5 | +++++ |
M | CHANGELOG | | | 1 | + |
M | Makefile | | | 27 | +++++++++++---------------- |
M | README | | | 19 | ++++++++++--------- |
A | account.c | | | 72 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | account.h | | | 65 | ++--------------------------------------------------------------- |
M | book.c | | | 72 | ++++++++++++++++++++++++++++++++---------------------------------------- |
M | book.h | | | 25 | +++++++------------------ |
D | hotbook.c | | | 56 | -------------------------------------------------------- |
A | payredu.c | | | 72 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | tests/account_tree.c | | | 47 | +++++++++++++++++++++++++++++++++++++++++++++++ |
A | tests/csv_export.c | | | 51 | +++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | tests/tests.mk | | | 11 | +++++++++++ |
13 files changed, 321 insertions(+), 202 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -4,3 +4,8 @@ hotbook
*.so
*.o
tags
+
+tests/*
+!tests/tests.mk
+!tests/account_tree.c
+!tests/csv_export.c
diff --git a/CHANGELOG b/CHANGELOG
@@ -3,3 +3,4 @@ Dark Ages
- Starting keeping CHANGELOG file for payredo
- Basic combined parser/lexer for the ledger format has been
implemented in book.c
+- Fix Makefile issue with tests/tests.mk
diff --git a/Makefile b/Makefile
@@ -1,13 +1,14 @@
GENERAL_FLAGS=-fPIC
LDFLAGS:=$(GENERAL_FLAGS) -lglfw -lGL -lm
CFLAGS:=$(GENERAL_FLAGS) -O0 -I. -g -Werror #-Wpedantic
+export LD_LIBRARY_PATH=.
-.DEFAULT_GOAL=book
+.DEFAULT_GOAL=payredu
CC=gcc
-%: %.c account.h
- $(CC) -o $@ $(CFLAGS) $(LDFLAGS) $<
+%: %.c libbook.so
+ $(CC) -o $@ $(CFLAGS) $(LDFLAGS) libbook.so $<
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
@@ -15,16 +16,16 @@ CC=gcc
bal:
ledger -f october-2023.txt bal
-lib%.so: %.o
- $(CC) -shared -Wl,-soname,$@ -o $@ $<
-
balance: balance.c ledger.h
hot: hot.c libbalance.so
-libbook.so: book.c book.h
+libbook.a: book.c book.h account.c account.h
+
+libbook.so: book.o account.o
+ $(CC) -shared -Wl,-soname,$@ -o $@ $^
-hotbook: hotbook.c
+payredu: payredu.c libbook.so
refresh:
git ls-files | entr sh hot.sh
@@ -32,13 +33,7 @@ refresh:
format:
indent -psl -pal --use-tabs -ts4 -br -brs -ce -cli0 book.c
-awk_query = $$(awk '/\/\* $1/{flag=1; next}/$1 \*\//{flag=0}flag' $2.c)
-test_cmd = if [ "$$($< $(call awk_query,TEST_INPUT,$<))" = "$(call awk_query,TEST_OUTPUT,$<)" ]; \
- then echo Passed; \
- else echo Failed; \
- fi
-
-#include tests/tests.mk
+include tests/tests.mk
clean:
- -rm *.so *.o hotbook libbook.so test/account_tree
+ -rm *.so *.o hotbook libbook.so $(TESTS)
diff --git a/README b/README
@@ -1,24 +1,25 @@
-payeredo
-========
+payredu
+=======
பேரேடு
======
-payeredo is a cross-platform frontend to ledger(pta) with emphasis on simplicity. perudu means ledger in Tamil. It is written in c99 and works on top of nuklear making it lightweight and fast.
+payredu is a cross-platform frontend to ledger(pta) with emphasis on simplicity. perudu means ledger in Tamil. It is written in c99 and works on top of nuklear making it lightweight and fast.
NOTE: The quality of the software is beta in the least, it's still in development.
-For now the following commands work,
+To build and run,
- $ ./hotbook
+ $ make
+ $ ./payredu
-Why payeredo when ledger-cli exists?
+Why payredu when ledger-cli exists?
------------------------------------
-ledger-cli itself pretty lightweight but it has a handful of dependencies and features which I don't particularly use.
+ledger-cli itself pretty lightweight but it has a handful of dependencies and features which I don't particularly care about.
-payeredo follows a very suckless approach to ledger and does NOT provide some of the advanced features ledger provides.
+payredu follows a very suckless approach to ledger and does NOT provide some of the advanced features ledger provides.
-It should be noted that payeredo is usually faster than ledger-cli, it does not provide some of the niceties that ledger-cil provides.
+It should be noted that payredu is usually faster than ledger-cli as it does not provide some of the niceties that ledger-cil provides.
Goals
-----
diff --git a/account.c b/account.c
@@ -0,0 +1,72 @@
+#include <account.h>
+
+#include <stdlib.h>
+
+// TODO make this dynamic
+size_t tree_depth = 4;
+
+
+map_tree_t *account_search(map_tree_t *rootp, char *acc, size_t acc_size)
+{
+ assert(rootp != NULL);
+ // we hit leaf node, return rootp
+ if (rootp->children == NULL) return rootp;
+
+ // return rootp when the 'acc' matches exactly with rootp->value
+ // acc: this, rootp->value: this
+ vstr_t *rk = rootp->value;
+ if (rk != NULL && acc_size == rk->len && (strncmp(acc, rk->str, acc_size) == 0)) {
+ return rootp;
+ }
+
+ // search the string in it's children
+ for (size_t i = 0; i < rootp->children_len; i++) {
+ vstr_t *val = rootp->children[i].value;
+ if (val != NULL && acc_size == val->len && (strncmp(acc, val->str, acc_size) == 0)) {
+ return rootp->children + i;
+ }
+ }
+ return NULL;
+}
+
+int account_add(map_tree_t **rootp, char *acc, size_t acc_size)
+{
+ size_t records_needed = tree_depth * 4;
+ if (*rootp == NULL) {
+ *rootp = malloc(sizeof(map_tree_t));
+ }
+ if ((*rootp)->children == NULL) {
+ (*rootp)->children =
+ (map_tree_t *) calloc(records_needed, sizeof(map_tree_t));
+ (*rootp)->children_len = 0;
+ (*rootp)->children_cap = records_needed;
+ }
+ map_tree_t* _rootp = *rootp;
+ size_t i = 0;
+ while (i < acc_size) {
+ if (acc[i] == ':' || i + 1 == acc_size) {
+ size_t j = i + 1;
+ map_tree_t *current_node = account_search(_rootp, acc, j);
+ if (current_node == NULL) {
+ // return the previously allocated child
+ current_node = _rootp->children + _rootp->children_len++;
+ // current_node->value is NULL when the search fails
+ // we have to set the value now
+ // TODO maybe save vstrs in a pool and use them, would provide a sane way to free memory
+ vstr_t *vstr = (vstr_t *) malloc(sizeof(vstr_t));
+ vstr->str = acc;
+ vstr->len = j;
+ current_node->value = vstr;
+ //printf("%zu : %zu %d %.*s\n", current_node, vstr, j, j, acc);
+ } else {
+ //printf("Present already= %d %.*s\n", j, j, acc);
+ }
+ if (j != acc_size) {
+ return account_add(¤t_node, acc + j,
+ acc_size - j);
+ }
+ }
+ i++;
+ }
+ return -1;
+}
diff --git a/account.h b/account.h
@@ -6,8 +6,6 @@
#include <vstr.h>
-size_t tree_depth = 4;
-
struct map_tree;
struct map_tree {
@@ -27,69 +25,10 @@ typedef struct map_tree map_tree_t;
// TODO handle both rootp,this:is:us case and rootp->children,is:us case
// Currently only the rootp->value and acc are compared
-map_tree_t *account_search(map_tree_t *rootp, char *acc, size_t acc_size)
-{
- assert(rootp != NULL);
- // we hit leaf node, return rootp
- if (rootp->children == NULL) return rootp;
- // return rootp when the 'acc' matches exactly with rootp->value
- // acc: this, rootp->value: this
- vstr_t *rk = rootp->value;
- if (rk != NULL && acc_size == rk->len && (strncmp(acc, rk->str, acc_size) == 0)) {
- return rootp;
- }
+map_tree_t *account_search(map_tree_t *rootp, char *acc, size_t acc_size);
- // search the string in it's children
- for (size_t i = 0; i < rootp->children_len; i++) {
- vstr_t *val = rootp->children[i].value;
- if (val != NULL && acc_size == val->len && (strncmp(acc, val->str, acc_size) == 0)) {
- return rootp->children + i;
- }
- }
- return NULL;
-}
-int account_add(map_tree_t **rootp, char *acc, size_t acc_size)
-{
- size_t records_needed = tree_depth * 4;
- if (*rootp == NULL) {
- *rootp = malloc(sizeof(map_tree_t));
- }
- if ((*rootp)->children == NULL) {
- (*rootp)->children =
- (map_tree_t *) calloc(records_needed, sizeof(map_tree_t));
- (*rootp)->children_len = 0;
- (*rootp)->children_cap = records_needed;
- }
- map_tree_t* _rootp = *rootp;
- size_t i = 0;
- while (i < acc_size) {
- if (acc[i] == ':' || i + 1 == acc_size) {
- size_t j = i + 1;
- map_tree_t *current_node = account_search(_rootp, acc, j);
- if (current_node == NULL) {
- // return the previously allocated child
- current_node = _rootp->children + _rootp->children_len++;
- // current_node->value is NULL when the search fails
- // we have to set the value now
- // TODO maybe save vstrs in a pool and use them, would provide a sane way to free memory
- vstr_t *vstr = (vstr_t *) malloc(sizeof(vstr_t));
- vstr->str = acc;
- vstr->len = j;
- current_node->value = vstr;
- //printf("%zu : %zu %d %.*s\n", current_node, vstr, j, j, acc);
- } else {
- //printf("Present already= %d %.*s\n", j, j, acc);
- }
- if (j != acc_size) {
- return account_add(¤t_node, acc + j,
- acc_size - j);
- }
- }
- i++;
- }
- return -1;
-}
+int account_add(map_tree_t **rootp, char *acc, size_t acc_size);
#endif
diff --git a/book.c b/book.c
@@ -27,8 +27,8 @@
#include <time.h>
#include "vstr.h"
-#include "account.h"
-#include "book.h"
+#include <account.h>
+#include <book.h>
#define BUFFER_SIZE 256
@@ -159,30 +159,31 @@ void ledger_parse_data(char *text, size_t text_len)
case '\r':
line_no++;
n_count++;
- printf("\n%d| ", line_no);
switch (state) {
// after parsing the amount seq, we set the state to ENTRY_WHO
case ENTRY_WHO:
case ENTRY_END:
- warning("----- Entry End Marked -----\n");
hold_sign = -1;
hold_amount = LONG_MAX;
// if entry_count <= 1 throw error
if (text[i - 1] == '\n') {
state = DATE;
// TODO push the entries to stack or somethin
- warning("----- Posting End Marked -----\n");
+ warning("\n==\n");
// state = POSTING_END;
} else {
state = ENTRY_WHO;
+ warning(",");
}
break;
case COMMENT:
+ state = ENTRY_START;
+ break;
case ENTRY_SIGN_DENOM_AMOUNT:
state = ENTRY_WHO;
break;
case ENTRY_DENOM:
- warningf("%s\n", "denom not found, setting state WHO");
+ //warningf("%s", "denom not found, setting state WHO");
state = ENTRY_WHO;
break;
case ENTRY_AMOUNT:
@@ -205,8 +206,7 @@ void ledger_parse_data(char *text, size_t text_len)
if (isdigit(c)) {
// try to parse a date
time_t tn = ledger_timestamp_from_ledger_date(text + i);
- warningf("date str: %.*s\n", 10, text + i);
- warningf("date: %ld\n", tn);
+ warningf("%.*s: %ld ", 10, text + i, tn);
// date is expected to have the form DD/MM/YYYY (10)
i += 10;
if (tn == (time_t) - 1) goto ledger_parse_error_handle;
@@ -226,23 +226,12 @@ void ledger_parse_data(char *text, size_t text_len)
comment_len++;
}
comment.len = comment_len;
- warningf("Comment: %.*s\n", comment_len,
+ warningf("Comment: %.*s", comment_len,
comment);
- state = ENTRY_WHO;
- }
- break;
- case ENTRY_SPACE:
- {
- size_t original_i = i;
- while (i < text_len && isspace(text[i])) i++;
- int wsc = i - original_i;
- warningf("i: %ld, Spaces: %d\n", i, wsc);
- if (wsc < 2) {
- goto ledger_parse_error_handle;
- }
- state = ENTRY_WHO;
+ state = ENTRY_START;
}
break;
+ case ENTRY_START:
case ENTRY_WHO:
{
// add this to register
@@ -268,9 +257,8 @@ void ledger_parse_data(char *text, size_t text_len)
}
ledger_who_parsed:
who_len = i - who_len;
- warningf("parsed: i=%d\n", i);
account_add(&rootp, who.str, who_len);
- warningf("i=%d, Who: %.*s\n", i, who_len, who);
+ warningf("\n(%d) Who: %.*s", i, who_len, who);
state = ENTRY_SIGN_DENOM_AMOUNT;
// add to tags here
}
@@ -302,7 +290,7 @@ ledger_who_parsed:
} break;
case ENTRY_DENOM: {
char _c;
- warningf("denom-i: %d\n", i + 1);
+ warningf(" %d: D:", i + 1);
char *denom = text + i;
size_t denom_len = 0;
while (i < text_len &&
@@ -313,12 +301,12 @@ ledger_who_parsed:
state = hold_sign? ENTRY_AMOUNT: ENTRY_SIGN_AMOUNT;
else
state = ENTRY_END;
- warningf("%d> len: %d, denom: %.*s\n", i, denom_len, denom_len, denom);
+ warningf(" %.*s(%d)", denom_len, denom, denom_len);
break;
}
case ENTRY_AMOUNT: {
char _c;
- warningf("amount-i: %d\n", i + 1);
+ warningf(" %d A:", i + 1);
char *amount = text + i;
size_t amount_len = 0;
while (i < text_len && (_c = *(text + i)) == '.' || isdigit(_c) || _c == ',') i++;
@@ -326,7 +314,7 @@ ledger_who_parsed:
// TODO convert amount to hold_amount integer
hold_amount = 0;
state = hold_denom_id == 0? ENTRY_DENOM : ENTRY_END;
- warningf("%d> len: %d, amount: %.*s\n", i, amount_len, amount_len, amount);
+ warningf(" %.*s(%d)", amount_len, amount, amount_len);
}
break;
default:
@@ -339,19 +327,23 @@ ledger_parse_error_handle:
warningf("Parse failed at %ld b:(%d), Expected %s, got '%c'",
line_no, i, states_str[state], text[i]);
}
-
-int main(int argc, char* argv[]) {
- FILE* in = fopen("october-2023.txt", "r");
- char* data = (char*)malloc(2048 * sizeof(char));
- size_t data_size = 0;
- size_t c_read = 0;
- while((c_read = fread(data + data_size + 0, 1, BUFFER_SIZE, in)) != 0) {
- data_size += c_read;
+Entry** ledger_read_file(const char* filename, time_t date_start, time_t date_end) {
+ Entity me = {"Account:Income"};
+ // list population, read from
+ FILE* file = fopen(filename, "r");
+ if (file == NULL) {
+ printf("Failed to open file %s\n", filename);
+ }
+ Entry** new_list = (Entry**)malloc(sizeof(Entry*) * 12);
+ for(int i = 0; i < 12; i++) {
+ Entry* entry = (Entry*)malloc(sizeof(Entry));
+ new_list[i] = entry;
+ entry->from = &me;
+ entry->to = (Entity*)malloc(sizeof(Entity));
+ entry->to->name = (char*)malloc(sizeof(char*) * 20);
+ strcpy(entry->to->name, "Man");
}
- if (ferror(in)) fprintf(stderr, "Error reading file\n");
- fprintf(stdout, "Startig loop\n");
- ledger_parse_data(data, data_size);
- return 0;
+ return new_list;
}
void *module_main(char *data, size_t data_len)
diff --git a/book.h b/book.h
@@ -1,3 +1,6 @@
+#ifndef _LEDGER_BOOK_H
+#define _LEDGER_BOOK_H
+
#include <string.h>
#include <stdlib.h>
@@ -11,23 +14,9 @@ typedef struct {
long long amt;
} Entry;
-Entity me = {"Account:Income"};
-Entry** ledger_read_file(const char* filename, time_t date_start, time_t date_end) {
- // list population, read from
- FILE* file = fopen(filename, "r");
- if (file == NULL) {
- printf("Failed to open file %s\n", filename);
- }
- Entry** new_list = (Entry**)malloc(sizeof(Entry*) * 12);
- for(int i = 0; i < 12; i++) {
- Entry* entry = (Entry*)malloc(sizeof(Entry));
- new_list[i] = entry;
- entry->from = &me;
- entry->to = (Entity*)malloc(sizeof(Entity));
- entry->to->name = (char*)malloc(sizeof(char*) * 20);
- strcpy(entry->to->name, "Man");
- }
- return new_list;
-}
+void ledger_parse_data(char *text, size_t text_len);
+
+Entry** ledger_read_file(const char* filename, time_t date_start, time_t date_end);
+#endif
diff --git a/hotbook.c b/hotbook.c
@@ -1,56 +0,0 @@
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <dlfcn.h>
-#include <signal.h>
-#include <unistd.h>
-
-#include "common.h"
-
-#define MAX_MEMORY 4064
-#define WINDOW_WIDTH 512
-#define WINDOW_HEIGHT 512
-#define BUFFER_SIZE 256
-
-int should_exit = 0;
-
-void sig_handle() {
- printf("Reloaded\n");
- system("date");
- should_exit = 1;
-}
-
-typedef void* (*module_main_func)(const char*, size_t);
-
-int main(int argc, char* argv[]) {
- signal(SIGQUIT, sig_handle);
- //while(1) {
- void* module = dlopen("./libbook.so", RTLD_NOW);
- while(module == NULL){
- fprintf(stderr, "Failed to load module. (%s)\n", dlerror());
- fprintf(stderr, "Press return to try again.\n");
- getchar();
- module = dlopen("./libbook.so", RTLD_NOW);
- }
- module_main_func module_main = dlsym(module, "module_main");
- FILE* in = fopen("october-2023.txt", "r");
- char* data = (char*)malloc(2048 * sizeof(char));
- size_t data_size = 0;
- size_t c_read = 0;
- while((c_read = fread(data + data_size + 0, 1, BUFFER_SIZE, in)) != 0) {
- data_size += c_read;
- }
- if (ferror(in)) fprintf(stderr, "Error reading file\n");
- fprintf(stdout, "Startig loop\n");
- module_main(data, data_size);
-
- while(should_exit == 0) {
- sleep(1);
- }
- should_exit = 0;
- dlclose(module);
- fprintf(stderr, "Continue?\n");
- //}
-
- return 0;
-}
diff --git a/payredu.c b/payredu.c
@@ -0,0 +1,72 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <dlfcn.h>
+#include <signal.h>
+#include <unistd.h>
+
+#include <book.h>
+
+#define MAX_MEMORY 4064
+#define WINDOW_WIDTH 512
+#define WINDOW_HEIGHT 512
+#define BUFFER_SIZE 256
+
+int should_exit = 0;
+
+void sig_handle() {
+ printf("Reloaded\n");
+ system("date");
+ should_exit = 1;
+}
+
+typedef void* (*module_main_func)(const char*, size_t);
+
+int main(int argc, char* argv[]) {
+ FILE* in = fopen("october-2023.txt", "r");
+ char* data = (char*)malloc(2048 * sizeof(char));
+ size_t data_size = 0;
+ size_t c_read = 0;
+ while((c_read = fread(data + data_size + 0, 1, BUFFER_SIZE, in)) != 0) {
+ data_size += c_read;
+ }
+ if (ferror(in)) fprintf(stderr, "Error reading file\n");
+ fprintf(stdout, "Startig loop\n");
+ ledger_parse_data(data, data_size);
+ return 0;
+}
+
+
+/*
+int main(int argc, char* argv[]) {
+ signal(SIGQUIT, sig_handle);
+ //while(1) {
+ void* module = dlopen("./libbook.so", RTLD_NOW);
+ while(module == NULL){
+ fprintf(stderr, "Failed to load module. (%s)\n", dlerror());
+ fprintf(stderr, "Press return to try again.\n");
+ getchar();
+ module = dlopen("./libbook.so", RTLD_NOW);
+ }
+ module_main_func module_main = dlsym(module, "module_main");
+ FILE* in = fopen("october-2023.txt", "r");
+ char* data = (char*)malloc(2048 * sizeof(char));
+ size_t data_size = 0;
+ size_t c_read = 0;
+ while((c_read = fread(data + data_size + 0, 1, BUFFER_SIZE, in)) != 0) {
+ data_size += c_read;
+ }
+ if (ferror(in)) fprintf(stderr, "Error reading file\n");
+ fprintf(stdout, "Startig loop\n");
+ module_main(data, data_size);
+
+ while(should_exit == 0) {
+ sleep(1);
+ }
+ should_exit = 0;
+ dlclose(module);
+ fprintf(stderr, "Continue?\n");
+ //}
+
+ return 0;
+}
+*/
diff --git a/tests/account_tree.c b/tests/account_tree.c
@@ -0,0 +1,47 @@
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <account.h>
+
+/* TEST_INPUT
+this:is:a:account
+virtualaccount:
+virtualaccount
+this:is:
+TEST_INPUT */
+
+void account_walk (map_tree_t* rootp)
+{
+ static int tab_acc;
+ if (rootp == NULL) return;
+ for(int i = 0; i < tab_acc; i++) {
+ printf(" ");
+ }
+ vstr_t *val = rootp->value;
+ if (val != NULL) {
+ printf("%.*s\n", val->len, val->str);
+ tab_acc++;
+ }
+ for (int i = 0; i < rootp->children_len; i++) {
+ account_walk(rootp->children + i);
+ }
+ tab_acc--;
+}
+
+
+int main(int argc, char* argv[]) {
+ map_tree_t* account_tree = NULL;
+ for(int i = 1; i < argc; i++) {
+ account_add(&account_tree, argv[i], strlen(argv[i]));
+ }
+ account_walk(account_tree);
+}
+
+/* TEST_OUTPUT
+this:
+ is:
+ a:
+ account
+virtualaccount:
+virtualaccount
+TEST_OUTPUT */
diff --git a/tests/csv_export.c b/tests/csv_export.c
@@ -0,0 +1,51 @@
+#include <stdio.h>
+#include <book.h>
+
+/* TEST_INPUT
+october-2023.txt
+TEST_INPUT */
+
+int main(int argc, char* argv[]) {
+ char* content = (char*)malloc(10000);
+ size_t content_len = 0;
+ //if (argc < 2) {
+ // fprintf(stderr, "Usage: %s filename.txt\n", argv[0]);
+ // return 1;
+ //}
+ //FILE* fp = fopen(argv[1], "r");
+ FILE* fp = fopen("october-2023.txt", "r");
+ if (fp == NULL) {
+ printf("Cannot open file\n");
+ goto export_csv_cleanup;
+ }
+ size_t content_read = 0;
+ char* content_ptr = content;
+ while ( (content_read = fread(content_ptr, 1, 1, fp)) != 0) {
+ content_ptr += content_read;
+ }
+ content_len = content_ptr - content;
+
+ printf("%s\n%ld", content, content_len);
+
+ ledger_parse_data(content, content_len);
+
+export_csv_cleanup:
+ if (fp != NULL) fclose(fp);
+ return 0;
+}
+
+/* TEST_OUTPUT
+"2015/10/12","","Exxon","Liabilities:MasterCard","$","-10","",""
+"2015/10/12","","Exxon","Expenses:Auto:Gas","$","10","",""
+"2015/10/12","","Donna's Cake World","Liabilities:Credit","$","-5","",""
+"2015/10/12","","Donna's Cake World","Expenses:Food:Dessert","$","5","",""
+"2024/10/25","","Zaitoon","Liabilities:Credit","$","-5","",""
+"2024/10/25","","Zaitoon","Expenses:Food:Dinner","$","5","",""
+"2024/10/24","","Donna's Cake World","Expenses:Food:Dessert","$","5","",""
+"2024/10/24","","Donna's Cake World","Expenses:Food:Dinner","$","19","",""
+"2024/10/24","","Donna's Cake World","Assets:Cash","$","-24","",""
+"2015/10/12","","Zoho","Income:Salary","$","-10000","",""
+"2015/10/12","","Zoho","Assets:Bank:Checking","$","10000","",""
+"2025/10/20","","Old loan from friend","Assets:Cash","$","200","",""
+"2025/10/20","","Old loan from friend","Friend:Cash","$","-200","",""
+TEST_OUTPUT */
diff --git a/tests/tests.mk b/tests/tests.mk
@@ -0,0 +1,11 @@
+awk_query = $$(awk '/\/\* $1/{flag=1; next}/$1 \*\//{flag=0}flag' $2.c)
+test_cmd = if [ "$$($< $(call awk_query,TEST_INPUT,$<))" = "$(call awk_query,TEST_OUTPUT,$<)" ]; \
+ then echo Passed; \
+ else echo Failed; \
+ fi
+
+%.test: %
+ @printf "Test: $<... "
+ @$(call test_cmd)
+
+test: $(addsuffix .test,$(basename $(wildcard tests/*.c)))