#! /bin/bash
#	$Id: xferfaxstats.sh.in 851 2008-06-18 05:12:47Z faxguy $
#
# HylaFAX Facsimile Software
#
# Copyright (c) 1990-1996 Sam Leffler
# Copyright (c) 1991-1996 Silicon Graphics, Inc.
# HylaFAX is a trademark of Silicon Graphics
# 
# Permission to use, copy, modify, distribute, and sell this software and 
# its documentation for any purpose is hereby granted without fee, provided
# that (i) the above copyright notices and this permission notice appear in
# all copies of the software and related documentation, and (ii) the names of
# Sam Leffler and Silicon Graphics may not be used in any advertising or
# publicity relating to the software without the specific, prior written
# permission of Sam Leffler and Silicon Graphics.
# 
# THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, 
# EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY 
# WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.  
# 
# IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR
# ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND,
# OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
# WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF 
# LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE 
# OF THIS SOFTWARE.
#

#
# Print Statistics about Transmitted Facsimile.
#
SPOOL=/var/spool/hylafax
DIR_LIBDATA=/etc/hylafax

SetupPrivateTmp()
{
    if [ -d "$HYLAFAX_TMPDIR" ]; then
        # Private temp area already created.
        return
    fi

    # Would have liked to use -t, but older mktemp don't support it.
    if [ -z "$TMPDIR" ] || [ ! -w "$TMPDIR" ]; then
        TMPDIR="/tmp"
    fi
    HYLAFAX_TMPDIR=`mktemp -d $TMPDIR/hylafaxtmp-XXXXXXXX 2>/dev/null` || {
        HYLAFAX_TMPDIR="$TMPDIR/hylafaxtmp-$RANDOM-$RANDOM-$RANDOM-$$"
        mkdir -m 0700 "$HYLAFAX_TMPDIR"
    }
    if [ $? != 0 ]
    then
	echo "Couldn't setup private temp area - exiting!" 1>&2
	exit 1
    fi
    # We want any called programs to use our tmp dir.
    TMPDIR=$HYLAFAX_TMPDIR
    export TMPDIR

    trap cleanupExit 0
    trap "hfExit 1" 1 2 15
}

CleanupPrivateTmp ()
{
    if [ -d "$HYLAFAX_TMPDIR" ]
    then
	rm -Rf "$HYLAFAX_TMPDIR"
    fi
}

cleanupExit ()
{
    trap - 0 1 2 15
    CleanupPrivateTmp
}

hfExit ()
{
    cleanupExit
    exit $1
}

test -f $DIR_LIBDATA/setup.cache || {
    cat<<EOF

FATAL ERROR: $DIR_LIBDATA/setup.cache is missing!

The file $DIR_LIBDATA/setup.cache is not present.  This
probably means the machine has not been setup using the faxsetup(8C)
command.  Read the documentation on setting up HylaFAX before you
startup a server system.

EOF
    hfExit 1
}
. $DIR_LIBDATA/setup.cache

FILES=
SORTKEY=-sender				# default is to sort by sender
MAPNAMES=no				# default is to not map email addresses (NIS)
TODAY="`date +'%D %H:%M'`"
AGE="360"				# report statistics for last year
SINCEDT=
ENDDT=

while [ x"$1" != x"" ] ; do
    case $1 in
    -send*|-csi|-dest*|-speed|-rate|-format|-job*|-dev*)
	    SORTKEY=$1;;
    -nomap) MAPNAMES=no;;
    -map)   MAPNAMES=yes;;
    -age)   shift; AGE=$1;;
    -since) shift; SINCEDT=$1;;
    -end) shift; ENDDT=$1;;
    -*)	    echo "Usage: $0 [-sortkey] [-age days] [-since date] [-end date] [-nomap] [files]"; hfExit 1;;
    *)	    FILES="$FILES $1";;
    esac
    shift
done
if [ -z "$FILES" ]; then
    FILES=$SPOOL/etc/xferfaxlog
fi

