payredu

[WIP] Cross-platform ledger GUI written in c99

commit 8b5eb06ff3db5f05ca5bebc5a57da3eb298ee719
parent 2a0b60d627b59c3a4eff1f72306707e246bfafb8
Author: Bharatvaj Hemanth <bharatvaj@yahoo.com>
Date: Wed, 26 Mar 2025 07:28:42 +0530

Amount, sign and denom parsing works!

Add support decimals with upto two decimal places

Move commented hot-reload code from payredu.c to hot.c

Fix hot-reload debugging
6 files changed, 176 insertions(+), 134 deletions(-)
M
CHANGELOG
|
9
++++++++-
M
Makefile
|
4
++--
M
book.c
|
206
++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
M
hot.c
|
47
+++++++++++++++++++++++++++++++++++++++++++----
M
october-2023.txt
|
2
+-
M
payredu.c
|
42
++----------------------------------------
diff --git a/CHANGELOG b/CHANGELOG
@@ -1,6 +1,13 @@
+0.2 (2025/03/27)
+---
+- Amount, sign and denom parsing works!
+- Add support decimals with upto two decimal places
+- Move commented hot-reload code from payredu.c to hot.c
+- Fix hot-reload debugging
+
 Dark Ages
 ---------
-- Starting keeping CHANGELOG file for payredo
+- Start keeping CHANGELOG file for payredu
 - 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
@@ -19,7 +19,7 @@ bal:
 
 balance: balance.c ledger.h
 
-hot: hot.c libbalance.so
+hot: hot.c libbook.so
 
 libbook.a: book.o account.o
 	ar cr $@ $>

@@ -36,4 +36,4 @@ format:
 include tests/tests.mk
 
 clean:
-	-rm *.so *.o hotbook libbook.so $(TESTS)
+	-rm *.so *.o hotbook libbook.a libbook.so $(TESTS)
diff --git a/book.c b/book.c
@@ -4,27 +4,13 @@
 #include <inttypes.h>
 #include <ctype.h>
 #include <limits.h>
+#include <errno.h>
+
 #include "common.h"
 #include "strn.h"
 
 #include <unistd.h>
 
-#ifdef ENABLE_GUI
-#include <GLFW/glfw3.h>
-#define NK_INCLUDE_FIXED_TYPES
-#define NK_INCLUDE_STANDARD_IO
-#define NK_INCLUDE_STANDARD_VARARGS
-#define NK_INCLUDE_DEFAULT_ALLOCATOR
-#define NK_INCLUDE_VERTEX_BUFFER_OUTPUT
-#define NK_INCLUDE_FONT_BAKING
-#define NK_INCLUDE_DEFAULT_FONT
-#define NK_IMPLEMENTATION
-#define NK_GLFW_GL2_IMPLEMENTATION
-#define NK_KEYSTATE_BASED_INPUT
-#include <nuklear.h>
-#include <nuklear_glfw_gl2.h>
-#endif
-
 #define _XOPEN_SOURCE
 #include <time.h>
 

