The replacement string is now only for matched region, not the whole string.
[timeplan.git] / timeplan.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <getopt.h>
4 #include <errno.h>
5 #include <string.h>
6 #include <time.h>
7 #include <assert.h>
8 #include <limits.h>
9 #include <ctype.h>
10 #include <regex.h>
11
12 #define ACTS_MAX 20
13 #define HASH_SIZE 211
14 #define CONFIGFILE "/.timeplanrc" /* HOME prepended */
15 #define DEF_TIMEPLAN "/.timeplan" /* HOME prepended */
16 #define MARK_DAY "_days"
17 #define DEF_FORMTOTAL ":6"
18 #define LENGTH(x) (sizeof((x))/sizeof(*(x)))
19 #define WHERE1 " in \"%s\" on line %d!\n"
20 #define WHERE2 finame,line
21 #define NL "\r\n"
22 #define ERRH1 stderr,"%s: "
23 #define ERRH2 pname
24 #define ERRNO1 ": %s!\n"
25 #define ERRNO2 strerror(errno)
26 #define FAKEUSE =0
27
28 static const char version[]="This is TimePlan, version 1.0\n";
29 static const char *pname;
30 static char *finame;
31 static FILE *fi;
32 static int verbose=0,tree=0;
33 static char *formtotal=DEF_FORMTOTAL;
34 static char buf[LINE_MAX];
35 static int line=0;
36 static enum { SORT_NO,SORT_TIMETOT,SORT_STORES } sortby=SORT_TIMETOT;
37
38 static void usage(void)
39 {
40         fprintf(stderr,"\
41 %s\
42 This command summarizes the timelog information:\n\
43 \n\
44 Usage: timeplan [-u|--unsort] [-s|--stores] [-T|--timetot]  [-t|--tree]\n\
45                 [-c|--condition <cond>] [-f|--formtotal <fmtstring>]\n\
46                 [-v|--verbose] [-h|--help] [-V|--version]\n\
47 \n\
48   -u, --unsort\t\tDon't sort the result in any way\n\
49   -s, --stores\t\tSort the result by stores count\n\
50   -T, --timetot\t\tSort the result by total time (default)\n\
51   -t, --tree\t\tOrganize data as hierarchy tree\n\
52   -c, --condition\tDefine condition variable\n\
53   -f, --formtotal\tFormat \"Total\" column (\"text *val/val:width text\")\n\
54   -v, --verbose\t\tInform about phases of transfer\n\
55   -h, --help\t\tPrint a summary of the options\n\
56   -V, --version\t\tPrint the version number\n\
57 ",version);
58         exit(EXIT_FAILURE);
59 }
60
61 static const struct option longopts[]={
62 {"unsort"   ,0,0,'u'},
63 {"stores"   ,0,0,'s'},
64 {"timetot"  ,0,0,'T'},
65 {"tree"     ,0,0,'t'},
66 {"condition",1,0,'c'},
67 {"formtotal",1,0,'f'},
68 {"verbose"  ,0,0,'v'},
69 {"help"     ,0,0,'h'},
70 {"version"  ,0,0,'V'}};
71
72 static int calctime(int timetot,int at,int of)
73 {
74         /* FIXME: better distribution */
75         return timetot/of;
76 }
77
78 static struct action {
79         struct action *next;
80         int timetot,stores;
81         char what[1];
82         } *hashtable[HASH_SIZE];
83 static int hashtable_tot=0;
84
85 static int dumpaction(const struct action *action)
86 {
87 int tot FAKEUSE,mins FAKEUSE,hours FAKEUSE,days FAKEUSE,origtot;
88 char *s,fmt[]="%?d";
89
90         if (action) {
91                 tot=(action->timetot+30)/60;
92                 mins=tot; hours=mins/60; days=hours/24;
93                 mins%=60; hours%=24;
94                 origtot=tot;
95                 }
96         else origtot=0;
97         for (s=formtotal;*s;s++)
98                 switch (*s) {
99                         case '*': case '/': {
100 long l;
101 char *end,*s2;
102
103                                 if (s[1]==*s) { s++; goto dump; }
104                                 for (s2=s+1;isdigit(*s2);s2++);
105                                 l=strtol(s+1,&end,10);
106                                 if (end!=s2) {
107                                         fprintf(ERRH1"Number parse error at column %d of formtotal string!\n",ERRH2,s-formtotal);
108                                         exit(EXIT_FAILURE);
109                                         }
110                                 if (*s=='*') tot*=l;
111                                 else         tot/=l;
112                                 s=s2-1;
113                                 break; }
114                         case ':':
115                                 if (s[1]==*s) { s++; goto dump; }
116                                 if (!isdigit(s[1])) goto dump;
117                                 if (action) {
118                                         fmt[1]=s[1];
119                                         printf(fmt,tot);
120                                         tot=origtot;
121                                         }
122                                 else origtot+=s[1]-'0';
123                                 s+=1;
124                                 break;
125                         default:
126 dump:
127                                 if (action) putchar(*s);
128                                 else origtot++;
129                         }
130         if (action)
131                 printf("=%02d/%02d:%02d\t%4d\t%s\n",days,hours,mins,
132                         action->stores,action->what);
133         return(origtot);
134 }
135
136 #define A (*Ap)
137 #define B (*Bp)
138 #define FUNC(which) \
139         static int sort_##which(const struct action **Ap,const struct action **Bp) \
140         { return (B->which>A->which)-(A->which>B->which); }
141 FUNC(timetot)
142 FUNC(stores)
143 #undef FUNC
144 #undef B
145 #undef A
146
147 static void dumphashtable(void)
148 {
149 int item;
150 struct action *action;
151 struct action **sorta FAKEUSE,**sorti FAKEUSE;
152
153         if (sortby!=SORT_NO) {
154 int totalwidth=dumpaction(NULL);
155
156                 while (totalwidth-->5) putchar(' ');
157                 puts("Total Da Hr Mi\tStor\tDescription");
158                 if (!(sorta=malloc(sizeof(*sorta)*hashtable_tot))) {
159                         fprintf(ERRH1"malloc() of %d pointers"ERRNO1,ERRH2,hashtable_tot,ERRNO2);
160                         exit(EXIT_FAILURE);
161                         }
162                 sorti=sorta;
163                 }
164         for (item=0;item<LENGTH(hashtable);item++)
165                 for (action=hashtable[item];action;action=action->next)
166                         if (sortby==SORT_NO)
167                                 dumpaction(action);
168                         else
169                                 *sorti++=action;
170         if (sortby==SORT_NO) return;
171         assert(sorti==sorta+hashtable_tot);
172         qsort(sorta,hashtable_tot,sizeof(*sorta),
173                 (int (*)(const void *,const void *))(sortby==SORT_TIMETOT ? sort_timetot : sort_stores));
174         for (sorti=sorta;sorti<sorta+hashtable_tot;sorti++)
175                 dumpaction(*sorti);
176         free(sorta);
177 }
178
179 static unsigned calchash(const char *s)
180 {
181 unsigned r=57;
182
183         while (*s) r=r*7+11*(*s++);
184         return r;
185 }
186
187 static void storeone(char *what,int length)
188 {
189 struct action **actionp,*action;
190
191         if (verbose) printf("storeone: %d: %s\n",length,what);
192         for (actionp=hashtable+(calchash(what)%HASH_SIZE);(action=*actionp);actionp=&action->next)
193                 if (!strcasecmp(action->what,what)) break;
194         if (!action) {
195                 if (!(action=malloc(sizeof(*action)+strlen(what)))) {
196                         fprintf(ERRH1"malloc() for \"%s\""ERRNO1,ERRH2,what,ERRNO2);
197                         exit(EXIT_FAILURE);
198                         }
199                 action->next=NULL;
200                 action->timetot=0;
201                 action->stores=0;
202                 strcpy(action->what,what);
203                 *actionp=action;
204                 hashtable_tot++;
205                 }
206         action->timetot+=length;
207         action->stores++;
208 }
209
210 struct textlist {
211         struct textlist *next;
212         int line;
213         char text[1];
214         };
215 struct textlist *conditions,**conditionstail=&conditions;
216
217 static int iscondition(const char *text)
218 {
219 struct textlist *cond;
220
221         for (cond=conditions;cond;cond=cond->next)
222                 if (!strcasecmp(text,cond->text)) return(1);
223         return(0);
224 }
225
226 static void addlist(struct textlist ***tail,const char *text,int line)
227 {
228 struct textlist *item;
229
230         if (!(item=malloc(sizeof(*item)+strlen(text)))) {
231                 fprintf(ERRH1"malloc() for \"%s\""ERRNO1,ERRH2,text,ERRNO2);
232                 exit(EXIT_FAILURE);
233                 }
234         strcpy(item->text,text);
235         item->next=NULL;
236         **tail=item;
237         *tail=&item->next;
238 }
239
240 static struct textlist *modifies,**modifiestail=&modifies;
241 static int modifies_tot;
242 static struct modistruct {
243         regex_t regex;
244         char *dst;
245         } *modistructs;
246
247 static void modify_load(void)
248 {
249 static int inited=0;
250 char *finame,*s,*s2;
251 FILE *fi;
252 int line=0,m;
253 struct textlist *item;
254 char buf[LINE_MAX];
255
256         if (inited) return;
257         inited=1;
258         if (asprintf(&finame,"%s"CONFIGFILE,getenv("HOME"))==-1) {
259                 fprintf(ERRH1"Config filename allocation",ERRH2);
260                 exit(EXIT_FAILURE);
261                 }
262         errno=0;
263         if (!(fi=fopen(finame,"rt"))) {
264                 if (errno!=ENOENT)
265                         fprintf(ERRH1"Config file \"%s\" read"ERRNO1,ERRH2,finame,ERRNO2);
266                 return;
267                 }
268
269         while (clearerr(fi),fgets(buf,sizeof(buf),fi)==buf) {
270                 line++;
271                 if ((s=strchr(buf,'\n')) && !s[1]) *s='\0';
272
273                 else {
274                         fprintf(ERRH1"fgets(3) results not newline-terminated"WHERE1,ERRH2,WHERE2);
275                         exit(EXIT_FAILURE);
276                         }
277                 if (!*buf || *buf=='#') {
278 nextline:
279                         continue;
280                         }
281                 if (*buf!='R') {
282                         fprintf(ERRH1"Unrecognized syntax"WHERE1,ERRH2,WHERE2);
283                         exit(EXIT_FAILURE);
284                         }
285                 s=buf+1;
286                 for (;;) {
287 int isplus;
288
289                         while (isspace(*s)) s++;
290                         if (*s==':') break;
291                         if (!isalpha(*s)) {
292                                 fprintf(ERRH1"Invalid character at offset %d"WHERE1,ERRH2,s-buf+1,WHERE2);
293                                 exit(EXIT_FAILURE);
294                                 }
295                         for (s2=s+1;isalpha(*s2);s2++);
296                         if (*s2!='+' && *s2!='-') {
297                                 fprintf(ERRH1"Only plus ('+') or minus ('-'), not '%c' expected at offset %d"WHERE1,ERRH2,*s2,s2-buf+1,WHERE2);
298                                 exit(EXIT_FAILURE);
299                                 }
300                         isplus=(*s2=='+'); *s2++='\0';
301                         if (iscondition(s)!=isplus)
302                                 goto nextline;
303                         s=s2;
304                         }
305                 s++; /* Skip ':' */
306                 addlist(&modifiestail,s,line);
307                 modifies_tot++;
308                 }
309         if (!feof(fi) || ferror(fi))  {
310                 fprintf(ERRH1"fgets(3) \"%s\""ERRNO1,ERRH2,finame,ERRNO2);
311                 exit(EXIT_FAILURE);
312                 }
313         if (fclose(fi))
314                 fprintf(ERRH1"fclose(3) \"%s\""ERRNO1,ERRH2,finame,ERRNO2);
315         if (!(modistructs=malloc(sizeof(*modistructs)*modifies_tot))) {
316                 fprintf(ERRH1"malloc() of %d modistructs's"ERRNO1,ERRH2,modifies_tot,ERRNO2);
317                 exit(EXIT_FAILURE);
318                 }
319         for (m=0,item=modifies;m<modifies_tot;m++,item=item->next) {
320 #define line (item->line)
321                 if (!(s=strchr(item->text,'\t'))) {
322                         fprintf(ERRH1"No delimiting tab-character found"WHERE1,ERRH2,WHERE2);
323                         exit(EXIT_FAILURE);
324                         }
325                 *s++='\0'; modistructs[m].dst=s;
326                 if (verbose) fprintf(stderr,"regcomp: %s -> %s\n",item->text,s);
327                 if (regcomp(&modistructs[m].regex,item->text,REG_EXTENDED|REG_ICASE)) {
328                         fprintf(ERRH1"regcomp() failed for \"%s\" (%s)"WHERE1,ERRH2,item->text,ERRNO2,WHERE2);
329                         exit(EXIT_FAILURE);
330                         }
331 #undef line
332                 }
333         assert(!item);
334         free(finame);
335 }
336
337 static char *modify(char *what)
338 {
339 regmatch_t matches[10];
340 int i,m;
341 static char modbuf1[sizeof(buf)],modbuf2[sizeof(modbuf1)];
342 char *src=what,*dstbase=modbuf1,*dst=dstbase;
343 const char *start,*end,*patt;
344 const struct textlist *item;
345
346         if (verbose) printf("modify: %s\n",what);
347         modify_load();
348         for (m=0,item=modifies;item;m++,item=item->next) {
349                 i=regexec(&modistructs[m].regex,src,LENGTH(matches),matches,0);
350                 if (i==REG_NOMATCH) continue;
351                 if (i) {
352                         fprintf(ERRH1"regexec() failed for \"%s\""ERRNO1,ERRH2,item->text,ERRNO2);
353                         exit(EXIT_FAILURE);
354                         }
355                 if (verbose) fprintf(stderr,"matched: %s -> %s\n",item->text,modistructs[m].dst);
356                 for (patt=modistructs[m].dst;*patt;patt++) {
357                         if (*patt=='@') {
358                                 start=src; end=src+strlen(src);
359                                 }
360                         else if (*patt>='0' && *patt<='9') {
361 regmatch_t *match=matches+(*patt-'0');
362                                 if (match->rm_so==-1
363                                  || match->rm_eo==-1) {
364                                         fprintf(ERRH1"Trying to substitute '%c' but no \"matches\" entry not set for \"%s\""WHERE1,
365                                                         ERRH2,*patt,item->text,WHERE2);
366                                         exit(EXIT_FAILURE);
367                                         }
368                                 if (match->rm_so>match->rm_eo) {
369                                         fprintf(ERRH1"Trying to substitute '%c' start>end (%d>%d) for \"%s\""WHERE1,
370                                                         ERRH2,*patt,match->rm_so,match->rm_eo,item->text,WHERE2);
371                                         exit(EXIT_FAILURE);
372                                         }
373                                 start=src+match->rm_so; end=src+match->rm_eo;
374                                 }
375                         else {
376                                 start=patt; end=patt+1;
377                                 }
378                         if ((dst-dstbase+(end-start))>=sizeof(modbuf1)) {
379                                 fprintf(ERRH1"Maximum buffer size exceeded during substition for \"%s\""WHERE1,
380                                                 ERRH2,item->text,WHERE2);
381                                 exit(EXIT_FAILURE);
382                                 }
383                         memcpy(dst,start,end-start);
384                         dst+=end-start;
385                         }
386                 if (dst==dstbase) return(NULL);
387                 *dst='\0';
388                 if (src==what) {
389                         assert(dstbase==modbuf1);
390                         src=dstbase;
391                         dst=dstbase=modbuf2;
392                         }
393                 else {
394 char *swap;
395                         assert((src==modbuf1 && dstbase==modbuf2)
396                                          ||(src==modbuf2 && dstbase==modbuf1));
397                         swap=src; src=dstbase; dst=dstbase=swap;
398                         }
399                 }
400         return src;
401 }
402
403 static void store(char *what,int length)
404 {
405 char *ce;
406
407         if (!(what=modify(what))) {
408                 if (verbose) puts("discarded.");
409                 return;
410                 }
411         if (verbose) printf("store: %d: %s\n",length,what);
412         while ((ce=(tree?strrchr:strchr)(what,'-'))) {
413                 if (!tree) *ce='\0';
414                 storeone(what,length);
415                 if (!tree) what=ce+1;
416                 else *ce='\0';
417                 }
418         storeone(what,length);
419 }
420
421 static void hit(time_t t)
422 {
423 static time_t last=-1;
424 static char bufbackup[sizeof(buf)];
425 char *acts[ACTS_MAX],*s;
426 int timetot,acti,i;
427
428         if (verbose) printf("hit: %ld: %s\n",t,buf+6);
429         if (last!=-1) {
430                 if (t<last) {
431                         fprintf(ERRH1"Time goes backward"WHERE1,ERRH2,WHERE2);
432                         t=last;
433                         }
434                 acts[0]=bufbackup+6;
435                 acti=1;
436                 while ((s=strchr(acts[acti-1],'+'))) {
437                         *s='\0';
438                         acts[acti++]=s+1;
439                         if (acti>=LENGTH(acts)) {
440                                 fprintf(ERRH1"Too many '+'-delimited actions (%d)"WHERE1,ERRH2,acti,WHERE2);
441                                 exit(EXIT_FAILURE);
442                                 }
443                         }
444                 timetot=t-last;
445                 for (i=0;i<acti;i++)
446                         store(acts[i],calctime(timetot,i,acti));
447                 }
448         strcpy(bufbackup,buf);
449         last=t;
450 }
451
452 int main(int argc,char **argv)
453 {
454 int optc;
455 time_t basetime=-1,currtime;
456 int hour,min;
457
458         pname=argv[0];
459         while ((optc=getopt_long(argc,argv,"b:pw:qusTtc:f:vhV",longopts,NULL))!=EOF) switch (optc) {
460                 
461                 case 'u':
462                         sortby=SORT_NO;
463                         break;
464                 case 's':
465                         sortby=SORT_STORES;
466                         break;
467                 case 'T':
468                         sortby=SORT_TIMETOT;
469                         break;
470                 case 't':
471                         tree=1;
472                         break;
473                 case 'c':
474                         if (iscondition(optarg))
475                                 fprintf(ERRH1"Condition \"%s\" already set!\n",ERRH2,optarg);
476                         else addlist(&conditionstail,optarg,-1);
477                         break;
478                 case 'f':
479                         formtotal=optarg;
480                         break;
481                 case 'v':
482                         verbose=1;
483                         break;
484                 case 'V':
485                         fprintf(stderr,version);
486                         exit(EXIT_FAILURE);
487                 default: /* also 'h' */
488                         usage();
489                         break;
490                 }
491         if (optind==argc) {
492                 if (asprintf(&finame,"%s"DEF_TIMEPLAN,getenv("HOME"))==-1) {
493                         fprintf(ERRH1"Default timeplan filename allocation",ERRH2);
494                         exit(EXIT_FAILURE);
495                         }
496                 }
497         else if (optind+1!=argc) usage();
498         else if (!strcmp(argv[optind],"-")) {
499                 finame="<stdin>";
500                 fi=stdin;
501                 }
502         else finame=argv[optind];
503         if (!fi && !(fi=fopen(finame,"r"))) {
504                 fprintf(ERRH1"open \"%s\" for reading"ERRNO1,ERRH2,finame,ERRNO2);
505                 exit(EXIT_FAILURE);
506                 }
507
508         while (clearerr(fi),fgets(buf,sizeof(buf),fi)==buf) {
509 char *s;
510
511                 line++;
512                 if ((s=strchr(buf,'\n')) && !s[1]) *s='\0';
513                 else {
514                         fprintf(ERRH1"fgets(3) results not newline-terminated"WHERE1,ERRH2,WHERE2);
515                         exit(EXIT_FAILURE);
516                         }
517                 if (!*buf) continue;
518                 if (*buf==':') { /* ":12.7.2000 (St)" */
519 struct tm tm;
520 char wday[3];
521 int i,parsed;
522 time_t t;
523 const char *days[]={"Ne","Po","Ut","St","Ct","Pa","So"};
524
525                         assert(LENGTH(days)==7);
526                         i=sscanf(buf,":%d.%d.%d (%2s)%n",&tm.tm_mday,&tm.tm_mon,&tm.tm_year,wday,&parsed);
527                         if ((i!=4 && i!=5) || parsed!=strlen(buf)) { /* See note in sscanf(3) man page */
528                                 fprintf(ERRH1"Timestamp with incorrect format"WHERE1,ERRH2,WHERE2);
529                                 exit(EXIT_FAILURE);
530                                 }
531                         for (i=0;i<7;i++) if (!strcmp(days[i],wday)) break;
532                         if (i>=7)
533                                 fprintf(ERRH1"Non-parsable week-day name \"%s\""WHERE1,ERRH2,wday,WHERE2);
534                         tm.tm_sec=tm.tm_min=tm.tm_hour=0;
535                         tm.tm_wday=tm.tm_yday=-1;
536                         tm.tm_isdst=0; /* FIXME */
537                         tm.tm_mon--;
538                         tm.tm_year-=1900;
539                         t=mktime(&tm);
540                         if (t==-1 || tm.tm_wday<0 || tm.tm_wday>6
541                             || tm.tm_mday<1 || tm.tm_mday>31
542                                         || tm.tm_mon <1 || tm.tm_mon >12
543                                         || tm.tm_year<80 || tm.tm_year>150
544                                   ) {
545                                 fprintf(ERRH1"Incorrect timestamp \"%s\""WHERE1,ERRH2,buf,WHERE2);
546                                 exit(EXIT_FAILURE);
547                                 }
548                         if (i<7 && tm.tm_wday!=i)
549                                 fprintf(ERRH1"Non-matching week-day, given \"%s\", calculated \"%s\""WHERE1,ERRH2,days[i],days[tm.tm_wday],WHERE2);
550 #define WANTED (60*60*24)
551                         if (basetime!=-1 && basetime+WANTED!=t)
552                                 fprintf(ERRH1"Non-continuous timestamp (%ld, wanted %d) \"%s\""WHERE1,ERRH2,t-basetime,WANTED,buf,WHERE2);
553 #undef WANTED
554                         basetime=t;
555                         store(MARK_DAY,0);
556                         continue;
557                         }
558
559                 if (!isdigit(buf[0]) || !isdigit(buf[1]) || buf[2]!=':' || !isdigit(buf[3]) || !isdigit(buf[4]) || buf[5]!='-'
560                  || sscanf(buf,"%d:%d-",&hour,&min)!=2
561                  || hour<0 || hour>23
562                  || min <0 || min >59
563                     ) {
564                         fprintf(ERRH1"Incorrect day-time \"%s\""WHERE1,ERRH2,buf,WHERE2);
565                         exit(EXIT_FAILURE);
566                         }
567                 if (basetime==-1) {
568                         fprintf(ERRH1"Day-time found but no basetime timestamp set"WHERE1,ERRH2,WHERE2);
569                         exit(EXIT_FAILURE);
570                         }
571                 currtime=basetime+(hour*60+min)*60;
572                 hit(currtime);
573                 }
574
575         if (!feof(fi) || ferror(fi))  {
576                 fprintf(ERRH1"fgets(3) \"%s\""ERRNO1,ERRH2,finame,ERRNO2);
577                 exit(EXIT_FAILURE);
578                 }
579         if (fi!=stdin && fclose(fi))
580                 fprintf(ERRH1"fclose(3) \"%s\""ERRNO1,ERRH2,finame,ERRNO2);
581
582         dumphashtable();
583         return(EXIT_SUCCESS);
584 }