#
# Construct awk rules to collect information according
# to the desired sort key.  There are multiple rules for
# each; to deal with the different formats that have
# existed over time.
#
case $SORTKEY in
-send*)
    AWKRULES='
	$2 == "SEND" && NF == 9  { acct($3, $4, $7, $8, $5, $6, $9); }
	$2 == "SEND" && NF == 11 { acct($4, $5, $9, $10, $7, $8, $11); }
	$2 == "SEND" && NF == 12 { acct($5, $6, $10, $11, $8, $9, $12); }
	$2 == "SEND" && NF == 13 { acct($7, $8, $11, $12, getBR($10), getDF($10), $13); }
	$2 == "SEND" && NF == 14 { acct($7, $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 16 { acct($7, $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 17 { acct($7 " (" $17 ")", $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 18 { acct($7 " (" $18 ")", $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 19 { acct($7 " (" $18 ")", $8, $11, $13, getBR($10), getDFfromDCS($19), $14); }
	$2 == "SEND" && NF == 20 { acct($7 " (" $18 ")", $8, $11, $13, getBR($10), getDFfromDCS($19), $14); }
	'
    KEYTITLE="Sender"
    ;;
-csi)
    AWKRULES='
	$2 == "SEND" && NF == 9  { acct($4, $4, $7, $8, $5, $6, $9); }
	$2 == "SEND" && NF == 11 { acct($6, $5, $9, $10, $7, $8, $11); }
	$2 == "SEND" && NF == 12 { acct($7, $6, $10, $11, $8, $9, $12); }
	$2 == "SEND" && NF == 13 { acct($9, $8, $11, $12, getBR($10), getDF($10), $13); }
	$2 == "SEND" && NF == 14 { acct($9, $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 16 { acct($9, $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 17 { acct($9, $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 18 { acct($9, $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 19 { acct($9, $8, $11, $13, getBR($10), getDFfromDCS($19), $14); }
	$2 == "SEND" && NF == 20 { acct($9, $8, $11, $13, getBR($10), getDFfromDCS($19), $14); }
	'
    KEYTITLE="CSI"
    MAPNAMES=no
    ;;
-dest*)
    AWKRULES='
	$2 == "SEND" && NF == 9  { acct($4, $4, $7, $8, $5, $6, $9); }
	$2 == "SEND" && NF == 11 { acct($5, $5, $9, $10, $7, $8, $11); }
	$2 == "SEND" && NF == 12 { acct($6, $6, $10, $11, $8, $9, $12); }
	$2 == "SEND" && NF == 13 { acct($8, $8, $11, $12, getBR($10), getDF($10), $13); }
	$2 == "SEND" && NF == 14 { acct($8, $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 16 { acct($8, $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 17 { acct($8, $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 18 { acct($8, $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 19 { acct($8, $8, $11, $13, getBR($10), getDFfromDCS($19), $14); }
	$2 == "SEND" && NF == 20 { acct($8, $8, $11, $13, getBR($10), getDFfromDCS($19), $14); }
	'
    KEYTITLE="Destination"
    MAPNAMES=no
    ;;
-speed|-rate)
    AWKRULES='
	$2 == "SEND" && NF == 9  { acct($5, $4, $7, $8, $5, $6, $9); }
	$2 == "SEND" && NF == 11 { acct($7, $5, $9, $10, $7, $8, $11); }
	$2 == "SEND" && NF == 12 { acct($8, $6, $10, $11, $8, $9, $12); }
	$2 == "SEND" && NF == 13 { acct(getBR($10), $8, $11, $12, getBR($10), getDF($10), $13); }
	$2 == "SEND" && NF == 14 { acct(getBR($10), $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 16 { acct(getBR($10), $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 17 { acct(getBR($10), $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 18 { acct(getBR($10), $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 19 { acct(getBR($10), $8, $11, $13, getBR($10), getDFfromDCS($19), $14); }
	$2 == "SEND" && NF == 20 { acct(getBR($10), $8, $11, $13, getBR($10), getDFfromDCS($19), $14); }
	'
    KEYTITLE="Speed"
    MAPNAMES=no
    ;;
-format)
    AWKRULES='
	$2 == "SEND" && NF == 9  { acct($6, $4, $7, $8, $5, $6, $9); }
	$2 == "SEND" && NF == 11 { acct($8, $5, $9, $10, $7, $8, $11); }
	$2 == "SEND" && NF == 12 { acct($9, $6, $10, $11, $8, $9, $12); }
	$2 == "SEND" && NF == 13 { acct(getDF($10), $8, $11, $12, getBR($10), getDF($10), $13); }
	$2 == "SEND" && NF == 14 { acct(getDF($10), $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 16 { acct(getDF($10), $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 17 { acct(getDF($10), $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 18 { acct(getDF($10), $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 19 { acct(getDF($10), $8, $11, $13, getBR($10), getDFfromDCS($19), $14); }
	$2 == "SEND" && NF == 20 { acct(getDF($10), $8, $11, $13, getBR($10), getDFfromDCS($19), $14); }
	'
    KEYTITLE="Format"
    MAPNAMES=no
    ;;
-job*)
    AWKRULES='
	$2 == "SEND" && NF == 13 { acct($6, $8, $11, $12, getBR($10), getDF($10), $13); }
	$2 == "SEND" && NF == 14 { acct($6, $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 16 { acct($6, $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 17 { acct($6, $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 18 { acct($6, $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 19 { acct($6, $8, $11, $13, getBR($10), getDFfromDCS($19), $14); }
	$2 == "SEND" && NF == 20 { acct($6, $8, $11, $13, getBR($10), getDFfromDCS($19), $14); }
	'
    KEYTITLE="JobTag"
    MAPNAMES=no
    ;;
-dev*)
    AWKRULES='
	$2 == "SEND" && NF == 13 { acct($4, $8, $11, $12, getBR($10), getDF($10), $13); }
	$2 == "SEND" && NF == 14 { acct($4, $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 16 { acct($4, $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 17 { acct($4, $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 18 { acct($4, $8, $11, $13, getBR($10), getDF($10), $14); }
	$2 == "SEND" && NF == 19 { acct($4, $8, $11, $13, getBR($10), getDFfromDCS($19), $14); }
	$2 == "SEND" && NF == 20 { acct($4, $8, $11, $13, getBR($10), getDFfromDCS($19), $14); }
	'
    KEYTITLE="Device"
    MAPNAMES=no
    ;;
esac

#
# Generate an awk program to process the statistics file.
#
SetupPrivateTmp
tmpAwk=$TMPDIR/xferfax$$

($CAT<<'EOF'
#
# Setup date conversion data structures.
#
function setupDateTimeStuff()
{
    daysInMonth[ 0] = 31; daysInMonth[ 1] = 28; daysInMonth[ 2] = 31;
    daysInMonth[ 3] = 30; daysInMonth[ 4] = 31; daysInMonth[ 5] = 30;
    daysInMonth[ 6] = 31; daysInMonth[ 7] = 31; daysInMonth[ 8] = 30;
    daysInMonth[ 9] = 31; daysInMonth[10] = 30; daysInMonth[11] = 31;

    FULLDAY = 24 * 60 * 60;
}

#
# Convert hh:mm:ss to seconds.
#
function cvtTime(s)
{
    t = i = 0;
    for (n = split(s, a, ":"); i++ < n; )
	t = t*60 + a[i];
    return t;
}

#
# Convert MM/DD/YY hh:mm to seconds.
# NB: this does not deal with leap years.
#
function cvtDateTime(s)
{
    yday = substr(s,7,2)
    if (yday < 70)
        yday += 30;
    else
        yday -= 70;
    yday = yday*365 + substr(s,4,2) - 1;
    mon = substr(s,1,2) - 1;
    for (i = 0; i < mon; i++)
	yday += daysInMonth[i];
    return yday*FULLDAY + cvtTime(substr(s,10) ":00");
}

function setupDigits()
{
  digits[0] = "0"; digits[1] = "1"; digits[2] = "2";
  digits[3] = "3"; digits[4] = "4"; digits[5] = "5";
  digits[6] = "6"; digits[7] = "7"; digits[8] = "8";
  digits[9] = "9";
}

#
# Format seconds as hh:mm:ss.
#
function fmtTime(t)
{
    v = int(t/3600);
    result = "";
    if (v > 0) {
	t -= v*3600;
        result = v ":"
    }
    v = int(t/60);
    if (v >= 10 || result != "")
	result = result digits[int(v/10)];
    result = result digits[int(v%10)];
    t -= v*60;
    return (result ":" digits[int(t/10)] digits[int(t%10)]);
}

#
# Setup map for histogram calculations
# and indexed table for decoding params.
#
function setupMaps(s, map, names)
{
    n = split(s, a, ":");
    for (i = 1; i <= n; i++) {
	map[a[i]] = i;
	names[i-1] = a[i];
    }
}

#
# Add pages to a histogram.
#
function addToMap(key, ix, pages, map)
{
    if (key == "") {
	for (i in map)
	    key = key ":";
    }
    n = split(key, a, ":");
    a[map[ix]] += pages;
    t = a[1];
    for (i = 2; i <= n; i++)
      t = t ":" a[i];
    return t;
}

#
# Merge two histogram maps.
#
function mergeMap(map2, map1)
{
    if (map2 == "")
	return map1;
    else if (map1 == "")
	return map2;
    # map1 & map2 are populated
    n1 = split(map1, a1, ":");
    n2 = split(map2, a2, ":");
    for (i = 1; i <= n1; i++)
	a2[i] += a1[i];
    t = a2[1];
    for (i = 2; i <= n; i++)
      t = t ":" a2[i];
    return t;
}

#
# Return the name of the item with the
# largest number of accumulated pages.
#
function bestInMap(totals, map)
{
   n = split(totals, a, ":");
   imax = 1; max = -1;
   for (j = 1; j <= n; j++)
       if (a[j] > max) {
	   max = a[j];
	   imax = j;
       }
   split(map, a, ":");
   return a[imax];
}

#
# Sort array a[l..r]
#
function qsort(a, l, r) {
    i = l;
    k = r+1;
    item = a[l];
    for (;;) {
	while (i < r) {
            i++;
	    if (a[i] >= item)
		break;
        }
	while (k > l) {
            k--;
	    if (a[k] <= item)
		break;
        }
        if (i >= k)
	    break;
	t = a[i]; a[i] = a[k]; a[k] = t;
    }
    t = a[l]; a[l] = a[k]; a[k] = t;
    if (k != 0 && l < k-1)
	qsort(a, l, k-1);
    if (k+1 < r)
	qsort(a, k+1, r);
}

function cleanup(s)
# remove special characters from the string s and return the results
# the first arg of gsub is a regex to be substituted (removed in this
# case) from the string s
#  remove double quotes, leading spaces at the beginning
#  of the string any spaces followed at the end of the string
{
    gsub("\"", "", s);
    gsub("^ +", "", s);
    gsub(" +$", "", s);
    return s;
}

function setupToLower()
{
    upperRE = "[ABCDEFGHIJKLMNOPQRSTUVWXYZ]";
    upper["A"] = "a"; upper["B"] = "b"; upper["C"] = "c";
    upper["D"] = "d"; upper["E"] = "e"; upper["F"] = "f";
    upper["G"] = "g"; upper["H"] = "h"; upper["I"] = "i";
    upper["J"] = "j"; upper["K"] = "k"; upper["L"] = "l";
    upper["M"] = "m"; upper["N"] = "n"; upper["O"] = "o";
    upper["P"] = "p"; upper["Q"] = "q"; upper["R"] = "r";
    upper["S"] = "s"; upper["T"] = "t"; upper["U"] = "u";
    upper["V"] = "v"; upper["W"] = "w"; upper["X"] = "x";
    upper["Y"] = "y"; upper["Z"] = "z";
}

function toLower(s)
{
    if (match(s, upperRE) != 0) {
	do {
	    c = substr(s, RSTART, 1);
	    gsub(c, upper[c], s);
	} while (match(s, upperRE));
    }
    return s;
}

function getBR(params)
{
    if ((int(int(params) / 2097152) % 2) == 1) {
        return brNames[int(int(params) / 8) % 16];
    } else {
        return brNames[int(int(params) / 2) % 8];
    }
}

function getDF(params)
{
    if ((int(int(params) / 2097152) % 2) == 1) {
        return dfNames[int(int(params) / 16384) % 4];
    } else {
        return dfNames[int(int(params) / 512) % 4];
    }
}

function hex(n) {
    # GNU Awk 3.1.3 has a type conversion error from hex strings,
    # so we have to do it the hard way
    val = 0;
    for (position = 1; position <= length(n); position++) {
	val = val * 16;
	char = tolower(substr(n, position, 1));
	if (char == "a") val = val + 10;
	else if (char == "b") val = val + 11;
	else if (char == "c") val = val + 12;
	else if (char == "d") val = val + 13;
	else if (char == "e") val = val + 14;
	else if (char == "f") val = val + 15;
	else val = val + int(char);
    }
    return val;
}

function isBitSet(bit, dcs) {
    byte = int((bit - 1) / 8);  
    pos = 7 - ((bit - 1) % 8);
    dcsbyte = hex(substr(dcs, (byte*3+1), 2));
    return int(int(dcsbyte / (2 ^ pos)) % 2);
}
        
function getDFfromDCS(dcs)
{
    if (substr(dcs, 1, 1) == "\"")
	dcs = substr(dcs, 2);
    if (substr(dcs, length(dcs), 1) == "\"")
	dcs = substr(dcs, 1, length(dcs)-1);
    if (isBitSet(68, dcs)) {
	if (isBitSet(69, dcs))  
	    return dfNames[6]; 
	return dfNames[5];
    }
    if (isBitSet(78, dcs) || isBitSet(79, dcs))
	return dfNames[4];
    if (isBitSet(31, dcs))  
	return dfNames[3];
    if (isBitSet(26, dcs))
	return dfNames[2];
    if (isBitSet(16, dcs))
	return dfNames[1];
    return dfNames[0];
}

#
# Accumulate a statistics record.
#

function acct(key, dest, pages, time, br, df, status)
{
    status = cleanup(status);
    if (length(status) > 11) {
	msg = toLower(substr(status, 1, 11));
	if (callFailed[msg])
	    return;
    }
    if (cvtDateTime($1) < KEEPSINCE || cvtDateTime($1) > KEEPEND)
	return;
    key = cleanup(key);
EOF
test "$MAPNAMES" = "yes" && $CAT<<'EOF'
    #
    # Try to merge unqualified names w/ fully qualified names
    # by stripping the host part of the domain address and
    # mapping unqualified names to a stripped qualified name.
    #
    n = split(key, parts, "@");
    if (n == 2) {			# user@addr
	user = parts[1];
	if (user != "root" && user != "guest") {
	    addr = parts[2];
	    #
	    # Strip hostname from multi-part domain name.
	    #
EOF
test "$MAPNAMES" = "yes" && echo 'p = split("'`hostname`'", hostname, ".");'
test "$MAPNAMES" = "yes" && $CAT<<'EOF'
	    n = split(addr, domains, ".");
	    if (p > 1 && n > p) {		# e.g. flake.asd.sgi.com
		l = length(domains[1])+1;
		addr = substr(addr, l+1, length(addr)-l);
		key = user "@" addr;
	    }
	    if (addrs[user] == "") {	# record mapped name
		addrs[user] = addr;
	    } else if (addrs[user] != addr) {
		if (!warned[user "@" addr]) {
		    warned[user "@" addr] = 1;
		    printf "Warning, address clash, \"%s\" and \"%s\".\n", \
		       user "@" addrs[user], user "@" addr
		}
	    }
	}
    } else if (n != 1) {
	printf "Warning, weird user address/name \"%s\".\n", key
    }
EOF
$CAT<<'EOF'
    dest = cleanup(dest);

    sendpages[key] += pages;
	sendcalls[key] += 1;
    time = cleanup(time);
    if (pages == 0 && time > 60)
	time = 0;
    sendtime[key] += cvtTime(time);
    if (status != "")
	senderrs[key]++;
    br = cleanup(br);
    sendrate[key] = addToMap(sendrate[key], br, pages, rateMap);
    df = cleanup(df);
    senddata[key] = addToMap(senddata[key], df, pages, dataMap);
}

#
# Print a rule between the stats and the totals line.
#
function printRule(n, s)
{
    r = "";
    while (n-- >= 0)
	r = r s;
    printf "%s\n", r;
}

BEGIN		{ FS="\t";
		  rates = "2400:4800:7200:9600:12000:14400:16800:19200:21600:24000:26400:28800:31200:33600";
		  setupMaps(rates, rateMap, brNames);
		  datas = "1-D MH:2-D MR:2-D Uncompressed Mode:2-D MMR:JBIG:JPEG Greyscale:JPEG Full-color";
		  setupMaps(datas, dataMap, dfNames);
		  callFailed["busy signal"] = 1;
		  callFailed["unknown pro"] = 1;
		  callFailed["no carrier "] = 1;
		  callFailed["no local di"] = 1;
		  callFailed["no answer f"] = 1;
		  callFailed["ringback de"] = 1;
		  callFailed["job aborted"] = 1;
		  setupToLower();
		  setupDateTimeStuff();
		  if (SINCEDT == "")
		      KEEPSINCE = cvtDateTime(TODAY) - AGE*FULLDAY;
		  else
		      KEEPSINCE = cvtDateTime(SINCEDT);
		  if (ENDDT == "")
		      KEEPEND = cvtDateTime(TODAY);
		  else
		      KEEPEND = cvtDateTime(ENDDT);
EOF
echo		  "KeyTitle = \"$KEYTITLE\""
$CAT<<'EOF'
		}
END		{ OFS="\t"; setupDigits();
		  maxlen = 15;
		  # merge unqualified and qualified names
		  for (key in sendpages) {
		      if (addrs[key] != "") {
			  fullkey = key "@" addrs[key];
			  sendpages[fullkey] += sendpages[key];
			  sendcalls[fullkey] += sendcalls[key];
			  sendtime[fullkey] += sendtime[key];
			  senderrs[fullkey] += senderrs[key];
			  sendrate[fullkey] = \
			      mergeMap(sendrate[fullkey], sendrate[key]);
			  senddata[fullkey] = \
			      mergeMap(senddata[fullkey], senddata[key]);
		      }
		  }
		  nsorted = 0;
		  for (key in sendpages) {
		      if (addrs[key] != "")	# unqualified name
			  continue;
		      l = length(key);
		      if (l > maxlen)
			maxlen = l;
		      sorted[nsorted++] = key;
		  }
		  qsort(sorted, 0, nsorted-1);
		  fmt = "%-" maxlen "." maxlen "s";	# e.g. %-24.24s
                  printf fmt " %6s %6s %9s %6s %5s %7s %7s\n",
		      KeyTitle, "Pages", "Calls", "Time", "Pg/min",
		      "Errs", "TypRate", "TypData";
		  tpages = 0;
		  ttime = 0;
		  terrs = 0;
		  for (k = 0; k < nsorted; k++) {
		      i = sorted[k];
		      t = sendtime[i]/60; if (t == 0) t = 1;
		      n = sendpages[i]; if (n == 0) n = 1;
		      brate = best
		      printf fmt " %6d %6d %9s %6.1f %5d %7d %7.7s\n",
			  i, sendpages[i], sendcalls[i], fmtTime(sendtime[i]),
			  sendpages[i] / t, senderrs[i],
			  bestInMap(sendrate[i], rates),
			  bestInMap(senddata[i], datas);
			tpages += sendpages[i];
			tcalls += sendcalls[i];
			ttime += sendtime[i];
			terrs += senderrs[i];
		  }
		  printRule(maxlen+1+6+1+6+1+9+6+1+5+1+7+1+7, "-");
		  t = ttime/60; if (t == 0) t = 1;
		  printf fmt " %6d %6d %9s %6.1f %5d\n",
		      "Total", tpages, tcalls, fmtTime(ttime), tpages/t, terrs;
		}
EOF
echo "$AWKRULES"
)>$tmpAwk
$AWK -f $tmpAwk -v TODAY="$TODAY" -v AGE="$AGE" -v SINCEDT="$SINCEDT" -v ENDDT="$ENDDT" $FILES

# cleanup
hfExit 0