@@ -74,7 +60,8 @@ map_tree_t *rootp = NULL;
 
 typedef struct {
 	vstr_t *denom;
-	size_t amount;
+	size_t quantity;
+	int8_t decimal;
 } LedgerValue;
 
 typedef struct {

@@ -88,7 +75,8 @@ typedef struct {
 	LedgerRecord **records;
 } LedgerEntry;
 
-time_t ledger_timestamp_from_ledger_date(char *date_str)
+time_t
+ledger_timestamp_from_ledger_date(char *date_str)
 {
 	// converts string 'YYYY-MM-DD' to unix timestamp
 	// date_str should be exactly 10 characters

@@ -108,12 +96,8 @@ const char *states_str[] = {
 	"ENTRY START",
 	"ENTRY SPACE",
 	"ENTRY WHO",
-	"ENTRY SIGN",
-	"ENTRY SIGN OR AMOUNT",
 	"ENTRY AMOUNT",
 	"ENTRY DENOM",
-	"ENTRY DENOM OR AMOUNT",
-	"ENTRY SIGN OR DENOM OR AMOUNT",
 	"ENTRY END",
 };
 

@@ -123,19 +107,19 @@ typedef enum {
 	ENTRY_START, // entry starts after a comment
 	ENTRY_SPACE,
 	ENTRY_WHO,
-	ENTRY_SIGN,
-	ENTRY_SIGN_AMOUNT,
 	ENTRY_AMOUNT,
 	ENTRY_DENOM ,
-	ENTRY_DENOM_AMOUNT,
-	ENTRY_SIGN_DENOM_AMOUNT,
 	ENTRY_END, // finish up entry if encountering any \n\n or \n text_len == i or text_len == i, otherwise set state to ENTRY_SPACE
 	POSTING_END,
 } LedgerParseStates;
 
 
-void ledger_parse_data(char *text, size_t text_len)
+void
+ledger_parse_data(char *text, size_t text_len)
 {
+	char *denom_list[256];
+	memset(denom_list, 0, 256);
+	char* denomptr = NULL;
 	setvbuf(stdout, NULL, _IONBF, 0);
 	LedgerParseStates state = DATE;
 	size_t line_no = 1;

@@ -148,14 +132,17 @@ void ledger_parse_data(char *text, size_t text_len)
 	time_t hold_date;
 	vstr_t hold_comment = { 0 };
 	vstr_t hold_register = { 0 };
-	long int hold_amount = LONG_MAX;
-	short hold_sign = -1;
+	long hold_amount = LONG_MAX;
+	char hold_sign = 0;
 	size_t hold_denom_id = { 0 };
 	short n_count = 0;
+	char hold_fquantity = 0;
+
+	hold_amount = LONG_MAX;
 
 	while (i < text_len) {
 		char c = text[i];
-		// we use \n to identify entry done in ledger
+		/* \n identifies an entry done in ledger */
 		switch (c) {
 			case '\n':
 			case '\r':

@@ -165,25 +152,23 @@ void ledger_parse_data(char *text, size_t text_len)
 					// after parsing the amount seq, we set the state to ENTRY_WHO
 					case ENTRY_WHO:
 					case ENTRY_END:
-						hold_sign = -1;
+						hold_sign = 0;
 						hold_amount = LONG_MAX;
+						hold_denom_id = 0;
 						// if entry_count <= 1 throw error
 						if (text[i - 1] == '\n') {
 							state = DATE;
 							// TODO push the entries to stack or somethin
 							warning("\n==\n");
 							// state = POSTING_END;
-						} else {
-							state = ENTRY_WHO;
-							warning(",");
+							break;
 						}
+						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", "denom not found, setting state WHO");
 						state = ENTRY_WHO;

@@ -218,17 +203,15 @@ void ledger_parse_data(char *text, size_t text_len)
 			case COMMENT:
 				if (isalnum(c)) {
 					// we hit alphanumerical after whitespace
-					size_t comment_len = 0;
 					vstr_t comment = {
 						.str = text + i,
 						.len = 0
 					};
 					while (i < text_len && *(text + i) != '\n') {
 						i++;
-						comment_len++;
+						comment.len++;
 					}
-					comment.len = comment_len;
-					warningf("Comment: %.*s", comment_len,
+					warningf("Comment: %.*s", comment.len,
 							comment);
 					state = ENTRY_START;
 				}

@@ -260,63 +243,110 @@ void ledger_parse_data(char *text, size_t text_len)
 ledger_who_parsed:
 					who_len = i - who_len;
 					account_add(&rootp, who.str, who_len);
-					warningf("\n(%d) Who: %.*s", i, who_len, who);
-					state = ENTRY_SIGN_DENOM_AMOUNT;
-					// add to tags here
+					warningf("\n@%d Who=%.*s", i, who_len, who);
+					state = ENTRY_DENOM;
+					/* TODO add to tags here */
 				}
 				break;
-			case ENTRY_SIGN_DENOM_AMOUNT:
-				if (*(text + i) == '-' ) {
-					// TODO throw already set error
-					if (hold_sign >= 0) goto ledger_parse_error_handle;
-					state = ENTRY_SIGN;
-				} else if (isdigit(*(text + i))) state = ENTRY_AMOUNT;
-				else state = ENTRY_DENOM;
-				continue;
-			case ENTRY_SIGN_AMOUNT:
-				if (*(text + i) == '-' ) {
-					// TODO throw already set error
-					if (hold_sign >= 0) goto ledger_parse_error_handle;
-					state = ENTRY_SIGN;
-				} else if (isdigit(*(text + i))) state = ENTRY_AMOUNT;
-				else goto ledger_parse_error_handle;
-				break;
-			case ENTRY_SIGN: {
-				if (*(text + i) == '-') {
-					   i++;
-					   // AMOUNT cannot be set before SIGN
-					   if (hold_amount != LONG_MAX) goto ledger_parse_error_handle;
-					   hold_sign = 1;
-					   state = ENTRY_SIGN_DENOM_AMOUNT;
-				}
-			 } break;
 			case ENTRY_DENOM: {
 				char _c;
-				warningf("  %d: D:", i + 1);
+				if (hold_denom_id) {
+					warning("Denom already parsed\n");
+					goto ledger_parse_error_handle;
+				}
 				char *denom = text + i;
 				size_t denom_len = 0;
+
+				if (*denom == '-') {
+					if (hold_sign) goto ledger_parse_error_handle;
+					hold_sign = 1;
+					i++;
+					state = ENTRY_DENOM;
+					break;
+				}
+
+				warningf(" @%d D", i + 1);
+
 				while (i < text_len &&
 						( isalpha(*(text + i))
+				/* TODO Search denomlist instead of just checking '$' */
 						 || *(text + i) == '$')) i++;
 				denom_len = (text + i) - denom;
-				if (hold_amount == LONG_MAX)
-					state = hold_sign? ENTRY_AMOUNT: ENTRY_SIGN_AMOUNT;
-				else
-					state = ENTRY_END;
-				warningf(" %.*s(%d)", denom_len, denom, denom_len);
+				/* FIXME Use proper id */
+				hold_denom_id = *denom;//get_denom_id(denom, denom_len);
+				// hold_denom_len = denom_len;
+				state = hold_amount == LONG_MAX? ENTRY_AMOUNT : ENTRY_END;
+				warningf("#%d(%.*s)", denom_len, denom_len, denom);
 				break;
 			}
 			case ENTRY_AMOUNT: {
 				char _c;
-				warningf("  %d A:", i + 1);
+				if (hold_amount != LONG_MAX) {
+					warning("Amount already parsed\n");
+					goto ledger_parse_error_handle;
+				}
+
 				char *amount = text + i;
 				size_t amount_len = 0;
-				while (i < text_len  &&  (_c = *(text + i)) == '.' || isdigit(_c) || _c == ',') i++;
-				amount_len = (text + i) - amount;
-				// TODO convert amount to hold_amount integer
-				hold_amount = 0;
-				state = hold_denom_id == 0? ENTRY_DENOM : ENTRY_END;
-				warningf(" %.*s(%d)", amount_len, amount, amount_len);
+
+				char *eptr = NULL;
+				char *efptr = NULL;
+
+				if (*amount == '-') {
+					if (hold_sign) goto ledger_parse_error_handle;
+					hold_sign = 1;
+					i++;
+					state = ENTRY_AMOUNT;
+					break;
+				}
+
+				warningf(" @%d A", i + 1);
+
+				hold_amount = strtol(amount, &eptr, 10);
+
+				if (errno == ERANGE) {
+					perror("FATAL: Big ints are not supported at the moment");
+					exit(-1);
+				}
+				if (errno) {
+					perror("Some unknown error occured while parsing quantity");
+					exit(-1);
+				}
+
+				amount_len = eptr - amount;
+
+				if (*eptr == ',') *eptr = '.';
+				if (amount_len && *eptr == '.') {
+					float temp = strtof(eptr, &efptr);
+					if (efptr == eptr) {
+						/*
+						TODO Check if '.' should end a transaction
+						TODO Handle @
+						*/
+						printf("Searching for decimal value got '%c'\n", *efptr);
+						exit(-1);
+					}
+
+					if (efptr - eptr > 3) {
+						warning("FATAL: Only 2 decimal places are supported at the moment\n");
+						exit(-1);
+					}
+					hold_fquantity = (char) (temp * 100);
+					eptr = efptr;
+				}
+
+				amount_len = eptr - amount;
+				i += amount_len;
+
+				/* number parse failed */
+				if (eptr == amount) {
+					if (hold_denom_id) goto ledger_parse_error_handle;
+					state = ENTRY_DENOM;
+					break;
+				}
+
+				state = hold_denom_id ? ENTRY_END : ENTRY_DENOM;
+				warningf("#%d(%.*s)", amount_len, amount_len, amount);
 				}
 				break;
 			default:

@@ -326,10 +356,12 @@ ledger_who_parsed:
 	warning("read complete\n");
 	return;
 ledger_parse_error_handle:
-	warningf("Parse failed at %ld b:(%d), Expected %s, got '%c'",
-			line_no, i, states_str[state], text[i]);
+	warningf("\nParse failed @%d line:%ld, Expected %s, got '%c'\n",
+			i, line_no, states_str[state], text[i]);
 }
-Entry** ledger_read_file(const char* filename, time_t date_start, time_t date_end) {
+
+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");

@@ -348,10 +380,12 @@ Entry** ledger_read_file(const char* filename, time_t date_start, time_t date_en
 	return new_list;
 }
 
-void *module_main(char *data, size_t data_len)
+void
+*module_main(char *data, size_t data_len)
 {
 	// printf("%s\n", data);
 	warning("\n=======| Startality |=======\n");
 	ledger_parse_data(data, data_len);
 	warning("\n========| Fatality |========\n");
 }
+
diff --git a/hot.c b/hot.c
@@ -2,7 +2,9 @@
 #include <stdlib.h>
 #include <dlfcn.h>
 #include <signal.h>
+#include <unistd.h>
 
+#ifdef ENABLE_GUI
 #include <GLFW/glfw3.h>
 #define NK_INCLUDE_FIXED_TYPES
 #define NK_INCLUDE_STANDARD_IO

@@ -16,18 +18,24 @@
 #define NK_KEYSTATE_BASED_INPUT
 #include <nuklear.h>
 #include <nuklear_glfw_gl2.h>
+#endif
+
 #include "common.h"
 
 #define MAX_MEMORY 4064
 #define WINDOW_WIDTH 512
 #define WINDOW_HEIGHT 512
+#define BUFFER_SIZE 256
 
 void* state = NULL;
 int should_exit = 0;
 // init gui state
 int width,height;
+
+#ifdef ENABLE_GUI
 struct nk_context* ctx;
 GLFWwindow* win;
+#endif
 
 void sig_handle() {
 	printf("Reloaded\n");

@@ -35,13 +43,15 @@ void sig_handle() {
 	should_exit = 1;
 }
 
-typedef void* (*module_main_func)(void*, int, int);
+typedef void* (*module_main_func)(void*, int);
 
 static void error_callback(int e, const char *d)
 {printf("Error %d: %s\n", e, d);}
 
-int main(int argc, char* argv[]) {
+int
+main(int argc, char* argv[]) {
 	signal(SIGQUIT, sig_handle);
+#ifdef ENABLE_GUI
 	glfwSetErrorCallback(error_callback);
 	if (!glfwInit()) {
 		fprintf(stdout, "[GFLW] failed to init!\n");

@@ -64,12 +74,12 @@ int main(int argc, char* argv[]) {
 		nk_style_set_font(ctx, &droid->handle);
 	}
 	while(1) {
-		void* module = dlopen("./libbalance.so", RTLD_NOW);
+		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("./libbalance.so", RTLD_NOW);
+			module = dlopen("./libbook.so", RTLD_NOW);
 		}
 		module_main_func module_main = dlsym(module, "module_main");
 		while (!glfwWindowShouldClose(win) && !should_exit) {

@@ -90,6 +100,35 @@ int main(int argc, char* argv[]) {
 		dlclose(module);
 		should_exit = 0;
 	}
+#else
+	//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");
+	//}
+#endif
 	return 0;
 }
+
diff --git a/october-2023.txt b/october-2023.txt
@@ -21,4 +21,4 @@
 
 2015/10/12 Zoho
 	Income:Salary
-	Assets:Bank:Checking	$10,000
+	Assets:Bank:Checking	$10,00
diff --git a/payredu.c b/payredu.c
@@ -7,8 +7,6 @@
 #include <book.h>
 
 #define MAX_MEMORY 4064
-#define WINDOW_WIDTH 512
-#define WINDOW_HEIGHT 512
 #define BUFFER_SIZE 256
 
 int should_exit = 0;

@@ -19,9 +17,8 @@ void sig_handle() {
 	should_exit = 1;
 }
 
-typedef void* (*module_main_func)(const char*, size_t);
-
-int main(int argc, char* argv[]) {
+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;

@@ -35,38 +32,3 @@ int main(int argc, char* argv[]) {
 	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;
-}
-*/