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
22 #define ERRH1 stderr,"%s: "
24 #define ERRNO1 ": %s!\n"
25 #define ERRNO2 strerror(errno)
28 static const char version[]="This is TimePlan, version 1.0.1\n";
29 static const char *pname;
32 static int verbose=0,tree=0,doaverage=0;
33 static char *formtotal=DEF_FORMTOTAL;
34 static char buf[LINE_MAX];
36 static enum { SORT_NO,SORT_TIMETOT,SORT_STORES } sortby=SORT_TIMETOT;
37 static unsigned lifetime_days=0;
38 static unsigned long lifetime_seq=1;
40 static void usage(void)
42 fprintf(stderr,"%s%s%s",
44 "This command summarizes the timelog information:\n\
46 Usage: timeplan [-u|--unsort] [-s|--stores] [-T|--timetot] [-t|--tree]\n\
47 [-c|--condition <cond>] [-f|--formtotal <fmtstring>]\n\
48 [-r|--rule <ruleL>:<ruleR>]\n\
49 [-v|--verbose] [-h|--help] [-V|--version]\n\
51 -u, --unsort\t\tDon't sort the result in any way\n\
52 -s, --stores\t\tSort the result by stores count\n\
53 -T, --timetot\t\tSort the result by total time (default)\n\
55 -t, --tree\t\tOrganize data as hierarchy tree\n\
56 -a, --average\t\tDisplay all times as average-per-day value\n\
57 -c, --condition\tDefine condition variable\n\
58 -f, --formtotal\tFormat \"Total\" column (\"text *val/val:width text\")\n\
59 -r, --rule\t\tAdd to the end of .rc file this rule (':'->'\\t')\n\
60 -v, --verbose\t\tInform about phases of transfer\n\
61 -h, --help\t\tPrint a summary of the options\n\
62 -V, --version\t\tPrint the version number\n\
67 static const struct option longopts[]={
73 {"condition",1,0,'c'},
74 {"formtotal",1,0,'f'},
78 {"version" ,0,0,'V'}};
80 static int calctime(int timetot,int at,int of)
82 /* FIXME: better distribution */
86 static struct action {
89 unsigned long last_seq;
91 } *hashtable[HASH_SIZE];
92 static int hashtable_tot=0;
94 static int dumpaction(const struct action *action)
96 int tot,mins FAKEUSE,hours FAKEUSE,days FAKEUSE,origtot;
100 tot=(action->timetot+30)/60;
101 if (doaverage && lifetime_days)
103 mins=tot; hours=mins/60; days=hours/24;
108 for (s=formtotal;*s;s++)
110 case '*': case '/': {
114 if (s[1]==*s) { s++; goto dump; }
115 for (s2=s+1;isdigit(*s2);s2++);
116 l=strtol(s+1,&end,10);
118 fprintf(ERRH1"Number parse error at column %ld of formtotal string!\n",ERRH2,(long)(s-formtotal));
126 if (s[1]==*s) { s++; goto dump; }
127 if (!isdigit(s[1])) goto dump;
133 else origtot+=s[1]-'0';
138 if (action) putchar(*s);
142 printf("=%03d/%02d:%02d %4d\t%s\n",days,hours,mins,
143 action->stores,action->what);
149 #define FUNC(which) \
150 static int sort_##which(const struct action **Ap,const struct action **Bp) \
151 { return (B->which>A->which)-(A->which>B->which); }
158 static void dumphashtable(void)
161 struct action *action;
162 struct action **sorta FAKEUSE,**sorti FAKEUSE;
164 if (sortby!=SORT_NO) {
165 int totalwidth=dumpaction(NULL);
167 while (totalwidth-->5) putchar(' ');
168 puts("Total Day Hr Mi Stor\tDescription");
169 if (!(sorta=malloc(sizeof(*sorta)*hashtable_tot))) {
170 fprintf(ERRH1"malloc() of %d pointers"ERRNO1,ERRH2,hashtable_tot,ERRNO2);
175 for (item=0;item<LENGTH(hashtable);item++)
176 for (action=hashtable[item];action;action=action->next)
181 if (sortby==SORT_NO) return;
182 assert(sorti==sorta+hashtable_tot);
183 qsort(sorta,hashtable_tot,sizeof(*sorta),
184 (int (*)(const void *,const void *))(sortby==SORT_TIMETOT ? sort_timetot : sort_stores));
185 for (sorti=sorta;sorti<sorta+hashtable_tot;sorti++)
190 static unsigned calchash(const char *s)
194 while (*s) r=r*7+11*toupper(*s++);
199 static void storeone(char *what,int length)
201 struct action **actionp,*action;
205 if (verbose) printf("storeone: %d: %s\n",length,what);
206 for (actionp=hashtable+calchash(what);(action=*actionp);actionp=&action->next)
207 if (!strcasecmp(action->what,what)) break;
209 if (!(action=malloc(sizeof(*action)+strlen(what)))) {
210 fprintf(ERRH1"malloc() for \"%s\""ERRNO1,ERRH2,what,ERRNO2);
216 strcpy(action->what,what);
221 if (action->last_seq==lifetime_seq) {
222 if (verbose) puts("storeone: preventing duplicate");
223 return; /* prevent duplicates like: TV-Music --> TV-Fun-Music-Fun */
226 action->last_seq=lifetime_seq;
227 action->timetot+=length;
232 struct textlist *next;
236 static struct textlist *conditions,**conditionstail=&conditions;
238 static int iscondition(const char *text)
240 struct textlist *cond;
242 for (cond=conditions;cond;cond=cond->next)
243 if (!strcasecmp(text,cond->text)) return 1;
247 static void addlist(struct textlist ***tail,const char *text,int line)
249 struct textlist *item;
251 if (!(item=malloc(sizeof(*item)+strlen(text)))) {
252 fprintf(ERRH1"malloc() for \"%s\""ERRNO1,ERRH2,text,ERRNO2);
255 strcpy(item->text,text);
261 static struct textlist *modifies,**modifiestail=&modifies,*modifiescmdl,**modifiescmdltail=&modifiescmdl;
262 static int modifies_tot;
263 static struct modistruct {
268 static void modify_load(void)
274 struct textlist *item;
279 if (asprintf(&finame,"%s"CONFIGFILE,getenv("HOME"))==-1) {
280 fprintf(ERRH1"Config filename allocation",ERRH2);
284 if (!(fi=fopen(finame,"rt"))) {
286 fprintf(ERRH1"Config file \"%s\" read"ERRNO1,ERRH2,finame,ERRNO2);
290 while (clearerr(fi),fgets(buf,sizeof(buf),fi)==buf) {
292 if ((s=strchr(buf,'\n')) && !s[1]) *s='\0';
295 fprintf(ERRH1"fgets(3) results not newline-terminated"WHERE1,ERRH2,WHERE2);
298 if (!*buf || *buf=='#') {
303 fprintf(ERRH1"Unrecognized syntax"WHERE1,ERRH2,WHERE2);
310 while (isspace(*s)) s++;
313 fprintf(ERRH1"Invalid character at offset %ld"WHERE1,ERRH2,(long)(s-buf+1),WHERE2);
316 for (s2=s+1;isalpha(*s2);s2++);
317 if (*s2!='+' && *s2!='-') {
318 fprintf(ERRH1"Only plus ('+') or minus ('-'), not '%c' expected at offset %ld"WHERE1,ERRH2,*s2,(long)(s2-buf+1),WHERE2);
321 isplus=(*s2=='+'); *s2++='\0';
322 if (iscondition(s)!=isplus)
327 addlist(&modifiestail,s,line);
330 if (!feof(fi) || ferror(fi)) {
331 fprintf(ERRH1"fgets(3) \"%s\""ERRNO1,ERRH2,finame,ERRNO2);
335 fprintf(ERRH1"fclose(3) \"%s\""ERRNO1,ERRH2,finame,ERRNO2);
336 *modifiestail=modifiescmdl; /* add all command-line arguments to the end */
337 if (!(modistructs=malloc(sizeof(*modistructs)*modifies_tot))) {
338 fprintf(ERRH1"malloc() of %d modistructs's"ERRNO1,ERRH2,modifies_tot,ERRNO2);
341 for (m=0,item=modifies;m<modifies_tot;m++,item=item->next) {
342 #define line (item->line)
343 if (!(s=strchr(item->text,'\t'))) {
344 fprintf(ERRH1"No delimiting tab-character found"WHERE1,ERRH2,WHERE2);
347 *s++='\0'; modistructs[m].dst=s;
348 if (verbose) printf("regcomp: %s -> %s\n",item->text,s);
349 if (regcomp(&modistructs[m].regex,item->text,REG_EXTENDED|REG_ICASE)) {
350 fprintf(ERRH1"regcomp() failed for \"%s\" (%s)"WHERE1,ERRH2,item->text,ERRNO2,WHERE2);
359 static char *modify(char *what)
361 regmatch_t matches[10];
364 static char modbuf1[sizeof(buf)],modbuf2[sizeof(modbuf1)];
365 char *src=what,*dstbase=modbuf1,*dst=dstbase;
366 const char *start FAKEUSE,*end FAKEUSE,*patt;
367 const struct textlist *item;
369 if (verbose) printf("modify: %s\n",what);
371 for (m=0,item=modifies,willprep=1;;m++,item=item->next) {
372 enum { PATT_START,PATT_MID,PATT_END,PATT_TERM } pattpos;
374 for (doprep=willprep;doprep>=0;doprep--) {
377 for (patt=src;;patt++) {
378 if (*patt=='-' || !*patt) {
391 i=regexec(&modistructs[m].regex,src,LENGTH(matches),matches,0);
392 if (i==REG_NOMATCH) {
397 fprintf(ERRH1"regexec() failed for \"%s\""ERRNO1,ERRH2,item->text,ERRNO2);
401 if (verbose) printf("matched: %s -> %s\n",item->text,modistructs[m].dst);
402 pattpos=PATT_START; patt=NULL;
403 while (pattpos!=PATT_TERM) {
407 end=src+matches->rm_so;
408 pattpos=PATT_MID; patt=modistructs[m].dst;
412 pattpos=PATT_END; patt=NULL;
415 else if (*patt=='@') {
416 start=src; end=src+strlen(src);
418 else if (*patt>='0' && *patt<='9') {
419 regmatch_t *match=matches+(*patt-'0');
421 || match->rm_eo==-1) {
422 fprintf(ERRH1"Trying to substitute '%c' but no \"matches\" entry not set for \"%s\""WHERE1,
423 ERRH2,*patt,item->text,WHERE2);
426 if (match->rm_so>match->rm_eo) {
427 fprintf(ERRH1"Trying to substitute '%c' start>end (%d>%d) for \"%s\""WHERE1,
428 ERRH2,*patt,match->rm_so,match->rm_eo,item->text,WHERE2);
431 start=src+match->rm_so; end=src+match->rm_eo;
434 start=patt; end=patt+1;
439 start=src+matches->rm_eo;
441 pattpos=PATT_TERM; /* assumed: patt=NULL; */
446 if ((dst-dstbase+(end-start))>=sizeof(modbuf1)-1/* -1 for '-' during prepping */) {
447 fprintf(ERRH1"Maximum buffer size exceeded during substition for \"%s\""WHERE1,
448 ERRH2,item->text,WHERE2);
451 memcpy(dst,start,end-start);
456 if (dst==dstbase || (dst==dstbase+1 && *dstbase=='-'))
460 assert(dstbase==modbuf1);
466 assert((src==modbuf1 && dstbase==modbuf2)
467 ||(src==modbuf2 && dstbase==modbuf1));
468 swap=src; src=dstbase; dst=dstbase=swap;
475 static struct modifycache {
476 struct modifycache *next;
477 char src[1]; /* dst[1] follows after '\0' */
478 } *mcachetable[HASH_SIZE];
479 static unsigned long modifycache_hits;
481 static char *modify_cached(char *what)
483 struct modifycache **mcachep,*mcache;
490 if (verbose) printf("modify_cached: %s\n",what);
491 for (mcachep=mcachetable+calchash(what);(mcache=*mcachep);mcachep=&mcache->next)
492 if (!strcasecmp(mcache->src,what)) break;
495 if (!(mcache=malloc(sizeof(*mcache)+whatl+1+(dst?strlen(dst):0)))) {
496 fprintf(ERRH1"malloc() for \"%s\""ERRNO1,ERRH2,what,ERRNO2);
500 memcpy(mcache->src,what,whatl+1);
502 strcpy(mcache->src+whatl+1,dst);
504 mcache->src[whatl+1]='\0'; /* dst will be empty */
508 if (verbose) printf("cache hit.\n");
509 /* if (verbose) would be performance hit */
512 if (!mcache->src[whatl+1])
514 return(mcache->src+whatl+1);
517 static void modify_cached_stats(void)
519 struct modifycache **mcachep,*mcache;
520 unsigned long depth,maxdepth=0,entries_total=0;
521 #define MODIFYCACHE_CALLS (modifycache_hits+entries_total)
523 for (mcachep=mcachetable;mcachep<mcachetable+HASH_SIZE;mcachep++) {
524 for (depth=0,mcache=*mcachep;mcache;mcache=mcache->next)
528 entries_total+=depth;
530 printf("modify_cached cache stats: hits=%u.%02u%% (%lu/%lu), HASH_SIZE=%d, maxdepth=%lu\n",
531 (unsigned)( 100*modifycache_hits/MODIFYCACHE_CALLS ),
532 (unsigned)((10000*modifycache_hits/MODIFYCACHE_CALLS)%100),
533 modifycache_hits,MODIFYCACHE_CALLS,
535 #undef MODIFYCACHE_CALLS
538 static void store(char *what,int length)
540 char ce_trash,*ce,*ceo=&ce_trash;
542 if (!(what=modify_cached(what))) {
543 if (verbose) puts("discarded.");
546 if (verbose) printf("store: %d: %s\n",length,what);
547 while ((ce=(tree?strrchr(what,'-'):strchr(what,'-')))) {
549 storeone(what,length);
550 if (!tree) { *ce='-'; what=ce+1; }
551 else { *ceo='-'; *(ceo=ce)='\0'; }
553 storeone(what,length);
558 static void hit(time_t t,char *bufaction)
560 static time_t last=-1;
561 static char bufbackup[sizeof(buf)];
562 char *acts[ACTS_MAX],*s;
565 if (verbose) printf("hit: %ld: %s\n",t,bufaction);
568 fprintf(ERRH1"Time goes backward"WHERE1,ERRH2,WHERE2);
573 while ((s=strchr(acts[acti-1],'+'))) {
576 if (acti>=LENGTH(acts)) {
577 fprintf(ERRH1"Too many '+'-delimited actions (%d)"WHERE1,ERRH2,acti,WHERE2);
583 store(acts[i],calctime(timetot,i,acti));
585 strcpy(bufbackup,bufaction);
589 int main(int argc,char **argv)
592 time_t basetime=-1,currtime;
596 while ((optc=getopt_long(argc,argv,"b:pw:qusTtac:f:r:vhV",longopts,NULL))!=EOF) switch (optc) {
614 if (iscondition(optarg))
615 fprintf(ERRH1"Condition \"%s\" already set!\n",ERRH2,optarg);
616 else addlist(&conditionstail,optarg,-1);
624 if (!(s=strchr(optarg,':'))) {
625 fprintf(ERRH1"No delimiting ':'-character found in -r option \"%s\"!\n",ERRH2,optarg);
628 if (!(dup=strdup(optarg))) {
629 fprintf(ERRH1"malloc() of \"%s\" string"ERRNO1,ERRH2,optarg,ERRNO2);
633 addlist(&modifiescmdltail,dup,0);
640 fprintf(stderr,version);
642 default: /* also 'h' */
647 if (asprintf(&finame,"%s"DEF_TIMEPLAN,getenv("HOME"))==-1) {
648 fprintf(ERRH1"Default timeplan filename allocation",ERRH2);
652 else if (optind+1!=argc) usage();
653 else if (!strcmp(argv[optind],"-")) {
657 else finame=argv[optind];
658 if (!fi && !(fi=fopen(finame,"r"))) {
659 fprintf(ERRH1"open \"%s\" for reading"ERRNO1,ERRH2,finame,ERRNO2);
663 while (clearerr(fi),fgets(buf,sizeof(buf),fi)==buf) {
667 if ((s=strchr(buf,'\n')) && !s[1]) *s='\0';
669 fprintf(ERRH1"fgets(3) results not newline-terminated"WHERE1,ERRH2,WHERE2);
673 if (*buf==':') { /* ":12.7.2000 (St)" */
678 const char *days[]={"Ne","Po","Ut","St","Ct","Pa","So"};
680 assert(LENGTH(days)==7);
681 i=sscanf(buf,":%d.%d.%d (%2s)%n",&tm.tm_mday,&tm.tm_mon,&tm.tm_year,wday,&parsed);
682 if ((i!=4 && i!=5) || parsed!=strlen(buf)) { /* See note in sscanf(3) man page */
683 fprintf(ERRH1"Timestamp with incorrect format"WHERE1,ERRH2,WHERE2);
686 for (i=0;i<7;i++) if (!strcmp(days[i],wday)) break;
688 fprintf(ERRH1"Non-parsable week-day name \"%s\""WHERE1,ERRH2,wday,WHERE2);
689 tm.tm_sec=tm.tm_min=tm.tm_hour=0;
690 tm.tm_wday=tm.tm_yday=-1;
691 tm.tm_isdst=0; /* FIXME */
695 if (t==-1 || tm.tm_wday<0 || tm.tm_wday>6
696 || tm.tm_mday<1 || tm.tm_mday>31
697 || tm.tm_mon <0 || tm.tm_mon >11
698 || tm.tm_year<80 || tm.tm_year>150
700 fprintf(ERRH1"Incorrect timestamp \"%s\""WHERE1,ERRH2,buf,WHERE2);
703 if (i<7 && tm.tm_wday!=i)
704 fprintf(ERRH1"Non-matching week-day, given \"%s\", calculated \"%s\""WHERE1,ERRH2,days[i],days[tm.tm_wday],WHERE2);
705 #define WANTED (60*60*24)
706 if (basetime!=-1 && basetime+WANTED!=t)
707 fprintf(ERRH1"Non-continuous timestamp (%ld, wanted %d) \"%s\""WHERE1,ERRH2,t-basetime,WANTED,buf,WHERE2);
715 if (!(isdigit(buf[0]) && isdigit(buf[1]) && buf[2]==':' && isdigit(buf[3]) && isdigit(buf[4])
716 && (buf[5]=='-' || (buf[5]==':' && isdigit(buf[6]) && isdigit(buf[7]) && buf[8]=='-'))
718 || (sec=0,sscanf(buf,(buf[5]=='-'?"%d:%d-":"%d:%d:%d-"),&hour,&min,&sec)!=2+(buf[5]==':'))
723 fprintf(ERRH1"Incorrect day-time \"%s\""WHERE1,ERRH2,buf,WHERE2);
727 fprintf(ERRH1"Day-time found but no basetime timestamp set"WHERE1,ERRH2,WHERE2);
730 currtime=basetime+(hour*60+min)*60+sec;
731 hit(currtime,buf+(buf[5]=='-'?6:9));
734 if (!feof(fi) || ferror(fi)) {
735 fprintf(ERRH1"fgets(3) \"%s\""ERRNO1,ERRH2,finame,ERRNO2);
738 if (fi!=stdin && fclose(fi))
739 fprintf(ERRH1"fclose(3) \"%s\""ERRNO1,ERRH2,finame,ERRNO2);
742 modify_cached_stats();