payredu

[WIP] Cross-platform ledger GUI written in c99
git clone git@nonplanar.org:payredu.git
Log | Files | Refs | README

book.c (9043B)


      1 #include <stdio.h>
      2 #include <stddef.h>
      3 #include <stdlib.h>
      4 #include <inttypes.h>
      5 #include <ctype.h>
      6 #include <limits.h>
      7 #include <errno.h>
      8 
      9 #include "common.h"
     10 #include "strn.h"
     11 
     12 #ifndef _WIN32
     13 #include <unistd.h>
     14 #endif
     15 
     16 #define _XOPEN_SOURCE
     17 #include <time.h>
     18 
     19 #include "vstr.h"
     20 #include <account.h>
     21 #include <book.h>
     22 
     23 #define BUFFER_SIZE 256
     24 
     25 #define warning(STR) \
     26 	fprintf(stdout, "\033[31m"STR"\033[0m");
     27 
     28 #define warningf(STR,...) \
     29 	fprintf(stdout, "\033[31m"STR"\033[0m", __VA_ARGS__);
     30 
     31 vstr_t tags[100] = { 0 };
     32 
     33 size_t tags_size = 0;
     34 
     35 /*
     36  * char* tags = { "Expenses:Auto:Gas", "Liabilities:MasterCard",
     37  * "Assets:Credit" };
     38  */
     39 
     40 /**
     41  *
     42  read the file | parse for newlines
     43  2023/10/24 - 5, fptr + 0, fptr + 6, fptr + 9
     44  Entry* entry = get_entry("2023/10/24");
     45 
     46  skip \n
     47  read until date, and read until from
     48 
     49  **/
     50 
     51 // commodity max size 256
     52 // commodity name max size 4
     53 char commodity_list[256][8];
     54 
     55 map_tree_t *rootp = NULL;
     56 
     57 
     58 // store numbers in the least denom
     59 // 1.50$ == 150
     60 // 2$ == 200
     61 // 2.23$ == 200
     62 
     63 typedef struct {
     64 	vstr_t *denom;
     65 	size_t quantity;
     66 	int8_t decimal;
     67 } LedgerValue;
     68 
     69 typedef struct {
     70 	vstr_t *reg;
     71 	LedgerValue val;
     72 } LedgerRecord;
     73 
     74 typedef struct {
     75 	time_t date;		// the date associated with the entry
     76 	vstr_t comment;		// comment associated with the entry
     77 	LedgerRecord **records;
     78 } LedgerEntry;
     79 
     80 time_t
     81 ledger_timestamp_from_ledger_date(char *date_str)
     82 {
     83 	// converts string 'YYYY-MM-DD' to unix timestamp
     84 	// date_str should be exactly 10 characters
     85 	// printf("%.*s\n", 4, date_str);
     86 	struct tm tm = { 0 };
     87 	tm.tm_year = natoi(date_str, 4) - 1900;
     88 	tm.tm_mon = natoi(date_str + 5, 2) - 1;
     89 	tm.tm_mday = natoi(date_str + 8, 2);
     90 	// warning("tm_year %d, tm_mon: %d, tm_mday: %d", natoi(date_str, 4),
     91 	// tm.tm_mon, tm.tm_mday);
     92 	return mktime(&tm);
     93 }
     94 
     95 const char *states_str[] = {
     96 	"DATE",
     97 	"COMMENT",
     98 	"ENTRY START",
     99 	"ENTRY SPACE",
    100 	"ENTRY WHO",
    101 	"ENTRY AMOUNT",
    102 	"ENTRY DENOM",
    103 	"ENTRY END",
    104 };
    105 
    106 typedef enum {
    107 	DATE,
    108 	COMMENT,
    109 	ENTRY_START, // entry starts after a comment
    110 	ENTRY_SPACE,
    111 	ENTRY_WHO,
    112 	ENTRY_AMOUNT,
    113 	ENTRY_DENOM ,
    114 	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
    115 	POSTING_END,
    116 } LedgerParseStates;
    117 
    118 
    119 void
    120 ledger_parse_data(char *text, size_t text_len)
    121 {
    122 	char *denom_list[256];
    123 	memset(denom_list, 0, 256);
    124 	char* denomptr = NULL;
    125 	setvbuf(stdout, NULL, _IONBF, 0);
    126 
    127 	LedgerParseStates state = DATE;
    128 	size_t line_no = 1;
    129 	size_t current_column = 1;
    130 	time_t t = time(NULL);
    131 	size_t i = 0;
    132 	// TODO it may be possible to push these to the tree itself, explore the possibility
    133 	// these act as temporary register until we push back the entry to a tree
    134 	time_t hold_date;
    135 	vstr_t hold_comment = { 0 };
    136 	vstr_t hold_register = { 0 };
    137 	long hold_amount = LONG_MAX;
    138 	char hold_sign = 0;
    139 	size_t hold_denom_id = { 0 };
    140 	short n_count = 0;
    141 	char hold_fquantity = 0;
    142 
    143 	hold_amount = LONG_MAX;
    144 
    145 	setvbuf(stdout, NULL, _IONBF, 0);
    146 	// TODO it may be possible to push these to the tree itself, explore the possibility
    147 	// these act as temporary register until we push back the entry to a tree
    148 
    149 	while (i < text_len) {
    150 		char c = text[i];
    151 		/* \n identifies an entry done in ledger */
    152 		switch (c) {
    153 			case '\n':
    154 			case '\r':
    155 				line_no++;
    156 				n_count++;
    157 				switch (state) {
    158 					// after parsing the amount seq, we set the state to ENTRY_WHO
    159 					case ENTRY_WHO:
    160 					case ENTRY_END:
    161 						hold_sign = 0;
    162 						hold_amount = LONG_MAX;
    163 						hold_denom_id = 0;
    164 						// if entry_count <= 1 throw error
    165 						if (text[i - 1] == '\n') {
    166 							state = DATE;
    167 							// TODO push the entries to stack or somethin
    168 							warning("\n==\n");
    169 							// state = POSTING_END;
    170 							break;
    171 						}
    172 						state = ENTRY_WHO;
    173 						warning(",");
    174 						break;
    175 					case COMMENT:
    176 						state = ENTRY_START;
    177 						break;
    178 					case ENTRY_DENOM:
    179 						//warningf("%s", "denom not found, setting state WHO");
    180 						state = ENTRY_WHO;
    181 						break;
    182 					case ENTRY_AMOUNT:
    183 						goto ledger_parse_error_handle;
    184 						break;
    185 				}
    186 				i++;
    187 				continue;
    188 			case ' ':
    189 			case '\t':
    190 				n_count = 0;
    191 				i++;
    192 				continue;
    193 				break;
    194 		}
    195 		n_count = 0;
    196 		// next state
    197 		switch (state) {
    198 			case DATE:
    199 				if (isdigit(c)) {
    200 					// try to parse a date
    201 					time_t tn = ledger_timestamp_from_ledger_date(text + i);
    202 					warningf("%.*s: %ld	", 10, text + i, (long)tn);
    203 					// date is expected to have the form DD/MM/YYYY (10)
    204 					i += 10;
    205 					if (tn == (time_t) - 1) goto ledger_parse_error_handle;
    206 					state = COMMENT;
    207 				}
    208 				break;
    209 			case COMMENT:
    210 				if (isalnum(c)) {
    211 					// we hit alphanumerical after whitespace
    212 					vstr_t comment = {
    213 						.str = text + i,
    214 						.len = 0
    215 					};
    216 					while (i < text_len && *(text + i) != '\n') {
    217 						i++;
    218 						comment.len++;
    219 					}
    220 					warningf("Comment: %.*s", comment.len,
    221 							comment.str);
    222 					state = ENTRY_START;
    223 				}
    224 				break;
    225 			case ENTRY_START:
    226 			case ENTRY_WHO:
    227 				{
    228 					// add this to register
    229 					size_t who_len = i;
    230 					vstr_t who = {
    231 						.str = text + i,
    232 						.len = 0
    233 					};
    234 					while (i < text_len) {
    235 						switch (text[i]) {
    236 							case '\n':
    237 							case '\r':
    238 								goto ledger_who_parsed;
    239 								break;
    240 							case '\t':
    241 							case ' ':
    242 								goto ledger_who_parsed;
    243 								break;
    244 							default:
    245 								i++;
    246 								break;
    247 						}
    248 					}
    249 ledger_who_parsed:
    250 					who_len = i - who_len;
    251 					account_add(&rootp, who.str, who_len);
    252 					warningf("\n@%d Who=%.*s", i, who_len, who);
    253 					state = ENTRY_DENOM;
    254 					/* TODO add to tags here */
    255 				}
    256 				break;
    257 			case ENTRY_DENOM: {
    258 				char _c;
    259 				if (hold_denom_id) {
    260 					warning("Denom already parsed\n");
    261 					goto ledger_parse_error_handle;
    262 				}
    263 				char *denom = text + i;
    264 				size_t denom_len = 0;
    265 
    266 				if (*denom == '-') {
    267 					if (hold_sign) goto ledger_parse_error_handle;
    268 					hold_sign = 1;
    269 					i++;
    270 					state = ENTRY_DENOM;
    271 					break;
    272 				}
    273 
    274 				warningf(" @%d D", i + 1);
    275 
    276 				while (i < text_len &&
    277 						( isalpha(*(text + i))
    278 				/* TODO Search denomlist instead of just checking '$' */
    279 						 || *(text + i) == '$')) i++;
    280 				denom_len = (text + i) - denom;
    281 				/* FIXME Use proper id */
    282 				hold_denom_id = *denom;//get_denom_id(denom, denom_len);
    283 				// hold_denom_len = denom_len;
    284 				state = hold_amount == LONG_MAX? ENTRY_AMOUNT : ENTRY_END;
    285 				warningf("#%d(%.*s)", denom_len, denom_len, denom);
    286 				break;
    287 			}
    288 			case ENTRY_AMOUNT: {
    289 				char _c;
    290 				if (hold_amount != LONG_MAX) {
    291 					warning("Amount already parsed\n");
    292 					goto ledger_parse_error_handle;
    293 				}
    294 
    295 				char *amount = text + i;
    296 				size_t amount_len = 0;
    297 
    298 				char *eptr = NULL;
    299 				char *efptr = NULL;
    300 
    301 				if (*amount == '-') {
    302 					if (hold_sign) goto ledger_parse_error_handle;
    303 					hold_sign = 1;
    304 					i++;
    305 					state = ENTRY_AMOUNT;
    306 					break;
    307 				}
    308 
    309 				warningf(" @%d A", i + 1);
    310 
    311 				hold_amount = strtol(amount, &eptr, 10);
    312 
    313 				if (errno == ERANGE) {
    314 					perror("FATAL: Big ints are not supported at the moment");
    315 					exit(-1);
    316 				}
    317 				if (errno) {
    318 					perror("Some unknown error occured while parsing quantity");
    319 					exit(-1);
    320 				}
    321 
    322 				amount_len = eptr - amount;
    323 
    324 				if (*eptr == ',') *eptr = '.';
    325 				if (amount_len && *eptr == '.') {
    326 					float temp = strtof(eptr, &efptr);
    327 					if (efptr == eptr) {
    328 						/*
    329 						TODO Check if '.' should end a transaction
    330 						TODO Handle @
    331 						*/
    332 						printf("Searching for decimal value got '%c'\n", *efptr);
    333 						exit(-1);
    334 					}
    335 
    336 					if (efptr - eptr > 3) {
    337 						warning("FATAL: Only 2 decimal places are supported at the moment\n");
    338 						exit(-1);
    339 					}
    340 					hold_fquantity = (char) (temp * 100);
    341 					eptr = efptr;
    342 				}
    343 
    344 				amount_len = eptr - amount;
    345 				i += amount_len;
    346 
    347 				/* number parse failed */
    348 				if (eptr == amount) {
    349 					if (hold_denom_id) goto ledger_parse_error_handle;
    350 					state = ENTRY_DENOM;
    351 					break;
    352 				}
    353 
    354 				state = hold_denom_id ? ENTRY_END : ENTRY_DENOM;
    355 				warningf("#%d(%.*s)", amount_len, amount_len, amount);
    356 				}
    357 				break;
    358 			default:
    359 				goto ledger_parse_error_handle;
    360 		}
    361 	}
    362 	warning("read complete\n");
    363 	return;
    364 ledger_parse_error_handle:
    365 	warningf("\nParse failed @%d line:%ld, Expected %s, got '%c'\n",
    366 			i, line_no, states_str[state], text[i]);
    367 }
    368 
    369 Entry**
    370 ledger_read_file(const char* filename, time_t date_start, time_t date_end) {
    371 	Entity me = {"Account:Income"};
    372 	// list population, read from
    373 	FILE* file = fopen(filename, "r");
    374 	if (file == NULL) {
    375 		printf("Failed to open file %s\n", filename);
    376 	}
    377 	Entry** new_list = (Entry**)malloc(sizeof(Entry*) * 12);
    378 	for(int i = 0; i < 12; i++) {
    379 		Entry* entry = (Entry*)malloc(sizeof(Entry));
    380 		new_list[i] = entry;
    381 		entry->from = &me;
    382 		entry->to = (Entity*)malloc(sizeof(Entity));
    383 		entry->to->name = (char*)malloc(sizeof(char*) * 20);
    384 		strncpy(entry->to->name, "Man", 3);
    385 	}
    386 	return new_list;
    387 }
    388 
    389 void
    390 *module_main(char *data, size_t data_len)
    391 {
    392 	// printf("%s\n", data);
    393 	warning("\n=======| Startality |=======\n");
    394 	ledger_parse_data(data, data_len);
    395 	warning("\n========| Fatality |========\n");
    396 	return NULL;
    397 }
    398