sw.c (8162B)
1 /* See LICENSE file for copyright and license details. 2 * sw is a simple wallet management tool for the terminal. */ 3 4 #define _XOPEN_SOURCE 5 #define _GNU_SOURCE 6 #include <stdarg.h> 7 #include <stdio.h> 8 #include <stdlib.h> 9 #include <string.h> 10 #include <time.h> 11 #include <limits.h> 12 13 #include "arg.h" 14 char *argv0; 15 16 enum {F_DATEFROM, F_DATETO, F_TEXT, F_NOTEXT}; 17 18 typedef union { 19 int i; 20 unsigned int ui; 21 float f; 22 const void *v; 23 } Arg; 24 25 typedef struct Movement Movement; 26 struct Movement { 27 int id; 28 int ts; 29 int filtered; 30 float amount; 31 char note[64]; 32 Movement *next; 33 }; 34 35 typedef struct { 36 float amount, pamount; 37 float income, pincome; 38 float expense, pexpense; 39 int count, pcount; 40 } Totals; 41 42 typedef struct Filter Filter; 43 struct Filter { 44 int type; 45 Arg arg; 46 Filter *next; 47 }; 48 49 /* function declarations */ 50 int addmov(char *date, float amount, char *note); 51 void addfilter(unsigned int type, void *data); 52 void attach(Movement *m); 53 void attachfltr(Filter *f); 54 void attach_sorted_desc(Movement **head, Movement *m); 55 void deletemov(int id); 56 void detach(Movement *m); 57 void detachfltr(Filter *f); 58 void die(const char *errstr, ...); 59 void *ecalloc(size_t nmemb, size_t size); 60 int filtermov(Movement *mov); 61 int filtermovs(int from, int to, char *txt); 62 void freemovs(void); 63 void freefltrs(void); 64 void loadmovs(void); 65 void refresh(void); 66 void savemovs(void); 67 void showmovs(void); 68 void sortmovs(void); 69 int strtoint(char *s); 70 int strtots(char *s); 71 void usage(void); 72 73 /* variables */ 74 Movement *movs; 75 Filter *filters; 76 Totals totals; 77 FILE *movsfile; 78 char movsfilename[256]; 79 int limit = 25; 80 int filtered = 0; 81 int nfilters = 0; 82 83 /* function implementations */ 84 int 85 addmov(char *date, float amount, char *note) { 86 Movement *m; 87 int id = 0; 88 89 for(m = movs; m; m = m->next) 90 if(m->id > id) 91 id = m->id; 92 ++id; 93 m = ecalloc(1, sizeof(Movement)); 94 m->id = id; 95 m->ts = strtots(date); 96 m->amount = amount; 97 memcpy(m->note, note, sizeof(m->note)); 98 m->note[ sizeof(m->note) - 1] = '\0'; 99 attach(m); 100 return 0; 101 } 102 103 void 104 addfilter(unsigned int type, void *data) { 105 Filter *f = ecalloc(1, sizeof(Filter)); 106 107 switch(type) { 108 case F_TEXT: 109 case F_NOTEXT: 110 f->arg.v = data; 111 break; 112 case F_DATEFROM: 113 case F_DATETO: 114 f->arg.i = strtots(data); 115 break; 116 default: die("invalid filter type\n", type); 117 } 118 119 f->type = type; 120 attachfltr(f); 121 } 122 123 void 124 attach(Movement *m) { 125 m->next = movs; 126 movs = m; 127 } 128 129 void 130 attachfltr(Filter *f) { 131 f->next = filters; 132 filters = f; 133 } 134 135 void 136 attach_sorted_desc(Movement **head, Movement *m) { 137 Movement *t; 138 139 if(!*head || m->ts > (*head)->ts) { 140 m->next = *head; 141 *head = m; 142 return; 143 } 144 t = *head; 145 while(t->next && m->ts <= t->next->ts) 146 t = t->next; 147 m->next = t->next; 148 t->next = m; 149 } 150 151 void 152 deletemov(int id) { 153 Movement *m; 154 155 for(m = movs; m; m = m->next) 156 if(m->id == id) 157 break; 158 if(m) { 159 detach(m); 160 free(m); 161 } 162 } 163 164 void 165 detach(Movement *m) { 166 Movement **tm; 167 168 for (tm = &movs; *tm && *tm != m; tm = &(*tm)->next); 169 *tm = m->next; 170 } 171 172 void 173 detachfltr(Filter *f) { 174 Filter **tf; 175 176 for (tf = &filters; *tf && *tf != f; tf = &(*tf)->next); 177 *tf = f->next; 178 } 179 180 void 181 die(const char *errstr, ...) { 182 va_list ap; 183 184 va_start(ap, errstr); 185 vfprintf(stderr, errstr, ap); 186 va_end(ap); 187 exit(1); 188 } 189 190 void * 191 ecalloc(size_t nmemb, size_t size) { 192 void *p; 193 194 if(!(p = calloc(nmemb, size))) 195 die("Cannot allocate memory.\n"); 196 return p; 197 } 198 199 int 200 filtermov(Movement *m) { 201 Filter *f; 202 int ormatch = -1; 203 204 for(f = filters; f; f = f->next) { 205 switch(f->type) { 206 case F_TEXT: 207 if(ormatch > 0) 208 break; 209 ormatch = !!strcasestr(m->note, (char *)f->arg.v); 210 break; 211 case F_NOTEXT: 212 if(!!strcasestr(m->note, (char *)f->arg.v)) 213 return 1; 214 break; 215 case F_DATEFROM: 216 if(!(m->ts >= f->arg.i)) 217 return 1; 218 break; 219 case F_DATETO: 220 if(!(m->ts <= f->arg.i)) 221 return 1; 222 break; 223 } 224 } 225 226 return ormatch == -1 ? 0 : !ormatch; 227 } 228 229 int 230 filtermovs(int from, int to, char *txt) { 231 Movement *m; 232 int n = 0; 233 234 for(m = movs; m; m = m->next) { 235 m->filtered = filtermov(m); 236 n += m->filtered; 237 } 238 return n; 239 } 240 241 void 242 freemovs(void) { 243 Movement *m; 244 245 while(movs) { 246 m = movs; 247 detach(m); 248 free(m); 249 } 250 } 251 252 void 253 freefltrs(void) { 254 Filter *f; 255 256 while(filters) { 257 f = filters; 258 detachfltr(f); 259 free(f); 260 } 261 } 262 263 void 264 loadmovs(void) { 265 Movement *m; 266 int r; 267 268 rewind(movsfile); 269 while(1) { 270 m = ecalloc(1, sizeof(Movement)); 271 r = fscanf(movsfile, "%d %d %f %[^\n]", &m->id, &m->ts, &m->amount, &m->note[0]); 272 if(r <= 0) { 273 if(feof(movsfile)) 274 break; 275 die("%s: file corrupted\n", movsfilename); 276 } 277 attach(m); 278 } 279 } 280 281 void 282 refresh(void) { 283 Movement *m; 284 285 totals.amount = totals.income = totals.expense = totals.count = 0; 286 totals.pamount = totals.pincome = totals.pexpense = totals.pcount = 0; 287 for(m = movs; m; m = m->next) { 288 totals.amount += m->amount; 289 if(m->amount >= 0) 290 totals.income += m->amount; 291 else 292 totals.expense += m->amount; 293 ++totals.count; 294 295 if(m->filtered) 296 continue; 297 298 if(totals.pcount >= limit) 299 continue; 300 totals.pamount += m->amount; 301 if(m->amount >= 0) 302 totals.pincome += m->amount; 303 else 304 totals.pexpense += m->amount; 305 ++totals.pcount; 306 } 307 } 308 309 void 310 savemovs(void) { 311 Movement *m; 312 313 movsfile = fopen(movsfilename, "w"); 314 for(m = movs; m; m = m->next) 315 fprintf(movsfile, "%d %d %f %s\n", m->id, m->ts, m->amount, m->note); 316 fclose(movsfile); 317 } 318 319 void 320 showmovs(void) { 321 Movement *m; 322 time_t ts; 323 char time[32]; 324 int listcount = totals.count - filtered; 325 int count = 0; 326 327 if(limit < listcount) 328 listcount = limit; 329 330 if(listcount) 331 printf("%5s | %16s | %8s | %s\n", "id", "date time", "amount", "note"); 332 for(m = movs; m && count < limit; m = m->next) { 333 if(m->filtered) 334 continue; 335 ++count; 336 ts = m->ts; 337 strftime(time, sizeof time, "%d/%m/%Y %H:%M", localtime(&ts)); 338 printf("%5d | %16s | %8.2f | %s\n", m->id, time, m->amount, m->note); 339 } 340 if(listcount > 1 && listcount < totals.count) 341 printf("%5s | %17s: %8.2f | income=%.2f expense=%.2f movements=%d\n", "", 342 "Partial", totals.pamount, totals.pincome, totals.pexpense, totals.pcount); 343 printf("%5s | %17s: %8.2f | income=%.2f expense=%.2f movements=%d\n", 344 "", "Total", totals.amount, totals.income, totals.expense, totals.count); 345 } 346 347 void 348 sortmovs(void) { 349 Movement *sorted = NULL, *m, *t; 350 351 m = movs; 352 while(m) { 353 t = m; 354 m = m->next; 355 attach_sorted_desc(&sorted, t); 356 } 357 movs = sorted; 358 } 359 360 int 361 strtoint(char *s) { 362 long n; 363 char *ep; 364 365 n = strtol(s, &ep, 10); 366 if(s == ep || *ep != '\0' || n == LONG_MIN || n == LONG_MAX) 367 return -1; 368 return (int)n; 369 } 370 371 int 372 strtots(char *s) { 373 struct tm tm = {.tm_isdst = - 1}; 374 375 if(!strcmp(s, "now")) 376 return time(NULL); 377 strptime(s, "%d/%m/%Y %H:%M", &tm); 378 return mktime(&tm); 379 } 380 381 void 382 usage(void) { 383 die("Usage: %s [-v] [-defiltx <arg>] [<date [time]> <amount> <note>]\n", argv0); 384 } 385 386 int 387 main(int argc, char *argv[]) { 388 int delid = 0; 389 int from = 0, to = 0; 390 int lflag = 0; 391 char *txt = NULL; 392 393 ARGBEGIN { 394 case 'd': delid = atoi(EARGF(usage())); break; 395 case 'e': addfilter(F_TEXT, EARGF(usage())); break; 396 case 'f': 397 addfilter(F_DATEFROM, EARGF(usage())); 398 if(!lflag) 399 limit = 0; 400 break; 401 case 'i': snprintf(movsfilename, sizeof movsfilename, "%s", EARGF(usage())); break; 402 case 'l': 403 lflag = 1; 404 limit = strtoint(EARGF(usage())); 405 if(limit < 0) 406 die("%s: -l: invalid argument\n", argv0); 407 break; 408 case 't': 409 addfilter(F_DATETO, EARGF(usage())); 410 if(!lflag) 411 limit = 0; 412 break; 413 case 'v': die("sw-"VERSION"\n"); 414 case 'x': addfilter(F_NOTEXT, EARGF(usage())); break; 415 default: usage(); 416 } ARGEND; 417 418 if(!limit) 419 limit = INT_MAX; 420 421 if(!*movsfilename) 422 snprintf(movsfilename, sizeof movsfilename, "%s/%s", getenv("HOME"), ".sw"); 423 movsfile = fopen(movsfilename, "r"); 424 if(!movsfile) 425 die("%s: cannot open the file\n", movsfilename); 426 loadmovs(); 427 fclose(movsfile); 428 429 if(delid) { 430 deletemov(delid); 431 savemovs(); 432 freemovs(); 433 freefltrs(); /* Only for coherence. We should check incompatible flags anyway */ 434 return 0; 435 } 436 if(argc) { 437 if(argc != 3) 438 usage(); 439 addmov(argv[0], atof(argv[1]), argv[2]); 440 savemovs(); 441 return 0; 442 } 443 filtered = filtermovs(from, to, txt); 444 sortmovs(); 445 refresh(); 446 showmovs(); 447 freemovs(); 448 freefltrs(); 449 return 0; 450 }