/* Copyright (c) 2017, Computer History Museum All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted (subject to the limitations in the disclaimer below) provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Computer History Museum nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "statmng.h" #define FILE_NUM 134 /* Copyright (c) 2000 by Qualcomm, Inc */ enum { kStatCount = kBeginShortStats, kShortStatCount = kEndStats-kBeginShortStats, kSaveStateSecs = 15*60, // Save playlist state every 15 minutes kStatFileVersion = 2, kDayStatCount = 24, kWeekStatCount = 7, kMonthStatCount = 31, kYearStatCount = 12, kAutoUpdateTicks = 120 // Auto udpate display 2 seconds after last change in stats }; enum { kFacetimeOther,kFacetimeRead,kFacetimeWrite }; enum { kThisStat,kLastStat,kAveStat,kTotalStat }; /************************************************************************ * data ************************************************************************/ typedef struct { long day[kDayStatCount]; long week[kWeekStatCount]; long month[kMonthStatCount]; long year[kYearStatCount]; } PeriodicStats; typedef struct { PeriodicStats current; PeriodicStats last; PeriodicStats average; long total; } NumericStats; typedef struct { long day; long week; long month; long year; } ShortPeriodicStats; typedef struct { ShortPeriodicStats current; ShortPeriodicStats last; ShortPeriodicStats average; long total; } ShortNumericStats; typedef struct { short version; long startTime; // When did we start collecting stats? long junkStartTime; // When did we start collecting junk stats? long currentTime; long unused1 ; // totalDays; long unused2 ; // totalWeeks; long unused3 ; // totalMonths; long unused4 ; // totalYears; NumericStats numStats[kStatCount]; ShortNumericStats shortStats[kShortStatCount]; } StatData, *StatDataPtr, **StatDataHandle; typedef struct { short rThis,rLast; Str32 sProjected; StatTimePeriod period; short rPeriod; Boolean showAverage; } ComposeStatsData; /************************************************************************ * globals ************************************************************************/ static StatDataHandle gStatData; static LongDateRec gCurrentTime; static Boolean gStatsDirty; static FMBHandle gFaceMeasure; static short gFacetimeMode; static uLong gAutoUpdateTicks; static short dataCountTab[] = { kDayStatCount,kWeekStatCount,kMonthStatCount,kYearStatCount }; static short kwdStrTab[] = { ksStatReceivedMail,ksStatSentMail,ksStatFaceTime, ksStatScoredJunk, ksStatScoredNotJunk, ksStatWhiteList, ksStatFalsePositives, ksStatFalseNegatives, ksStatFalseWhiteList,0,0,0, 0, ksStatReceivedAttach,ksStatSentAttach,ksStatReadMsg,0,ksStatForwardMsg, ksStatReplyMsg,ksStatRedirectMsg,ksStatFaceTimeRead,ksStatFaceTimeCompose,ksStatFaceTimeOther }; #define CalcedStat(i) (i>=kStatJunkTotal && i<=kStatJunkPercent) /************************************************************************ * prototypes ************************************************************************/ static OSErr ReadStatData(void); static OSErr WriteStatData(void); static void CheckTimeChange(void); static OSErr LoadStats(void); static OSErr SaveStats(Boolean force); static void UpdateFacetime(void); static void UpdateFacetimeLo(void); static void GetStatTotals(NumericStats *stats,StatTimePeriod period,uLong values[],uLong startTime); void GetStatTotalPercents(NumericStats *statNumerator,NumericStats *statDenominator,StatTimePeriod period,uLong values[]); static long SumArray(long *values,short len); #define SumArrayHi(a) SumArray(a,sizeof(a)/sizeof(a[0])) static PicHandle GetGraphPict(NumericStats *stats,StatTimePeriod period,Boolean seconds,Boolean showAverage,StringPtr sThis,StringPtr sLast,StringPtr sAverage); static Handle GetXLabels(StatTimePeriod period); static UPtr RightAlign7(uLong value,UPtr s); static UPtr RightAlign7Dec(uLong value,UPtr s); static UPtr RightAlign(uLong value,UPtr s,short minLen,short decPlace); static void AddToAve(uLong *to,uLong *from,short length); static void UpdateNumStatLo(StatType which, long value); static OSErr GetAbbrevNames(Handle *monthNames, Handle *dayNames); static void MoveNames(Str15 *names, long count, long abbrLen, StringPtr dest); static void ComposeNumStats(AccuPtr a,StatType type,short strType,ComposeStatsData *data); static void ComposeShortStats(AccuPtr a,StatType type,short strType,ComposeStatsData *data,short rObject,Boolean percent); static void ComposeUsageActivities(AccuPtr a, ComposeStatsData *data); static PicHandle InitGraphPict(GraphData *data,SeriesInfo *theSeries,StatTimePeriod period,StringPtr *labels); static void FinishGraphPict(GraphData *data); static void ComposeFRR(AccuPtr a, ComposeStatsData *data); void ComposeJunkJunk(AccuPtr a, ComposeStatsData *data); static uLong CalcElapsedUnits(StatTimePeriod period); static void AccuAddNumStats(AccuPtr a,short keyword,PeriodicStats *stats); static void AccuAddArray(AccuPtr a,short keyword,uLong *stats,short count); static void AccuAddShortStats(AccuPtr a,short keyword,ShortPeriodicStats *stats); static void ParseStatFile(Handle hStatXML); static short WordStrn(short rStrn,long value); void UpdateCalcStats(void); typedef enum { opPlus, opMinus, opPercent } OpType; void NumericStatOperation(StatType destination,StatType increment,OpType op); void PeriodicStatOperation(PeriodicStats *destP, PeriodicStats *incP, OpType op); void LongArrayOperation(long *array,long *sub,short n,OpType op); /************************************************************************ * InitStats - initialize statistics ************************************************************************/ void InitStats(void) { gFaceMeasure = NewFaceMeasure(); FaceMeasureBegin(gFaceMeasure); gFacetimeMode = kFacetimeOther; LoadStats(); } /************************************************************************ * ShutdownStats - shutdown statistics ************************************************************************/ void ShutdownStats(void) { if (!gStatData) return; UpdateFacetime(); SaveStats(true); ZapHandle(gStatData); DisposeFaceMeasure(gFaceMeasure); gFaceMeasure = nil; } /************************************************************************ * StatsIdle - idle time processing for statistics ************************************************************************/ void StatsIdle(void) { static uLong nextIdle,nextSaveStatus; short facetimeMode; WindowPtr winWP; uLong seconds; if (!gStatData || InBG) return; if (nextIdle && TickCount()previewPTE) facetimeMode = kFacetimeRead; break; } } if (facetimeMode != gFacetimeMode) { UpdateFacetime(); gFacetimeMode = facetimeMode; } // Save stats on disk every 15 minutes in case of system failure seconds = LocalDateTime(); if (nextSaveStatus) { if (seconds >= nextSaveStatus) { SaveStats(false); nextSaveStatus = seconds + kSaveStateSecs; } } else nextSaveStatus = seconds + kSaveStateSecs; if (gAutoUpdateTicks && (TickCount() >= gAutoUpdateTicks)) { RedisplayStats(); gAutoUpdateTicks = 0; } } /************************************************************************ * UpdateNumStat - update a statistic ************************************************************************/ void UpdateNumStat(StatType which, long value) { CheckTimeChange(); UpdateNumStatLo(which,value); } /************************************************************************ * UpdateNumStatWithTime - update a statistic specifying the time ************************************************************************/ void UpdateNumStatWithTime(StatType which, long value, uLong seconds) { uLong curSeconds; NumericStats *pNum = nil; ShortNumericStats *pShort = nil; if (!gStatData) return; CheckTimeChange(); curSeconds = LocalDateTime(); if (seconds > curSeconds) // If time is in future, just use current time UpdateNumStatLo(which,value); else if (value) { StatDataPtr pData; LongDateCvt theTime; LongDateRec dt; short dYear,dMonth,dWeek,dDay; theTime.hl.lHigh = 0; theTime.hl.lLow = seconds; LongSecondsToDate(&theTime.c,&dt); dDay = gCurrentTime.ld.dayOfYear - dt.ld.dayOfYear; dWeek = gCurrentTime.ld.weekOfYear - dt.ld.weekOfYear; dMonth = gCurrentTime.ld.month - dt.ld.month; dYear = gCurrentTime.ld.year - dt.ld.year; pData = *gStatData; if (which < kStatCount) { pNum = &pData->numStats[which]; if (dYear==0) { // current year pNum->current.year[dt.ld.month-1] += value; if (dMonth==0) pNum->current.month[dt.ld.day-1] += value; // current month else { if (dMonth==1) pNum->last.month[dt.ld.day-1] += value; // last month pNum->average.month[dt.ld.day-1] += value; // add to average } if (dWeek==0) pNum->current.week[dt.ld.dayOfWeek-1] += value; // current week else { if (dWeek==1) pNum->last.week[dt.ld.dayOfWeek-1] += value; // last week pNum->average.week[dt.ld.dayOfWeek-1] += value; // add to average } if (dDay==0) pNum->current.day[dt.ld.hour] += value; // today else { if (dDay==1) pNum->last.day[dt.ld.hour] += value; // yesterday pNum->average.day[dt.ld.hour] += value; // add to average } } else { if (dYear==1) { // last year pNum->last.year[dt.ld.month-1] += value; if (dt.ld.month==12 && gCurrentTime.ld.month==1) pNum->last.month[dt.ld.day-1] += value; // last month if (dt.ld.weekOfYear==52 && gCurrentTime.ld.weekOfYear==1) pNum->last.week[dt.ld.dayOfWeek-1] += value; // last week } pNum->average.year[dt.ld.month-1] += value; // add to average } pNum->total += value; } else if (which < kEndStats) { pShort = &pData->shortStats[which-kBeginShortStats-1]; if (dYear==0) { // current year pShort->current.year += value; if (dMonth==0) pShort->current.month += value; // current month else { if (dMonth==1) pShort->last.month += value; // last month pShort->average.month += value; } if (dWeek==0) pShort->current.week += value; // current week else { if (dWeek==1) pShort->last.week += value; // last week pShort->average.week += value; } if (dDay==0) pShort->current.day += value; // today else { if (dDay==1) pShort->last.day += value; // yesterday pShort->average.day += value; } } else { if (dYear==1) { // last year pShort->last.year += value; if (dt.ld.month==12 && gCurrentTime.ld.month==1) pShort->last.month += value; // last month if (dt.ld.weekOfYear==52 && gCurrentTime.ld.weekOfYear==1) pShort->last.week += value; // last week } pShort->average.year += value; } pShort->total += value; } gStatsDirty = true; gAutoUpdateTicks = TickCount() + kAutoUpdateTicks; } } /************************************************************************ * UpdateNumStatLo - update a statistic ************************************************************************/ static void UpdateNumStatLo(StatType which, long value) { StatDataPtr pData; NumericStats *pNum = nil; ShortNumericStats *pShort = nil; if (LoadStats()) return; if (value) { pData = *gStatData; if (which < kStatCount) { pNum = &pData->numStats[which]; pNum->current.day[gCurrentTime.ld.hour] += value; pNum->current.week[gCurrentTime.ld.dayOfWeek-1] += value; pNum->current.month[gCurrentTime.ld.day-1] += value; pNum->current.year[gCurrentTime.ld.month-1] += value; pNum->total += value; } else if (which < kEndStats) { pShort = &pData->shortStats[which-kBeginShortStats-1]; pShort->current.day += value; pShort->current.week += value; pShort->current.month += value; pShort->current.year += value; pShort->total += value; } gStatsDirty = true; gAutoUpdateTicks = TickCount() + kAutoUpdateTicks; } } /************************************************************************ * CheckTimeChange - see if we have gone to a new time period ************************************************************************/ void CheckTimeChange(void) { LongDateRec dt,dtStart; LongDateCvt theTime; enum { kNewYear=0x01,kSkipYear=0x02,kNewMonth=0x04,kSkipMonth=0x08, kNewWeek=0x10,kSkipWeek=0x20,kNewDay=0x40,kSkipDay=0x80, kStartYear=0x100,kStartMonth=0x200,kStartWeek=0x400,kStartDay=0x800 } timeFlags; if (!gStatData) return; theTime.hl.lHigh = 0; GetDateTime(&theTime.hl.lLow); LongSecondsToDate(&theTime.c,&dt); theTime.hl.lLow = (*gStatData)->startTime; LongSecondsToDate(&theTime.c,&dtStart); timeFlags = 0; if (dt.ld.hour != gCurrentTime.ld.hour) // facetime needs to be updated at least every hour UpdateFacetimeLo(); if (dt.ld.year > gCurrentTime.ld.year) { // Happy New Year! timeFlags |= kNewYear+kNewMonth+kNewDay; if (dt.ld.year - gCurrentTime.ld.year > 1) // Skipped a year timeFlags |= kSkipYear+kSkipMonth+kSkipWeek+kSkipDay; if (dtStart.ld.year==gCurrentTime.ld.year) // don't include very first month in average timeFlags |= kStartYear; } if (dt.ld.month > gCurrentTime.ld.month || dt.ld.year > gCurrentTime.ld.year) { // New month timeFlags |= kNewMonth+kNewDay; if (dt.ld.month - gCurrentTime.ld.month > 1) // Skipped a month timeFlags |= kSkipMonth+kSkipWeek+kSkipDay; if (dtStart.ld.year==gCurrentTime.ld.year && dtStart.ld.month==gCurrentTime.ld.month) // don't include very first day in average timeFlags |= kStartMonth; } if (dt.ld.weekOfYear > gCurrentTime.ld.weekOfYear || dt.ld.year > gCurrentTime.ld.year) { // New week timeFlags |= kNewWeek+kNewDay; if (dt.ld.weekOfYear - gCurrentTime.ld.weekOfYear > 1) // Skipped a week timeFlags |= kSkipWeek+kSkipDay; if (dtStart.ld.year==gCurrentTime.ld.year && dtStart.ld.weekOfYear==gCurrentTime.ld.weekOfYear) // don't include very first day in average timeFlags |= kStartWeek; } if (dt.ld.dayOfYear > gCurrentTime.ld.dayOfYear || dt.ld.year > gCurrentTime.ld.year) { // New day timeFlags |= kNewDay; if (dt.ld.dayOfYear - gCurrentTime.ld.dayOfYear > 1) // Skipped a day timeFlags |= kSkipDay; if (dtStart.ld.year==gCurrentTime.ld.year && dtStart.ld.dayOfYear==gCurrentTime.ld.dayOfYear) // don't include very first hour in average timeFlags |= kStartDay; } if (timeFlags) { short i; NumericStats *pStats; ShortNumericStats *pShortStats; LDRef(gStatData); // Do numeric stats for(i=0,pStats=&(*gStatData)->numStats;icurrent.year,pStats->last.year,sizeof(pStats->current.year)); if (timeFlags&kStartYear) // don't use first partial month pStats->current.year[dtStart.ld.month-1] = 0; AddToAve(pStats->average.year,pStats->current.year,kYearStatCount); Zero(pStats->current.year); if (timeFlags&kSkipYear) Zero(pStats->last.year); } // Do month if (timeFlags&kNewMonth) { BMD(pStats->current.month,pStats->last.month,sizeof(pStats->current.month)); if (timeFlags&kStartMonth) // don't use first partial day pStats->current.month[dtStart.ld.day-1] = 0; AddToAve(pStats->average.month,pStats->current.month,kMonthStatCount); Zero(pStats->current.month); if (timeFlags&kSkipMonth) Zero(pStats->last.month); } // Do week if (timeFlags&kNewWeek) { BMD(pStats->current.week,pStats->last.week,sizeof(pStats->current.week)); if (timeFlags&kStartWeek) // don't use first partial day pStats->current.week[dtStart.ld.dayOfWeek-1] = 0; AddToAve(pStats->average.week,pStats->current.week,kWeekStatCount); Zero(pStats->current.week); if (timeFlags&kSkipWeek) Zero(pStats->last.week); } // Do day if (timeFlags&kNewDay) { BMD(pStats->current.day,pStats->last.day,sizeof(pStats->current.day)); if (timeFlags&kStartDay) // don't use first partial hour pStats->current.day[dtStart.ld.hour] = 0; AddToAve(pStats->average.day,pStats->current.day,kDayStatCount); Zero(pStats->current.day); if (timeFlags&kSkipDay) Zero(pStats->last.day); } } // Do short stats for(i=0,pShortStats=&(*gStatData)->shortStats;icurrent.year = 0; pShortStats->last.year = pShortStats->current.year; pShortStats->current.year = 0; } // Do month if (timeFlags&kNewMonth) { if (timeFlags&kSkipMonth) pShortStats->current.month = 0; pShortStats->last.month = pShortStats->current.month; pShortStats->current.month = 0; } // Do week if (timeFlags&kNewWeek) { if (timeFlags&kSkipWeek) pShortStats->current.week = 0; pShortStats->last.week = pShortStats->current.week; pShortStats->current.week = 0; } // Do day if (timeFlags&kNewDay) { if (timeFlags&kSkipDay) pShortStats->current.day = 0; pShortStats->last.day = pShortStats->current.day; pShortStats->current.day = 0; } } gAutoUpdateTicks = TickCount() + kAutoUpdateTicks; UL(gStatData); } gCurrentTime = dt; } /************************************************************************ * AddToAve - add current stats to average ************************************************************************/ static void AddToAve(uLong *to,uLong *from,short length) { while(length--) *to++ += *from++; } /************************************************************************ * LoadStats - load stats from file in XML format ************************************************************************/ OSErr LoadStats(void) { OSErr err; FSSpec spec,renameSpec; Str255 s; Handle hStatXML; LongDateCvt theTime; if (gStatData) return noErr; // Already got it gStatData = NewZH(StatData); if (!gStatData) return MemError (); (*gStatData)->startTime = (*gStatData)->junkStartTime = (*gStatData)->currentTime = LocalDateTime(); // in case we can't get any saved stats FSMakeFSSpec(Root.vRef,Root.dirId,GetRString(s,STATISTICS_FILE),&spec); err = Snarf(&spec,&hStatXML,nil); if (!err) { ParseStatFile(hStatXML); ZapHandle(hStatXML); } else if (!FSpExists(&spec)) { // There was a problem loading the stats data. Either the file doesn't exist yet // or there was a file error. In the latter case, rename the bad file so we have // a backup and so we can create a new one. renameSpec = spec; PSCat(renameSpec.name,"\p.bad"); UniqueSpec(&renameSpec,31); FSpRename(&spec,renameSpec.name); } theTime.hl.lHigh = 0; theTime.hl.lLow = (*gStatData)->currentTime; LongSecondsToDate(&theTime.c,&gCurrentTime); CheckTimeChange(); return err; } /************************************************************************ * SaveStats - save stats to file in XML format ************************************************************************/ OSErr SaveStats(Boolean force) { OSErr err; FSSpec spec; Str255 s,sValue; Accumulator a; StatDataPtr pStatData; short i; if (!gStatData || !gStatsDirty) return noErr; if (!force && !DiskSpunUp()) return noErr; // if the disk isn't spun up, bail CheckTimeChange(); (*gStatData)->currentTime = LocalDateTime(); pStatData = LDRef(gStatData); // start XML AccuInit(&a); AccuAddStr(&a,GetRString(s,StatXMLStrn+ksStatXMLVersion)); AccuAddCRLF(&a); AccuAddTagLine(&a,GetRString(s,StatXMLStrn+ksStatDocEntry),false); XMLIncIndent(); // initial data AccuAddXMLObjectInt(&a,GetRString(s,StatXMLStrn+ksStatVersion),kStatFileVersion); // version AccuAddXMLObject(&a,GetRString(s,StatXMLStrn+ksStatStartTime),R822Date(sValue,pStatData->startTime-ZoneSecs())); // start time AccuAddXMLObject(&a,GetRString(s,StatXMLStrn+ksStatJunkStartTime),R822Date(sValue,pStatData->junkStartTime-ZoneSecs())); // junk start time AccuAddXMLObject(&a,GetRString(s,StatXMLStrn+ksStatCurrentTime),R822Date(sValue,pStatData->currentTime-ZoneSecs())); // current time AccuAddCRLF(&a); // number stats for(i=0;inumStats[i].current); AccuAddNumStats(&a,StatXMLStrn+ksStatPrevious,&pStatData->numStats[i].last); AccuAddNumStats(&a,StatXMLStrn+ksStatAveSum,&pStatData->numStats[i].average); AccuAddXMLObjectInt(&a,GetRString(s,StatXMLStrn+ksStatTotal),pStatData->numStats[i].total); } else { // short number stats AccuAddShortStats(&a,StatXMLStrn+ksStatCurrent,&pStatData->shortStats[i-kBeginShortStats-1].current); AccuAddShortStats(&a,StatXMLStrn+ksStatPrevious,&pStatData->shortStats[i-kBeginShortStats-1].last); AccuAddShortStats(&a,StatXMLStrn+ksStatAveSum,&pStatData->shortStats[i-kBeginShortStats-1].average); AccuAddXMLObjectInt(&a,GetRString(s,StatXMLStrn+ksStatTotal),pStatData->shortStats[i-kBeginShortStats-1].total); } XMLDecIndent(); AccuAddTagLine(&a,GetRString(s,StatXMLStrn+kwdStrTab[i]),true); AccuAddCRLF(&a); } // end XML XMLDecIndent(); AccuAddTagLine(&a,GetRString(s,StatXMLStrn+ksStatDocEntry),true); AccuTrim(&a); UL(gStatData); // save out to file FSMakeFSSpec(Root.vRef,Root.dirId,GetRString(s,STATISTICS_FILE),&spec); err = Blat(&spec,a.data,false); AccuZap(a); gStatsDirty = false; return err; } /************************************************************************ * ResetStatistics - throw away the stats and start all over ************************************************************************/ void ResetStatistics(void) { FSSpec spec; Str255 s; ShutdownStats(); FSMakeFSSpec(Root.vRef,Root.dirId,GetRString(s,STATISTICS_FILE),&spec); FSpTrash(&spec); InitStats(); } /************************************************************************ * AccuAddShortStats - save short stats ************************************************************************/ static void AccuAddShortStats(AccuPtr a,short keyword,ShortPeriodicStats *stats) { Str63 s; AccuAddTagLine(a,GetRString(s,keyword),false); XMLIncIndent(); AccuAddXMLObjectInt(a,GetRString(s,StatXMLStrn+ksStatDay),stats->day); AccuAddXMLObjectInt(a,GetRString(s,StatXMLStrn+ksStatWeek),stats->week); AccuAddXMLObjectInt(a,GetRString(s,StatXMLStrn+ksStatMonth),stats->month); AccuAddXMLObjectInt(a,GetRString(s,StatXMLStrn+ksStatYear),stats->year); XMLDecIndent(); AccuAddTagLine(a,GetRString(s,keyword),true); } /************************************************************************ * AccuAddNumStats - save number stats ************************************************************************/ static void AccuAddNumStats(AccuPtr a,short keyword,PeriodicStats *stats) { Str63 s; AccuAddTagLine(a,GetRString(s,keyword),false); XMLIncIndent(); AccuAddArray(a,StatXMLStrn+ksStatDay,&stats->day,kDayStatCount); AccuAddArray(a,StatXMLStrn+ksStatWeek,&stats->week,kWeekStatCount); AccuAddArray(a,StatXMLStrn+ksStatMonth,&stats->month,kMonthStatCount); AccuAddArray(a,StatXMLStrn+ksStatYear,&stats->year,kYearStatCount); XMLDecIndent(); AccuAddTagLine(a,GetRString(s,keyword),true); } /************************************************************************ * AccuAddArray - save an array of data ************************************************************************/ static void AccuAddArray(AccuPtr a,short keyword,uLong *stats,short count) { Str63 s; short i; AccuIndent(a); AccuAddTag(a,GetRString(s,keyword),false); for(i=0;i 0) { UpdateNumStatLo(kStatFaceTime,faceTime); UpdateNumStatLo(gFacetimeMode==kFacetimeOther?kStatFaceTimeOther:gFacetimeMode==kFacetimeRead?kStatFaceTimeRead:kStatFaceTimeCompose,faceTime); } FaceMeasureReset(gFaceMeasure); } /************************************************************************ * SumArray - return sum of long array ************************************************************************/ static long SumArray(long *values,short len) { short i; long sum=0; for(i=0;itotal/secs : 0 ; switch (period) { case kStatDay: values[kThisStat] = SumArray(stats->current.day,kDayStatCount); values[kLastStat] = SumArray(stats->last.day,kDayStatCount); values[kAveStat] = dayAve; break; case kStatWeek: values[kThisStat] = SumArray(stats->current.week,kWeekStatCount); values[kLastStat] = SumArray(stats->last.week,kWeekStatCount); values[kAveStat] = 7.0*dayAve; break; case kStatMonth: values[kThisStat] = SumArray(stats->current.month,kMonthStatCount); values[kLastStat] = SumArray(stats->last.month,kMonthStatCount); values[kAveStat] = 30.4375*dayAve; break; case kStatYear: values[kThisStat] = SumArray(stats->current.year,kYearStatCount); values[kLastStat] = SumArray(stats->last.year,kYearStatCount); values[kAveStat] = 365.25*dayAve; break; } values[kTotalStat] = stats->total; } /************************************************************************ * GetStatTotalPercents - get some totals ************************************************************************/ void GetStatTotalPercents(NumericStats *statNumerator,NumericStats *statDenominator,StatTimePeriod period,uLong values[]) { switch (period) { case kStatDay: values[kThisStat] = (100*SumArray(statNumerator->current.day,kDayStatCount))/SumArray(statDenominator->current.day,kDayStatCount); values[kLastStat] = (100*SumArray(statNumerator->last.day,kDayStatCount))/SumArray(statDenominator->last.day,kDayStatCount); break; case kStatWeek: values[kThisStat] = (100*SumArray(statNumerator->current.week,kDayStatCount))/SumArray(statDenominator->current.week,kDayStatCount); values[kLastStat] = (100*SumArray(statNumerator->last.week,kDayStatCount))/SumArray(statDenominator->last.week,kDayStatCount); break; case kStatMonth: values[kThisStat] = (100*SumArray(statNumerator->current.month,kDayStatCount))/SumArray(statDenominator->current.month,kDayStatCount); values[kLastStat] = (100*SumArray(statNumerator->last.month,kDayStatCount))/SumArray(statDenominator->last.month,kDayStatCount); break; case kStatYear: values[kThisStat] = (100*SumArray(statNumerator->current.year,kDayStatCount))/SumArray(statDenominator->current.year,kDayStatCount); values[kLastStat] = (100*SumArray(statNumerator->last.year,kDayStatCount))/SumArray(statDenominator->last.year,kDayStatCount); break; } values[kTotalStat] = (100*statNumerator->total)/statDenominator->total; } /************************************************************************ * GetStatsAsText - return stats in HTML format ************************************************************************/ Handle GetStatsAsText(StatTimePeriod period,Boolean extended) { Accumulator a; ComposeStatsData data; Str255 s; Str32 sTime; short rPeriodTab[] = { sStatDay,sStatWeek,sStatMonth,sStatYear }; short rThisTab[] = { sStatToday,sStatThisWeek,sStatThisMonth,sStatThisYear }; short rLastTab[] = { sStatYesterday,sStatLastWeek,sStatLastMonth,sStatLastYear }; uLong secondsTab[] = { 24*60*60,7*24*60*60,31*24*60*60,365*24*60*60 }; Boolean oldShowAve; Boolean canScoreJunk = CanScoreJunk(); PushGWorld(); data.period = period; data.rPeriod = StatHTMLStrn+rPeriodTab[period]; data.rThis = StatHTMLStrn+rThisTab[period]; data.rLast = StatHTMLStrn+rLastTab[period]; // Don't display averages until we have statistics for 24 hours data.showAverage = LocalDateTime() > (*gStatData)->startTime + 60*60*24; if (AccuInit(&a)) { PopGWorld(); return nil; } if (!data.showAverage || ((LocalDateTime()-(*gStatData)->startTime) < secondsTab[period])) GetRString(data.sProjected,StatHTMLStrn+sStatProjected); else data.sProjected[0] = 0; UpdateCalcStats(); AccuAddRes(&a,StatHTMLStrn+sStatHTMLBegin); // begin HTML LDRef(gStatData); if (!extended) { ComposeNumStats(&a,kStatReceivedMail,sStatRecdEmail,&data); // Received Email AccuAddRes(&a,StatHTMLStrn+sStatDivider); // divider if (canScoreJunk) { oldShowAve = data.showAverage; data.showAverage = false; ComposeNumStats(&a,kStatJunkPercent,sStatJunkPercent,&data); // Junk; calculated data.showAverage = oldShowAve; AccuAddRes(&a,StatHTMLStrn+sStatDivider); // divider } ComposeNumStats(&a,kStatSentMail,sStatSentEmail,&data); // Sent Email AccuAddRes(&a,StatHTMLStrn+sStatDivider); // divider ComposeNumStats(&a,kStatFaceTime,0,&data); // Usage AccuAddRes(&a,StatHTMLStrn+sStatDivider); // divider } else { // More stats ComposeNumStats(&a,kStatReceivedMail,sStatRecdEmail,&data); // Received Email ComposeShortStats(&a,kStatReadMsg,sStatMsgsRead,&data,StatHTMLStrn+sStatMsgZero,true); // Read mail ComposeShortStats(&a,kStatReceivedAttach,sStatAttachRecv,&data,StatHTMLStrn+sStatAttachZero,false); // Received attachments AccuAddRes(&a,StatHTMLStrn+sStatDivider); // divider if (canScoreJunk) { oldShowAve = data.showAverage; data.showAverage = false; ComposeNumStats(&a,kStatJunkPercent,sStatJunkPercent,&data); // Junk; calculated data.showAverage = oldShowAve; ComposeJunkJunk(&a,&data); AccuAddRes(&a,StatHTMLStrn+sStatDivider); // divider } ComposeNumStats(&a,kStatSentMail,sStatSentEmail,&data); // Sent Email ComposeFRR(&a,&data); // Forward, Reply, Redirect ComposeShortStats(&a,kStatSentAttach,sStatAttachSent,&data,StatHTMLStrn+sStatAttachZero,false); // Sent attachments AccuAddRes(&a,StatHTMLStrn+sStatDivider); // divider ComposeNumStats(&a,kStatFaceTime,0,&data); // Usage ComposeUsageActivities(&a,&data); // reading, composing, other AccuAddRes(&a,StatHTMLStrn+sStatDivider); // divider } // Since when? DateString((*gStatData)->startTime,longDate,s,nil); TimeString((*gStatData)->startTime,false,sTime,nil); AccuComposeR(&a,StatHTMLStrn+sStatSince,s,sTime); // Current time DateString(LocalDateTime(),longDate,s,nil); TimeString(LocalDateTime(),false,sTime,nil); AccuComposeR(&a,StatHTMLStrn+sStatCurrentTime,s,sTime); UL(gStatData); AccuAddRes(&a,StatHTMLStrn+sStatHTMLEnd); // end HTML PopGWorld(); return a.data; } /************************************************************************ * ComposeFRR - show forward, reply, redirect ************************************************************************/ static void ComposeFRR(AccuPtr a, ComposeStatsData *data) { uLong secs; Str32 sThisVal,sLastVal,sAveVal,sTotalVal; Str63 sThisPeriod,sLastPeriod; ShortNumericStats *sStats; float dayAve; PicHandle hPict; GraphData graphData; SeriesInfo theSeries[3]; StringPtr labels[3]; uLong thisSeries[3],lastSeries[3],aveSeries[3],totalSeries; short i; for(i=0;i<3;i++) { sStats = &(*gStatData)->shortStats[kStatForwardMsg-kBeginShortStats-1+i]; secs = LocalDateTime() - (*gStatData)->startTime; dayAve = secs ? 10*24.0*60.0*60.0*(float)sStats->total/secs : 0 ; switch (data->period) { case kStatDay: thisSeries[i] = sStats->current.day; lastSeries[i] = sStats->last.day; aveSeries[i] = dayAve; break; case kStatWeek: thisSeries[i] = sStats->current.week; lastSeries[i] = sStats->last.week; aveSeries[i] = dayAve*7.0; break; case kStatMonth: thisSeries[i] = sStats->current.month; lastSeries[i] = sStats->last.month; aveSeries[i] = dayAve*30.4375; break; case kStatYear: thisSeries[i] = sStats->current.year; lastSeries[i] = sStats->last.year; aveSeries[i] = dayAve*365.25; break; } totalSeries = sStats->total; RightAlign7(thisSeries[i],sThisVal); RightAlign7(lastSeries[i],sLastVal); RightAlign7Dec(aveSeries[i],sAveVal); if (!data->showAverage) memset(sAveVal+1,'Ê',*sAveVal); // replace with spaces RightAlign7(totalSeries,sTotalVal); AccuComposeR(a,StatHTMLStrn+sStatFRR,StatHTMLStrn+sStatForward+i, sThisVal,WordStrn(StatHTMLStrn+sStatMsgZero,thisSeries[i]),data->rThis, sLastVal,WordStrn(StatHTMLStrn+sStatMsgZero,lastSeries[i]),data->rLast, sAveVal,data->rPeriod,data->sProjected, sTotalVal,WordStrn(StatHTMLStrn+sStatMsgZero,totalSeries)); } // add a graph labels[0] = GetRString(sThisPeriod,data->rThis); labels[1] = GetRString(sLastPeriod,data->rLast); labels[2] = GetRString(sAveVal,StatHTMLStrn+sStatAverage); Zero(theSeries); if (hPict = InitGraphPict(&graphData,theSeries,kStatFaceTime,labels)) { graphData.dataCount = 3; graphData.type = kBarGraph; graphData.xLabels = GetResource('STR#',FRRStrn); theSeries[0].data = thisSeries; theSeries[1].data = lastSeries; theSeries[2].data = aveSeries; theSeries[2].divider = 10; // increase average graph resolution if (!CalcElapsedUnits(data->period)) graphData.seriesCount = 2; // not enough data to do average yet FinishGraphPict(&graphData); AccuComposeR(a,StatHTMLStrn+sStatPicImage,hPict); } } /************************************************************************ * ComposeJunkJunk - show more junk stats ************************************************************************/ void ComposeJunkJunk(AccuPtr a, ComposeStatsData *data) { Str32 sThis, sLast; uLong junkTotal, total, falseNeg, falsePos, falseWhite; uLong lastJunkTotal, lastTotal, lastFalseNeg, lastFalsePos, lastFalseWhite; switch (data->period) { case kStatDay: junkTotal = SumArrayHi((*gStatData)->numStats[kStatJunkTotal].current.day); total = SumArrayHi((*gStatData)->numStats[kStatTotal].current.day); falseNeg = SumArrayHi((*gStatData)->numStats[kStatFalseNegatives].current.day); falsePos = SumArrayHi((*gStatData)->numStats[kStatFalsePositives].current.day); falseWhite = SumArrayHi((*gStatData)->numStats[kStatFalseWhiteList].current.day); lastJunkTotal = SumArrayHi((*gStatData)->numStats[kStatJunkTotal].last.day); lastTotal = SumArrayHi((*gStatData)->numStats[kStatTotal].last.day); lastFalseNeg = SumArrayHi((*gStatData)->numStats[kStatFalseNegatives].last.day); lastFalsePos = SumArrayHi((*gStatData)->numStats[kStatFalsePositives].last.day); lastFalseWhite = SumArrayHi((*gStatData)->numStats[kStatFalseWhiteList].last.day); break; case kStatWeek: junkTotal = SumArrayHi((*gStatData)->numStats[kStatJunkTotal].current.week); total = SumArrayHi((*gStatData)->numStats[kStatTotal].current.week); falseNeg = SumArrayHi((*gStatData)->numStats[kStatFalseNegatives].current.week); falsePos = SumArrayHi((*gStatData)->numStats[kStatFalsePositives].current.week); falseWhite = SumArrayHi((*gStatData)->numStats[kStatFalseWhiteList].current.week); lastJunkTotal = SumArrayHi((*gStatData)->numStats[kStatJunkTotal].last.week); lastTotal = SumArrayHi((*gStatData)->numStats[kStatTotal].last.week); lastFalseNeg = SumArrayHi((*gStatData)->numStats[kStatFalseNegatives].last.week); lastFalsePos = SumArrayHi((*gStatData)->numStats[kStatFalsePositives].last.week); lastFalseWhite = SumArrayHi((*gStatData)->numStats[kStatFalseWhiteList].last.week); break; case kStatMonth: junkTotal = SumArrayHi((*gStatData)->numStats[kStatJunkTotal].current.month); total = SumArrayHi((*gStatData)->numStats[kStatTotal].current.month); falseNeg = SumArrayHi((*gStatData)->numStats[kStatFalseNegatives].current.month); falsePos = SumArrayHi((*gStatData)->numStats[kStatFalsePositives].current.month); falseWhite = SumArrayHi((*gStatData)->numStats[kStatFalseWhiteList].current.month); lastJunkTotal = SumArrayHi((*gStatData)->numStats[kStatJunkTotal].last.month); lastTotal = SumArrayHi((*gStatData)->numStats[kStatTotal].last.month); lastFalseNeg = SumArrayHi((*gStatData)->numStats[kStatFalseNegatives].last.month); lastFalsePos = SumArrayHi((*gStatData)->numStats[kStatFalsePositives].last.month); lastFalseWhite = SumArrayHi((*gStatData)->numStats[kStatFalseWhiteList].last.month); break; case kStatYear: junkTotal = SumArrayHi((*gStatData)->numStats[kStatJunkTotal].current.year); total = SumArrayHi((*gStatData)->numStats[kStatTotal].current.year); falseNeg = SumArrayHi((*gStatData)->numStats[kStatFalseNegatives].current.year); falsePos = SumArrayHi((*gStatData)->numStats[kStatFalsePositives].current.year); falseWhite = SumArrayHi((*gStatData)->numStats[kStatFalseWhiteList].current.year); lastJunkTotal = SumArrayHi((*gStatData)->numStats[kStatJunkTotal].last.year); lastTotal = SumArrayHi((*gStatData)->numStats[kStatTotal].last.year); lastFalseNeg = SumArrayHi((*gStatData)->numStats[kStatFalseNegatives].last.year); lastFalsePos = SumArrayHi((*gStatData)->numStats[kStatFalsePositives].last.year); lastFalseWhite = SumArrayHi((*gStatData)->numStats[kStatFalseWhiteList].last.year); break; } // no divide by zero if (!total) total = 1; if (!lastTotal) lastTotal = 1; AccuComposeR(a,StatHTMLStrn+sStatMsgAndPercent, StatHTMLStrn+sStatJunkJunk, // Header for section RightAlign7(junkTotal,sThis),WordStrn(StatHTMLStrn+sStatMsgZero,junkTotal),(100*junkTotal)/total,data->rThis, // this year's info RightAlign7(lastJunkTotal,sLast),WordStrn(StatHTMLStrn+sStatMsgZero,lastJunkTotal),(100*lastJunkTotal)/lastTotal,data->rLast); // last year's info AccuComposeR(a,StatHTMLStrn+sStatMsgAndPercent, StatHTMLStrn+sStatJunkFalseNeg, // Header for section RightAlign7(falseNeg,sThis),WordStrn(StatHTMLStrn+sStatMsgZero,falseNeg),(100*falseNeg)/total,data->rThis, // this year's info RightAlign7(lastFalseNeg,sLast),WordStrn(StatHTMLStrn+sStatMsgZero,lastFalseNeg),(100*lastFalseNeg)/lastTotal,data->rLast); // last year's info AccuComposeR(a,StatHTMLStrn+sStatMsgAndPercent, StatHTMLStrn+sStatJunkFalsePos, // Header for section RightAlign7(falsePos,sThis),WordStrn(StatHTMLStrn+sStatMsgZero,falsePos),(100*falsePos)/total,data->rThis, // this year's info RightAlign7(lastFalsePos,sLast),WordStrn(StatHTMLStrn+sStatMsgZero,lastFalsePos),(100*lastFalsePos)/lastTotal,data->rLast); // last year's info if (JunkPrefWhiteList()) AccuComposeR(a,StatHTMLStrn+sStatMsgAndPercent, StatHTMLStrn+sStatJunkFalseWhite, // Header for section RightAlign7(falseWhite,sThis),WordStrn(StatHTMLStrn+sStatMsgZero,falseWhite),(100*falseWhite)/total,data->rThis, // this year's info RightAlign7(lastFalseWhite,sLast),WordStrn(StatHTMLStrn+sStatMsgZero,lastFalseWhite),(100*lastFalseWhite)/lastTotal,data->rLast); // last year's info AccuComposeR(a,StatHTMLStrn+sStatPercent, StatHTMLStrn+sStatAccuracy, // Header for section RightAlign7((100*(total-falsePos-falseNeg-falseWhite))/(total-falseWhite),sThis),data->rThis, // this year's info RightAlign7((100*(lastTotal-lastFalsePos-lastFalseNeg-lastFalseWhite))/(lastTotal-lastFalseWhite),sLast),data->rLast); // last year's info } /************************************************************************ * ComposeUsageActivities - show reading, composing, other percentages ************************************************************************/ static void ComposeUsageActivities(AccuPtr a, ComposeStatsData *data) { uLong read,compose,other,total,readPercent,composePercent,otherPercent; Str32 sRead,sCompose,sOther; PicHandle hPict; GraphData graphData; SeriesInfo theSeries[3]; StringPtr labels[3]; uLong series[3]; switch (data->period) { case kStatDay: read = (*gStatData)->shortStats[kStatFaceTimeRead-kBeginShortStats-1].current.day; compose = (*gStatData)->shortStats[kStatFaceTimeCompose-kBeginShortStats-1].current.day; other = (*gStatData)->shortStats[kStatFaceTimeOther-kBeginShortStats-1].current.day; break; case kStatWeek: read = (*gStatData)->shortStats[kStatFaceTimeRead-kBeginShortStats-1].current.week; compose = (*gStatData)->shortStats[kStatFaceTimeCompose-kBeginShortStats-1].current.week; other = (*gStatData)->shortStats[kStatFaceTimeOther-kBeginShortStats-1].current.week; break; case kStatMonth: read = (*gStatData)->shortStats[kStatFaceTimeRead-kBeginShortStats-1].current.month; compose = (*gStatData)->shortStats[kStatFaceTimeCompose-kBeginShortStats-1].current.month; other = (*gStatData)->shortStats[kStatFaceTimeOther-kBeginShortStats-1].current.month; break; case kStatYear: read = (*gStatData)->shortStats[kStatFaceTimeRead-kBeginShortStats-1].current.year; compose = (*gStatData)->shortStats[kStatFaceTimeCompose-kBeginShortStats-1].current.year; other = (*gStatData)->shortStats[kStatFaceTimeOther-kBeginShortStats-1].current.year; break; } total = read+compose+other; readPercent = read*100/total; composePercent = compose*100/total; otherPercent = 100 - readPercent - composePercent; RightAlign7(readPercent,sRead); RightAlign7(composePercent,sCompose); RightAlign7(otherPercent,sOther); AccuComposeR(a,StatHTMLStrn+sStatUsageAct,data->rThis,sRead,sCompose,sOther); // add a graph Zero(theSeries); labels[0] = GetRString(sRead,StatHTMLStrn+sStatReading); labels[1] = GetRString(sCompose,StatHTMLStrn+sStatComposing); labels[2] = GetRString(sOther,StatHTMLStrn+sStatOther); if (hPict = InitGraphPict(&graphData,theSeries,kStatFaceTime,labels)) { graphData.type = kPieGraph; graphData.dataCount = 3; theSeries[0].data = series; series[0] = readPercent; series[1] = composePercent; series[2] = otherPercent; FinishGraphPict(&graphData); AccuComposeR(a,StatHTMLStrn+sStatPicImage,hPict); } } /************************************************************************ * ComposeShortStats - compose a set of statistics ************************************************************************/ static void ComposeShortStats(AccuPtr a,StatType type,short strType,ComposeStatsData *data,short rObject,Boolean percent) { uLong thisVal,lastVal,aveVal; Str32 sThisVal,sLastVal,sAveVal,sTotalVal; float dayAve,secs,recvMailAve; ShortNumericStats *stats; NumericStats *recvMailStats; secs = LocalDateTime() - (*gStatData)->startTime; // average is times 10 so we can do a single decimal place stats = &(*gStatData)->shortStats[type-kBeginShortStats-1]; dayAve = secs ? 10*24.0*60.0*60.0*(float)stats->total/secs : 0 ; if (type==kStatReadMsg) { recvMailStats = &(*gStatData)->numStats[kStatReceivedMail]; recvMailAve = secs ? 24.0*60.0*60.0*(float)recvMailStats->total/secs : 0 ; aveVal = dayAve * 100 / recvMailAve; } switch (data->period) { case kStatDay: thisVal = stats->current.day; lastVal = stats->last.day; if (percent) { thisVal = thisVal * 100 / SumArray(recvMailStats->current.day,kDayStatCount); lastVal = lastVal * 100 / SumArray(recvMailStats->last.day,kDayStatCount); } else aveVal = dayAve; break; case kStatWeek: thisVal = stats->current.week; lastVal = stats->last.week; if (percent) { thisVal = thisVal * 100 / SumArray(recvMailStats->current.week,kWeekStatCount); lastVal = lastVal * 100 / SumArray(recvMailStats->last.week,kWeekStatCount); } else aveVal = 7.0*dayAve; break; case kStatMonth: thisVal = stats->current.month; lastVal = stats->last.month; if (percent) { thisVal = thisVal * 100 / SumArray(recvMailStats->current.month,kMonthStatCount); lastVal = lastVal * 100 / SumArray(recvMailStats->last.month,kMonthStatCount); } else aveVal = 30.4375*dayAve; break; case kStatYear: thisVal = stats->current.year; lastVal = stats->last.year; if (percent) { thisVal = thisVal * 100 / SumArray(recvMailStats->current.year,kYearStatCount); lastVal = lastVal * 100 / SumArray(recvMailStats->last.year,kYearStatCount); } else aveVal = 365.25*dayAve; break; } if (percent) { // make sure average values don't exceen 100% if (thisVal > 100) thisVal = 100; if (lastVal > 100) lastVal = 100; if (aveVal > 1000) aveVal = 1000; // average is times 10 } RightAlign7(thisVal,sThisVal); RightAlign7(lastVal,sLastVal); RightAlign7Dec(aveVal,sAveVal); if (!data->showAverage) memset(sAveVal+1,'Ê',*sAveVal); // replace with spaces RightAlign7(stats->total,sTotalVal); if (percent) { AccuComposeR(a,StatHTMLStrn+strType, sThisVal,WordStrn(rObject,percent?2:thisVal),data->rThis, sLastVal,WordStrn(rObject,percent?2:lastVal),data->rLast, sAveVal, sTotalVal,WordStrn(rObject,stats->total)); } else { AccuComposeR(a,StatHTMLStrn+strType, sThisVal,WordStrn(rObject,percent?2:thisVal),data->rThis, sLastVal,WordStrn(rObject,percent?2:lastVal),data->rLast, sAveVal,data->rPeriod,data->sProjected, sTotalVal,WordStrn(rObject,stats->total)); } } /************************************************************************ * ComposeNumStats - compose a set of statistics ************************************************************************/ static void ComposeNumStats(AccuPtr a,StatType type,short strType,ComposeStatsData *data) { uLong values[4]; PicHandle hPict; Str32 sThisVal,sLastVal,sAveVal,sTotalVal; Str63 sThisPeriod,sLastPeriod; if (type==kStatJunkPercent) GetStatTotalPercents(&(*gStatData)->numStats[kStatJunkTotal],&(*gStatData)->numStats[kStatTotal],data->period,values); else GetStatTotals(&(*gStatData)->numStats[type],data->period,values,type!=kStatJunkPercent?(*gStatData)->startTime:(*gStatData)->junkStartTime); if (type == kStatFaceTime) { // Usage RightAlign7Dec(values[kThisStat]/360,sThisVal); RightAlign7Dec(values[kLastStat]/360,sLastVal); RightAlign7Dec(values[kAveStat]/3600,sAveVal); if (!data->showAverage) memset(sAveVal+1,'Ê',*sAveVal); // replace with spaces RightAlign7Dec(values[kTotalStat]/360,sTotalVal); AccuComposeR(a,StatHTMLStrn+sStatUsageEntry, sThisVal,data->rThis, sLastVal,data->rLast, sAveVal,data->rPeriod,data->sProjected, sTotalVal); } else { short fmt = data->showAverage ? sStatMsgsEntry : sStatNoAveEntry; short unit = type==kStatJunkPercent ? sStatPercentZero : sStatMsgZero; RightAlign7(values[kThisStat],sThisVal); RightAlign7(values[kLastStat],sLastVal); RightAlign7Dec(values[kAveStat],sAveVal); RightAlign7(values[kTotalStat],sTotalVal); AccuComposeR(a,StatHTMLStrn+fmt,StatHTMLStrn+strType, sThisVal,WordStrn(StatHTMLStrn+unit,values[kThisStat]),data->rThis, sLastVal,WordStrn(StatHTMLStrn+unit,values[kLastStat]),data->rLast, sAveVal,data->rPeriod,data->sProjected, sTotalVal,WordStrn(StatHTMLStrn+unit,values[kTotalStat])); } // add a graph hPict = GetGraphPict(&(*gStatData)->numStats[type],data->period,type == kStatFaceTime,data->showAverage, GetRString(sThisPeriod,data->rThis),GetRString(sLastPeriod,data->rLast),GetRString(sAveVal,StatHTMLStrn+sStatAverage)); if (hPict) AccuComposeR(a,StatHTMLStrn+sStatPicImage,hPict); } /************************************************************************ * WordStrn - return correct singular or plural form of string resource ************************************************************************/ static short WordStrn(short rStrn,long value) { if (value==0) return rStrn; else if (value==1) return rStrn+1; else return rStrn+2; } /************************************************************************ * RightAlign7 - right align with 7 chars in field ************************************************************************/ static UPtr RightAlign7(uLong value,UPtr s) { return RightAlign(value,s,7,0); } /************************************************************************ * RightAlign7Dec - right align with 7 chars in field and use decimal place ************************************************************************/ static UPtr RightAlign7Dec(uLong value,UPtr s) { return RightAlign(value,s,7,1); } /************************************************************************ * RightAlign - right align with X chars in field, and add any commas ************************************************************************/ static UPtr RightAlign(uLong value,UPtr s,short minLen,short decPlace) { short len; char cTokThousands, cDecPoint; if (decPlace && value >= 100) { // number is big enough we aren't going to worry about decimal while (decPlace>0) { value /= 10; decPlace--; } } NumToString(value,s); if (decPlace && (cDecPoint = GetIntlNumberPart(tokDecPoint))) { if (*s==1) PInsertC(s,sizeof(s),'0',s+1); PInsertC(s,sizeof(s),cDecPoint,s+*s-decPlace+1); } // Put in some commas if large number len = *s; if (cTokThousands = GetIntlNumberPart(tokThousands)) { if (len > 9) PInsertC(s,32,cTokThousands,s+*s-8); if (len > 6) PInsertC(s,32,cTokThousands,s+*s-5); if (len > 3) PInsertC(s,32,cTokThousands,s+*s-2); } while (*s < minLen) PInsertC(s,32,'Ê',s+1); return s; } /************************************************************************ * GetGraphPict - make a graph ************************************************************************/ static PicHandle GetGraphPict(NumericStats *stats,StatTimePeriod period,Boolean seconds,Boolean showAverage,StringPtr sThis,StringPtr sLast,StringPtr sAverage) { uLong aveVals[31],*aveRaw; short i; GraphData data; SeriesInfo theSeries[3]; StringPtr labels[3]; DateTimeRec dtStart,dtNow; uLong elapsedUnits; Boolean doAverage = true; short compare1; PicHandle hPic; labels[0] = sThis; labels[1] = sLast; labels[2] = sAverage; if (hPic = InitGraphPict(&data,theSeries,period,labels)) { data.xLabels = GetXLabels(period); if (seconds) { data.divider = 360; // 60*60/10 data.decPlace = 1; } theSeries[0].type = GetRLong(STAT_CURRENT_TYPE); theSeries[1].type = GetRLong(STAT_PREVIOUS_TYPE); theSeries[2].type = GetRLong(STAT_AVERAGE_TYPE); SecondsToDate(LocalDateTime(),&dtNow); SecondsToDate((*gStatData)->startTime,&dtStart); dtNow.minute = dtNow.second = 0; dtStart.minute = dtStart.second = 0; elapsedUnits = CalcElapsedUnits(period); switch (period) { case kStatDay: theSeries[0].data = stats->current.day; theSeries[1].data = stats->last.day; aveRaw = stats->average.day; compare1 = dtStart.hour+1; theSeries[0].dataCount = dtNow.hour+1; break; case kStatWeek: theSeries[0].data = stats->current.week; theSeries[1].data = stats->last.week; aveRaw = stats->average.week; compare1 = dtStart.dayOfWeek-1; theSeries[0].dataCount = dtNow.dayOfWeek; break; case kStatMonth: theSeries[0].data = stats->current.month; theSeries[1].data = stats->last.month; aveRaw = stats->average.month; compare1 = dtStart.day-1; theSeries[0].dataCount = dtNow.day; break; case kStatYear: theSeries[0].data = stats->current.year; theSeries[1].data = stats->last.year; aveRaw = stats->average.year; compare1 = dtStart.month-1; theSeries[0].dataCount = dtNow.month; break; } // calculate averages if (elapsedUnits && showAverage) { short divider; for(i=0;i1) divider--; aveVals[i] = (float)aveRaw[i]*10.0/divider; } theSeries[2].data = aveVals; theSeries[2].divider = 10; // increase graph resolution } else data.seriesCount = 2; // not enough data to do average yet FinishGraphPict(&data); } return hPic; } /************************************************************************ * CalcElapsedUnits - how many time periods since beginning of stats? ************************************************************************/ static uLong CalcElapsedUnits(StatTimePeriod period) { uLong startSecs,nowSecs,elapsedSecs; DateTimeRec dtStart,dtNow; uLong elapsedUnits; SecondsToDate(LocalDateTime(),&dtNow); SecondsToDate((*gStatData)->startTime,&dtStart); dtNow.minute = dtNow.second = 0; dtStart.minute = dtStart.second = 0; switch (period) { case kStatDay: dtStart.hour++; DateToSeconds(&dtStart,&startSecs); DateToSeconds(&dtNow,&nowSecs); elapsedSecs = nowSecs > startSecs ? nowSecs - startSecs : 0; elapsedUnits = elapsedSecs/(24*60*60); break; case kStatWeek: dtNow.hour = dtStart.hour = 0; dtStart.day++; DateToSeconds(&dtStart,&startSecs); DateToSeconds(&dtNow,&nowSecs); elapsedSecs = nowSecs > startSecs ? nowSecs - startSecs : 0; elapsedUnits = elapsedSecs/(7*24*60*60); break; case kStatMonth: elapsedUnits = 12*(dtNow.year-dtStart.year); dtStart.year = dtNow.year; if (TimeCompare(&dtNow,&dtStart)<0) { elapsedUnits -= 12; dtNow.month += 12; } if (dtNow.month != dtStart.month) elapsedUnits += dtNow.month - dtStart.month - 1; break; case kStatYear: elapsedUnits = dtNow.year-dtStart.year; dtStart.year = dtNow.year; if (TimeCompare(&dtNow,&dtStart)<0) elapsedUnits--; break; } return elapsedUnits; } /************************************************************************ * InitGraphPict - make a graph ************************************************************************/ static PicHandle InitGraphPict(GraphData *data,SeriesInfo *theSeries,StatTimePeriod period,StringPtr *labels) { PicHandle hPic = nil; OpenCPicParams picParams; MyWindowPtr statWin; // Need a window to use for drawing the picture // The stat window should be available if (statWin = GetStatWin()) { Zero(picParams); SetRect(&picParams.srcRect,0,0,GetRLong(STAT_GRAPH_WIDTH),GetRLong(STAT_GRAPH_HEIGHT)); picParams.hRes = 0x00480000; picParams.vRes = 0x00480000; picParams.version = -2; SetPort_(GetMyWindowCGrafPtr(statWin)); if (hPic = OpenCPicture(&picParams)) { Zero(*data); SetRect(&data->bounds,0,0,GetRLong(STAT_GRAPH_WIDTH),GetRLong(STAT_GRAPH_HEIGHT)); ClipRect(&data->bounds); data->type = kLineGraph; data->seriesCount = 3; data->dataCount = dataCountTab[period]; data->series = theSeries; data->xLabels = GetXLabels(period); data->legend = 1; GetRColor(&theSeries[0].color,STAT_CURRENT_COLOR); theSeries[0].dataCount = data->dataCount; theSeries[0].label = labels[0]; theSeries[0].divider = 0; GetRColor(&theSeries[1].color,STAT_PREVIOUS_COLOR); theSeries[1].dataCount = data->dataCount; theSeries[1].label = labels[1]; theSeries[1].divider = 0; GetRColor(&theSeries[2].color,STAT_AVERAGE_COLOR); theSeries[2].dataCount = data->dataCount; theSeries[2].label = labels[2]; theSeries[2].divider = 0; if (period==kStatDay) data->largeTickInterval = 4; else if (period==kStatMonth) data->largeTickInterval = 5; } } return hPic; } /************************************************************************ * FinishGraphPict - finish up graph PICT ************************************************************************/ static void FinishGraphPict(GraphData *data) { DrawGraph(data); ClosePicture(); } /************************************************************************ * GetXLabels - generate the X-axis labels ************************************************************************/ static Handle GetXLabels(StatTimePeriod period) { static Handle labelList[4]; Str255 s; OSErr err = noErr; if (!labelList[period] || !*labelList[period]) { // need to generate stringlist Accumulator a; short i; switch (period) { case kStatDay: labelList[period] = GetResource('STR#',StatHoursStrn); break; case kStatMonth: if (AccuInit(&a)) return nil; AccuAddPtr(&a,dataCountTab+period,sizeof(short)); // count for(i=1;i<=dataCountTab[period];i++) { if (i%5) // display every 5th value *s = 0; else NumToString(i,s); if (err = AccuAddPtr(&a,&s,*s+1)) break; } if (err) AccuZap(a); else { AccuTrim(&a); labelList[period] = a.data; } break; case kStatWeek: case kStatYear: // This is Pete's suggested way to get these // Get short names for days of week or months GetAbbrevNames(&labelList[kStatYear],&labelList[kStatWeek]); break; } } return labelList[period]; } /************************************************************************ * GetAbbrevNames - get abbreviated month and weekday names ************************************************************************/ static OSErr GetAbbrevNames(Handle *monthNames, Handle *dayNames) { Itl1ExtRec **iHndl; Boolean hasLocal, hasMonths, hasDays; long monthsLen, daysLen; if((iHndl = (Itl1ExtRec **)GetIntlResource(1)) == nil) return memFullErr; hasLocal = ((**iHndl).base.localRtn[0] == 0xA89F); hasMonths = hasLocal && ((**iHndl).abbrevMonthsTableOffset != 0) && ((**iHndl).abbrevMonthsTableLength != 0); hasDays = hasLocal && ((**iHndl).abbrevDaysTableOffset != 0) && ((**iHndl).abbrevDaysTableLength != 0); monthsLen = hasMonths ? (**iHndl).abbrevMonthsTableLength : ((**iHndl).base.abbrLen + 1) * 12; daysLen = hasDays ? (**iHndl).abbrevDaysTableLength : ((**iHndl).base.abbrLen + 1) * 7; *monthNames = NuHTempBetter(monthsLen+sizeof(short)); *dayNames = NuHTempBetter(daysLen+sizeof(short)); if((*monthNames == nil) || (*dayNames == nil)) { ZapHandle(*monthNames); return memFullErr; } if(hasMonths) { BMD(&(**iHndl).base + (**iHndl).abbrevMonthsTableOffset, **monthNames, (**iHndl).abbrevMonthsTableLength); } else { *(short *)**monthNames = 12; MoveNames(&(**iHndl).base.months, 12, (**iHndl).base.abbrLen, **monthNames + 2); } if(hasDays) { BMD(&(**iHndl).base + (**iHndl).abbrevDaysTableOffset, **dayNames, (**iHndl).abbrevDaysTableLength); } else { *(short *)**dayNames = 7; MoveNames(&(**iHndl).base.days, 7, (**iHndl).base.abbrLen, **dayNames + 2); } return noErr; } /************************************************************************ * MoveNames - move names from array to STR# format ************************************************************************/ static void MoveNames(Str15 *names, long count, long abbrLen, StringPtr dest) { short srcIdx; for(srcIdx=0;srcIdxstartTime = BeautifyDate(sToken,&zone); pStatData->startTime+=zone; break; case ksStatCurrentTime: pStatData->currentTime = BeautifyDate(sToken,&zone); pStatData->currentTime+=zone; break; case ksStatJunkStartTime: pStatData->junkStartTime = BeautifyDate(sToken,&zone); pStatData->junkStartTime+=zone; break; } break; case kFillModeNumStat: // filling number stat if (pNumValue && maxArraySize) { // array of data, scan for each data item if (tokenInfo.tokenLen) { long saveOffset = tokenInfo.offset; long saveSize = tokenInfo.size; unsigned char c; // Redirect token info to look at attributes tokenInfo.offset = tokenInfo.tokenStart; tokenInfo.size = tokenInfo.tokenStart+tokenInfo.tokenLen; for(i=0;itotal = PStrToNum(sToken); break; case kFillModeShortStat: // filling short stat if (pShortValue) *pShortValue = PStrToNum(sToken); else if (pShortStats && tokenStack[stackIdx]==ksStatTotal) pShortStats->total = PStrToNum(sToken); break; } } } else { // Process a tag short tokenIdx; tokenIdx = GetTokenIdx(tokenInfo.aKeywords,sToken); switch (tokenType) { case kElementTag: switch (tokenIdx) { case ksStatDocEntry: validDocEntry = true; break; case ksStatReceivedMail: pNumStats = &pStatData->numStats[kStatReceivedMail]; mode=kFillModeNumStat; break; case ksStatSentMail: pNumStats = &pStatData->numStats[kStatSentMail]; mode=kFillModeNumStat; break; case ksStatFaceTime: pNumStats = &pStatData->numStats[kStatFaceTime]; mode=kFillModeNumStat; break; case ksStatScoredJunk: pNumStats = &pStatData->numStats[kStatScoredJunk]; mode=kFillModeNumStat; break; case ksStatScoredNotJunk: pNumStats = &pStatData->numStats[kStatScoredNotJunk]; mode=kFillModeNumStat; break; case ksStatWhiteList: pNumStats = &pStatData->numStats[kStatWhiteList]; mode=kFillModeNumStat; break; case ksStatFalsePositives: pNumStats = &pStatData->numStats[kStatFalsePositives]; mode=kFillModeNumStat; break; case ksStatFalseNegatives: pNumStats = &pStatData->numStats[kStatFalseNegatives]; mode=kFillModeNumStat; break; case ksStatFalseWhiteList: pNumStats = &pStatData->numStats[kStatFalseWhiteList]; mode=kFillModeNumStat; break; case ksStatReceivedAttach: pShortStats = &pStatData->shortStats[kStatReceivedAttach-kBeginShortStats-1]; mode=kFillModeShortStat; break; case ksStatSentAttach: pShortStats = &pStatData->shortStats[kStatSentAttach-kBeginShortStats-1]; mode=kFillModeShortStat; break; case ksStatReadMsg: pShortStats = &pStatData->shortStats[kStatReadMsg-kBeginShortStats-1]; mode=kFillModeShortStat; break; case ksStatForwardMsg: pShortStats = &pStatData->shortStats[kStatForwardMsg-kBeginShortStats-1]; mode=kFillModeShortStat; break; case ksStatReplyMsg: pShortStats = &pStatData->shortStats[kStatReplyMsg-kBeginShortStats-1]; mode=kFillModeShortStat; break; case ksStatRedirectMsg: pShortStats = &pStatData->shortStats[kStatRedirectMsg-kBeginShortStats-1]; mode=kFillModeShortStat; break; case ksStatFaceTimeRead: pShortStats = &pStatData->shortStats[kStatFaceTimeRead-kBeginShortStats-1]; mode=kFillModeShortStat; break; case ksStatFaceTimeCompose: pShortStats = &pStatData->shortStats[kStatFaceTimeCompose-kBeginShortStats-1]; mode=kFillModeShortStat; break; case ksStatFaceTimeOther: pShortStats = &pStatData->shortStats[kStatFaceTimeOther-kBeginShortStats-1]; mode=kFillModeShortStat; break; case ksStatCurrent: if (pNumStats) pNumPeriodic = &pNumStats->current; else if (pShortStats) pShortPeriodic = &pShortStats->current; break; case ksStatPrevious: if (pNumStats) pNumPeriodic = &pNumStats->last; else if (pShortStats) pShortPeriodic = &pShortStats->last; break; case ksStatAveSum: if (pNumStats) pNumPeriodic = &pNumStats->average; else if (pShortStats) pShortPeriodic = &pShortStats->average; break; // case ksStatTotal: case ksStatDay: if (pNumPeriodic) { pNumValue = &pNumPeriodic->day; maxArraySize = kDayStatCount; } else if (pShortPeriodic) pShortValue = &pShortPeriodic->day; break; case ksStatWeek: if (pNumPeriodic) { pNumValue = &pNumPeriodic->week; maxArraySize = kWeekStatCount; } else if (pShortPeriodic) pShortValue = &pShortPeriodic->week; break; case ksStatMonth: if (pNumPeriodic) { pNumValue = &pNumPeriodic->month; maxArraySize = kMonthStatCount; } else if (pShortPeriodic) pShortValue = &pShortPeriodic->month; break; case ksStatYear: if (pNumPeriodic) { pNumValue = &pNumPeriodic->year; maxArraySize = kYearStatCount; } else if (pShortPeriodic) pShortValue = &pShortPeriodic->year; break; } // Push token on stack if (stackIdx < kMaxTokenStack-1) tokenStack[++stackIdx] = tokenIdx; break; case kEndTag: // End of element. Pop off stack. Actually search whole stack // in case document is not well-formed. while (stackIdx >= 0) { // if (tokenStack[stackIdx--] == tokenIdx) break; } switch (tokenIdx) { case ksStatDocEntry: validDocEntry = false; break; case ksStatReceivedMail: case ksStatSentMail: case ksStatFaceTime: case ksStatScoredJunk: case ksStatScoredNotJunk: case ksStatWhiteList: case ksStatFalsePositives: case ksStatFalseNegatives: case ksStatFalseWhiteList: case ksStatReceivedAttach: case ksStatSentAttach: case ksStatReadMsg: case ksStatForwardMsg: case ksStatReplyMsg: case ksStatRedirectMsg: case ksStatFaceTimeRead: case ksStatFaceTimeCompose: case ksStatFaceTimeOther: pShortStats = nil; pNumStats = nil; mode = kFillModeNone; break; case ksStatCurrent: case ksStatPrevious: case ksStatAveSum: pNumPeriodic = nil; pShortPeriodic = nil; break; // case ksStatTotal: case ksStatDay: case ksStatWeek: case ksStatMonth: case ksStatYear: pNumValue = nil; pShortValue = nil; maxArraySize = 0; break; } break; case kEmptyElementTag: // shouldn't have any of these! break; } } } AccuZap(accuKeywordList); UL(gStatData); } /************************************************************************ * GetIntlNumberPart - get an international number part ************************************************************************/ char GetIntlNumberPart(short charToken) { Handle itlHandle; long offset,length; char c = 0; GetIntlResourceTable(smSystemScript,smNumberPartsTable,&itlHandle,&offset,&length); if (itlHandle) { NumberPartsPtr pNumParts = (NumberPartsPtr)(*itlHandle + offset); c = pNumParts->data[charToken-1].a[1]; } else { // can't get number part table for some reason. use U.S. defaults switch (charToken) { case tokDecPoint: c = '.'; break; case tokThousands: c = ','; break; } } return c; } /************************************************************************ * UpdateCalcStats - update calculated status ************************************************************************/ void UpdateCalcStats(void) { long oldTotal; // Take the ones we score as junk... (*gStatData)->numStats[kStatJunkTotal] = (*gStatData)->numStats[kStatScoredJunk]; // Subtract the false positives (they weren't really junk) NumericStatOperation(kStatJunkTotal,kStatFalsePositives,opMinus); // Add the false negatives (they were junk, but the scoring missed them) NumericStatOperation(kStatJunkTotal,kStatFalseNegatives,opPlus); // Add the junk that was on the white list and so didn't get recognized as junk NumericStatOperation(kStatJunkTotal,kStatFalseWhiteList,opPlus); // the result is all the junk we got! // Take the ones we didn't score as junk... (*gStatData)->numStats[kStatTotal] = (*gStatData)->numStats[kStatScoredNotJunk]; // Add the whitelist NumericStatOperation(kStatTotal,kStatWhiteList,opPlus); // Add the scored junk NumericStatOperation(kStatTotal,kStatScoredJunk,opPlus); // The result is all eligible mail // Compute percentages (*gStatData)->numStats[kStatJunkPercent] = (*gStatData)->numStats[kStatJunkTotal]; oldTotal = (*gStatData)->numStats[kStatJunkPercent].total; NumericStatOperation(kStatJunkPercent,kStatTotal,opPercent); (*gStatData)->numStats[kStatJunkPercent].total = oldTotal; } /************************************************************************ * NumericStatOperation - add or subtract one numeric stat from another ************************************************************************/ void NumericStatOperation(StatType destination,StatType increment,OpType op) { PeriodicStatOperation(&(*gStatData)->numStats[destination].current,&(*gStatData)->numStats[increment].current,op); PeriodicStatOperation(&(*gStatData)->numStats[destination].last,&(*gStatData)->numStats[increment].last,op); PeriodicStatOperation(&(*gStatData)->numStats[destination].average,&(*gStatData)->numStats[increment].average,op); switch (op) { case opPlus: (*gStatData)->numStats[destination].total += (*gStatData)->numStats[increment].total; break; case opMinus: (*gStatData)->numStats[destination].total -= (*gStatData)->numStats[increment].total; break; case opPercent: (*gStatData)->numStats[destination].total = (*gStatData)->numStats[increment].total ? (100*(*gStatData)->numStats[destination].total)/(*gStatData)->numStats[increment].total : 0; break; } } /************************************************************************ * PeriodicStatOperation - add or subtract one periodic stat from another ************************************************************************/ void PeriodicStatOperation(PeriodicStats *destP, PeriodicStats *incP, OpType op) { LongArrayOperation(destP->day,incP->day,sizeof(incP->day)/sizeof(incP->day[0]),op); LongArrayOperation(destP->week,incP->week,sizeof(incP->week)/sizeof(incP->week[0]),op); LongArrayOperation(destP->month,incP->month,sizeof(incP->month)/sizeof(incP->month[0]),op); LongArrayOperation(destP->year,incP->year,sizeof(incP->year)/sizeof(incP->year[0]),op); } /************************************************************************ * LongArrayOperation - add or subtract one array of longs from another ************************************************************************/ void LongArrayOperation(long *array,long *sub,short n,OpType op) { switch (op) { case opPlus: while (n-->0) *array++ += *sub++; break; case opMinus: while (n-->0) *array++ -= *sub++; break; case opPercent: for (;n-->0;array++,sub++) *array = (100 * (*array))/(*sub); break; } }