Mike Gerwitz

Activist for User Freedom

aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMike Gerwitz <gerwitm@lovullo.com>2016-08-24 09:43:05 -0400
committerMike Gerwitz <gerwitm@lovullo.com>2016-08-24 12:38:00 -0400
commitff01f39c1e8c9b9549d884a0db1f9a74799cf37e (patch)
tree35978db88a8d385250b1b47ad05966e19516373d /src/current/tools
parent6c0aa54bd1b7b49d736f0db3a8f48b7aa90b3b65 (diff)
downloadtame-ff01f39c1e8c9b9549d884a0db1f9a74799cf37e.tar.gz
tame-ff01f39c1e8c9b9549d884a0db1f9a74799cf37e.tar.bz2
tame-ff01f39c1e8c9b9549d884a0db1f9a74799cf37e.zip
Liberate current implementation of "Calc DSL"
(Copyright headers will be added in the next commit; these are the original files, unaltered in any way.) The internal project name at LoVullo is simply "Calc DSL". This liberates the entire thing. If anything was missed, I'll be added later. To continue building at LoVullo with this move, symlinks are used for the transition; this is the exact code that is used in production. There is a lot here---over 25,000 lines. Much of it is in disarray from the environment surrounding its development, but it does work well for what it was intended to do. (LoVullo folks: fork point is 65723a0 in calcdsl.git.)
Diffstat (limited to 'src/current/tools')
-rwxr-xr-xsrc/current/tools/csv2xml110
-rwxr-xr-xsrc/current/tools/csvi124
-rwxr-xr-xsrc/current/tools/csvm2csv112
-rwxr-xr-xsrc/current/tools/gen-make96
-rw-r--r--src/current/tools/lib/zipre.php124
-rwxr-xr-xsrc/current/tools/tdat2xml274
-rwxr-xr-xsrc/current/tools/zipre23
7 files changed, 863 insertions, 0 deletions
diff --git a/src/current/tools/csv2xml b/src/current/tools/csv2xml
new file mode 100755
index 0000000..3a5d1b8
--- /dev/null
+++ b/src/current/tools/csv2xml
@@ -0,0 +1,110 @@
+#!/usr/bin/awk -f
+#
+# Compiles the given CSV into a table definition
+
+
+function columngen( header )
+{
+ # output a field constant for each field in the header
+ i = 0
+ while ( field = header[ ++i ] )
+ {
+ printf " <t:table-column name=\"%s\" " \
+ "index=\"%d\" seq=\"%s\" />\n",
+ field,
+ ( i - 1 ),
+ ( seq[ i ] ) ? "true" : "false"
+ }
+}
+
+
+function seqchk( last )
+{
+ # if there's no last row, then do not bother
+ i = 0
+ while ( i++ < NF )
+ {
+ if ( seq[ i ] == "" ) seq[ i ] = 1
+
+ # this field is sequential if it is greater than or equal to the last field
+ # (we don't check for descending [yet]); note that on the first check, last
+ # will be empty and therefore this check will succeed (properly
+ # initializing seq[i] to 1)
+ seq[ i ] = seq[ i ] && ( $(i) >= last[ i ] )
+ }
+}
+
+
+# header
+BEGIN {
+ rootpath = "../../../"
+ file = ARGV[1]
+
+ # grab only the filename (remove all preceding directories and the file ext)
+ name = gensub( /^.*\/|\.[^.]+$/, "", "g", file )
+
+
+ # output package header
+ printf \
+ "<?xml-stylesheet type=\"text/xsl\" href=\"%1$srater/summary.xsl\"?>\n" \
+ "<package\n" \
+ " xmlns=\"http://www.lovullo.com/rater\"\n" \
+ " xmlns:c=\"http://www.lovullo.com/calc\"\n" \
+ " xmlns:t=\"http://www.lovullo.com/rater/apply-template\"\n" \
+ " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" \
+ " xsi:schemaLocation=\"http://www.lovullo.com/rater %1$srater/rater.xsd\"\n\n" \
+ " name=\"suppliers/rates/tables/%2$s\"\n" \
+ " desc=\"%2$s rate table\">\n\n" \
+ " <!--\n" \
+ " WARNING: This file was generated by csv2xml; do not modify!\n" \
+ " -->\n\n" \
+ " <import package=\"/rater/core\" />\n" \
+ " <import package=\"/rater/core/vector/table\" />\n\n", \
+ rootpath, name
+
+ # the first row of the CSV is the header representing the column identifiers
+ getline
+ split( $0, header, /,/ )
+
+ # table constant identifier
+ tconst = toupper( gensub( /-/, "_", "g", name ) ) "_RATE_TABLE"
+
+ # generate the header for the table constant
+ printf " <t:create-table name=\"%s\">\n", name
+
+ printf "%s", " <t:table-rows data=\"\n"
+
+ # delimit fields by commas (the field separator for CSVs); note that this
+ # won't work properly if strings contain commas
+ FS = ","
+}
+
+
+# each row of the CSV
+{
+ # generate value string for each field
+ i = 0
+ while ( i++ < NF )
+ {
+ printf "%s", ( ( i > 1 ) ? "," : "" ) $(i)
+ }
+
+ print ";"
+
+ seqchk( last )
+ split( $0, last )
+}
+
+
+# footer
+END {
+ # end of table-rows node
+ print "\" />"
+
+ # columns can't be generated until after we know which ones represent
+ # sequential data
+ columngen( header )
+
+ print " </t:create-table>"
+ print "</package>"
+}
diff --git a/src/current/tools/csvi b/src/current/tools/csvi
new file mode 100755
index 0000000..08d80e2
--- /dev/null
+++ b/src/current/tools/csvi
@@ -0,0 +1,124 @@
+#!/usr/bin/awk -f
+#
+# Performs interpolation for columns in a CSV and outputs the result
+#
+# Configurable values (use -vname=value from command line):
+# step - use predeterminated step instead of calculating from first two rows
+#
+# #
+
+function storeline()
+{
+ for ( i = 1; i <= hlen; i++ ) {
+ prev[i] = $i
+ }
+}
+
+function clearline()
+{
+ for ( i = 1; i <= hlen; i++ ) {
+ prev[i] = 0
+ }
+}
+
+function getprev()
+{
+ for ( i = 1; i <= hlen; i++ ) {
+ $i = prev[i]
+ }
+}
+
+
+function interpolate()
+{
+ lastval = prev[1]
+
+ curval = $1
+ diff = curval - lastval
+
+ # does this value fall in line with the requested step?
+ if ( diff == step )
+ {
+ storeline()
+
+ # we're good; continue
+ print
+ next
+ }
+
+ # if we do not yet have a value large enough to reach our step, then continue
+ # until we do (do not store this line)
+ n = int( diff / step )
+ if ( n <= 0 ) {
+ next
+ }
+
+ # determine interpolation values
+ for ( i = 2; i <= hlen; i++ ) {
+ ival[i] = ( ( $i - prev[i] ) / n )
+ }
+
+ getprev()
+
+ # let us interpolate values that are divisible by the step
+ do
+ {
+ # increase the last value by our step
+ $1 += step
+
+ # interpolate each column value (notice that we skip the first column, which
+ # was handled directly above)
+ for ( i = 2; i <= hlen; i++ ) {
+ $i += ival[i]
+ }
+
+ # print the new line
+ print
+ } while ( ( diff -= step ) > 0 )
+
+ # anything remaining does not fit into our step and will be ignored; we'll
+ # continue with our next step at the next line
+
+ # consider this to be our last line
+ storeline()
+}
+
+
+BEGIN {
+ # the first row of the CSV is the header representing the column identifiers
+ getline
+ hlen = split( $0, header, /,/ )
+
+ # output the header
+ print $0
+
+ # delimit fields by commas (the field separator for CSVs); note that this
+ # won't work properly if strings contain commas
+ FS = OFS = ","
+
+ clearline()
+ getline
+
+ # if no step was provided, then calculate one based on the first two rows
+ if ( step == 0 ) {
+ # output the first row, which does not need to be interpolated
+ print
+
+ # compute the step
+ vala = $1
+ getline
+ valb = $1
+ step = valb - vala
+
+ # since the second line is used to determine the step, then it must match the
+ # step and therefore is good to output
+ print
+
+ # begin.
+ storeline()
+ }
+}
+
+
+# for each row
+{ interpolate() }
diff --git a/src/current/tools/csvm2csv b/src/current/tools/csvm2csv
new file mode 100755
index 0000000..410d9fa
--- /dev/null
+++ b/src/current/tools/csvm2csv
@@ -0,0 +1,112 @@
+#!/usr/bin/awk -f
+#
+# Compiles a "magic" CSV file into a normal CSV
+#
+# "Magic" CSVs simply exist to make life easier: they permit comments, blank
+# lines, variables, sub-delimiter expansion, and any number of ranges per line.
+# Ranges will be expanded in every combination, making rate tables highly
+# maintainable.
+#
+# Variables are also supported when defined using :var=val. Variables may expand
+# into ranges, 'cause they're awesome. Multiple variables may be delimited by
+# semi-colons, as may multiple values.
+#
+# For example:
+# :foo=1--3
+# $foo;7;9--10:$foo, 5--10
+#
+# Would generate:
+# 1, 5
+# 1, 6
+# ...
+# 5, 10
+# 2, 5
+# ...
+# 9, 5
+# ...
+# 1, 5
+# 1, 6
+# ...
+
+
+function rangeout( i, m, j, me, orig )
+{
+ if ( i > NF )
+ {
+ print
+ return
+ }
+
+ orig = $i
+
+ # check first for delimiters
+ if ( match( $i, /^([^;]+);(.*)$/, m ) )
+ {
+ # give it a shot with the first value
+ $i = m[1]
+ rangeout( i )
+
+ # strip off the first value and process with following value(s)
+ $i = m[2]
+ rangeout( i )
+
+ # we've delegated; we're done
+ $i = orig
+ return
+ }
+
+ # attempt to parse variable (may expand into a range)
+ if ( match( $i, /^\$([a-zA-Z_-]+)$/, m ) )
+ {
+ $i = vars[ m[1] ];
+ }
+
+ # parse range
+ if ( match( $i, /^([0-9]+)--([0-9]+)$/, m ) )
+ {
+ j = m[1]
+ me = m[2]
+ do
+ {
+ $i = j
+ rangeout( i + 1 )
+ } while ( j++ < me )
+ }
+ else
+ {
+ rangeout( i + 1 );
+ }
+
+ # restore to original value
+ $i = orig
+}
+
+
+BEGIN {
+ # we're parsing CSVs
+ FS = " *, *"
+ OFS = ","
+}
+
+
+# skip all lines that begin with `#', which denotes a comment, or are empty
+/^#|^$/ { next; }
+
+# lines that begin with a colon are variable definitions
+/^:/ {
+ match( $0, /^:([a-zA-Z_-]+)=(.*?)$/, m )
+ vars[ m[1] ] = m[2]
+ next
+}
+
+# lines containing ranges (denoted by `--', the en dash, which is a typesetting
+# convetion for ranges), sub-delimiters, or variables must be expanded
+/--|;|\$[a-zA-Z_-]/ { rangeout( 1 ); next; }
+
+# all other lines are normal; simply output them verbatim
+{
+ # this assignment will ensure that awk processes the output, ensuring that
+ # extra spaces between commas are stripped
+ $1=$1
+ print
+}
diff --git a/src/current/tools/gen-make b/src/current/tools/gen-make
new file mode 100755
index 0000000..3f59bfe
--- /dev/null
+++ b/src/current/tools/gen-make
@@ -0,0 +1,96 @@
+#!/bin/bash
+#
+# Generates Makefile containing dependencies for each package
+# #
+
+# windows machines may not have the tools to resolve a path, so let's do so
+# ourselves (TODO: there's better (and more performant) ways of doing this than
+# repeated string replacements); TODO: ./
+resolv-path()
+{
+ # no need to do anything if the string does not contain a parent dir reference
+ # (we use this convoluted string replacement check for woe32/64 to prevent
+ # additional spawns (e.g. sed) that would slow us down and because =~ is not
+ # properly supported in msys
+ [[ "$1" != "${1/..\//}"a ]] || {
+ echo "$1"
+ return
+ }
+
+ local path=
+ while read name; do
+ if [ "$name" == .. ]; then
+ [ -n "$path" ] || {
+ echo "warning: will not resolve $1" >&2
+ return 5
+ }
+
+ path="${path%/*}"
+ continue
+ fi
+
+ path="$path/$name"
+ done <<< "${1//\//$'\n'}"
+
+ # echo path without leading /
+ echo -n "${path:1}"
+}
+
+
+# rule for building
+[ -z "$GEN_MAKE" ] && {
+ echo "%.xmlo:: %.tmp"
+ echo -e "\t@rm -f \$@ \$<"
+ [ -n "$xmlo_cmd" ] \
+ && echo -e "\t$xmlo_cmd" \
+ || echo -e "\ttouch \$@"
+
+ echo "%.xmlo:: %.xml | prexmlo"
+ [ -n "$xmlo_cmd" ] \
+ && echo -e "\t$xmlo_cmd" \
+ || echo -e "\ttouch \$@"
+
+ export GEN_MAKE="$( pwd )/$0"
+ exec "$GEN_MAKE" "$@"
+}
+
+until [ $# -eq 0 ]; do (
+ path="${1%%/}"
+ echo "[gen-make] scanning $path" >&2
+
+ cd "$( basename $path )/" || exit $?
+
+ deps=$( find -maxdepth 1 -iname '*.dep' )
+ for dpath in $deps; do
+ # equivalent to basename command; use this since spawning processes on
+ # windoze is slow as shit (originally we did find -exec bashename)
+ d="${dpath##*/}"
+
+ echo "[gen-make] found $path/$d" >&2
+ echo -n "$path/${d%.*}.xmlo:"
+
+ # output deps
+ while read dep; do
+ # if the first character is a slash, then it's relative to the project
+ # root---the resolution has already been done for us!
+ if [ "${dep:0:1}" == '/' ]; then
+ echo -n " ${dep:1}.xmlo"
+ continue
+ fi
+
+ echo -n ' '
+ resolv-path "$path/$dep.xmlo"
+ done < "$d"
+
+ echo
+ done
+
+ # recurse on every subdirectory
+ for p in */; do
+ [ "$p" == ./ -o "$p" == ../ ] && continue
+ [ ! -d "$p" ] || "$GEN_MAKE" "$path/$p" || {
+ echo "fatal: failed to recurse on $( pwd )/$path/$p" >&2
+ exit 1
+ }
+ done
+); shift; done
diff --git a/src/current/tools/lib/zipre.php b/src/current/tools/lib/zipre.php
new file mode 100644
index 0000000..b4f22e1
--- /dev/null
+++ b/src/current/tools/lib/zipre.php
@@ -0,0 +1,124 @@
+<?php
+
+
+function gen_re_quick( $data )
+{
+ $re = ( '^' . gen_re( $data, 0 ) );
+
+ // attempt to simplify the regex (we're not going to put a lot of effort into
+ // this)
+ return re_simplify( $re );
+}
+
+
+function gen_re( $data, $offset )
+{
+ // if we've reached the end of the zip length, or if there's no more zips to
+ // look at, then stop
+ if ( ( count( $data ) === 0 )
+ || ( $offset === 5 )
+ )
+ {
+ return '';
+ }
+
+ $out = '(';
+
+ // loop through each digit at the current offset
+ $last = '';
+ foreach ( $data as $zip )
+ {
+ if ( !( isset( $zip[ $offset ] ) ) )
+ {
+ continue;
+ }
+
+ $digit = $zip[ $offset ];
+
+ // if we've already seen this digit in the current position, then
+ // continue
+ if ( $digit === $last )
+ {
+ continue;
+ }
+
+ // we're going to recurse now, delimiting allowable digits with pipes
+ // (for 'OR'); we'll recurse on a sublist that matches the zip up to
+ // (and including) the current digit (to do this, note that we only need
+ // to check the current digit, since our current list is already a
+ // sublist of the parent list up to the current point)
+ $prefix = substr( $zip, 0, $offset + 1 );
+
+ $out .= ( $last === '' ) ? '' : '|';
+ $out .= $digit . gen_re(
+ filter_data( $data, $digit, $offset ),
+ ( $offset + 1 )
+ );
+
+ $last = $digit;
+ }
+
+ return $out . ')';
+}
+
+function filter_data( $data, $chr, $offset )
+{
+ $ret = array();
+
+ foreach ( $data as $val )
+ {
+ if ( $val[ $offset] === $chr )
+ {
+ $ret[] = $val;
+ }
+ }
+
+ return $ret;
+}
+
+function re_simplify( $re )
+{
+ // the only simplification we currently do is joining sequential digit ORs
+ // into a character range (e.g. (1|2|3|4) becomes [1-4])
+ return preg_replace_callback( '/\([0-9](\|[0-9])*\)/', function( $results )
+ {
+ $match = $results[ 0 ];
+ $digits = explode( '|', str_replace( array( '(', ')' ), '', $match ) );
+
+ // are the digits sequential (we will only perform this optimization if
+ // there's more than 3 digits, since otherwise the replacement would
+ // result in a string of equal or greater length)?
+ if ( ( count( $digits ) > 3 ) && is_seq( $digits ) )
+ {
+ return sprintf( '[%d-%d]',
+ $digits[ 0 ],
+ $digits[ count( $digits ) - 1 ]
+ );
+ }
+ elseif ( count( $digits ) === 1 )
+ {
+ // if there's only one digit, then that's all we need to return
+ return $digits[ 0 ];
+ }
+
+ return '[' . implode( '', $digits ) . ']';
+ }, $re );
+}
+
+function is_seq( $digits, $last = '' )
+{
+ // stop recursing once we're out of digits
+ if ( count( $digits ) === 0 )
+ {
+ return true;
+ }
+
+ // grab the current digit and remove it from the list (this has the effect
+ // of both cons and cdr)
+ $digit = (int)( array_shift( $digits ) );
+
+ // consider this a sequence if this digit is one more than the last (or if
+ // there is no last digit) and if the following digit is sequential
+ return ( ( $last === '' ) || ( $digit === ( $last + 1) ) )
+ && is_seq( $digits, $digit );
+}
diff --git a/src/current/tools/tdat2xml b/src/current/tools/tdat2xml
new file mode 100755
index 0000000..db4e9b1
--- /dev/null
+++ b/src/current/tools/tdat2xml
@@ -0,0 +1,274 @@
+#!/usr/bin/env php
+<?xml-stylesheet type="text/xsl" href="../../rater/summary.xsl"?>
+<lv:package
+ xmlns:lv="http://www.lovullo.com/rater"
+ xmlns:c="http://www.lovullo.com/calc"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.lovullo.com/rater ../../rater/rater.xsd"
+
+<?php
+
+include 'lib/zipre.php';
+
+function parse_tdesc( $line )
+{
+ if ( !( preg_match( '/^([0-9A-Z]+) (.+)$/', $line, $match ) ) )
+ {
+ throw new Exception( 'Invalid territory descriptor' );
+ }
+
+ return array( $match[ 1 ], $match[ 2 ] );
+}
+
+function gen_yields( $id, $name )
+{
+ return sprintf( 'is%sTerr%s', ucfirst( $name ), $id );
+}
+
+function gen_classification( $id, $name, $desc, $prev_yields, $queue, $or )
+{
+ $yields = gen_yields( $id, $name );
+
+ $prev_value = '';
+ foreach ( $prev_yields as $prev )
+ {
+ if ( !$prev )
+ {
+ continue;
+ }
+
+ $prev_value .= ' <lv:match on="' . $prev . '" value="FALSE" />' . "\n";
+ }
+
+ return sprintf(
+ '<lv:classify as="%s-terr%s" desc="%s" yields="%s">' .
+ "\n%s" .
+ " %s\n" .
+ "\n</lv:classify>\n",
+ $name,
+ gen_identifier( $id ),
+ $desc,
+ $yields,
+ $prev_value,
+ gen_any_block( $queue, $or )
+ );
+}
+
+
+function gen_any_block( $queue, $or )
+{
+ $any = gen_zip_re( $queue ) .
+ gen_on_class( $or );
+
+ return ( $any )
+ ? '<lv:any>' . $any . '</lv:any>'
+ : '';
+}
+
+
+function gen_zip_re( $data )
+{
+ if ( count( $data ) === 0 )
+ {
+ return '';
+ }
+
+ return sprintf(
+ '<lv:match on="zip" pattern="%s" />',
+ gen_re_quick( $data )
+ );
+}
+
+function gen_on_class( $data )
+{
+ if ( count( $data ) === 0 )
+ {
+ return '';
+ }
+
+ $cur = array_shift( $data );
+
+ return sprintf(
+ '<lv:match on="%s" value="TRUE" />%s',
+ $cur,
+ gen_on_class( $data )
+ );
+}
+
+function gen_identifier( $id )
+{
+ return is_numeric( $id )
+ ? $id
+ : '-' . strtolower( $id );
+}
+
+function gen_identifier_value( $id )
+{
+ // for non-numeric identifiers, return ascii value
+ // of character to represent our value
+ return is_numeric( $id )
+ ? $id
+ : ord( $id );
+}
+
+$file = $argv[ 1 ];
+$fdat = explode( '.', basename( $file ) );
+$name = $fdat[ 0 ];
+
+$cur = '';
+$queue = array();
+$or = array();
+
+$fh = fopen( $file, 'r' );
+
+echo 'name="rates/territories/', $name, '" ', "\n",
+ 'desc="', ucfirst( $name ), ' territory classifications">' . "\n\n";
+
+echo "<!--\n",
+ " WARNING: This file was generated by {$argv[0]}; do not modify!\n",
+ "-->\n\n";
+
+$ids = array();
+$params = array();
+$imports = array();
+$prev_yields = '';
+$prev_yields_all = array();
+$classes = '';
+
+$param_type = 'terrType' . ucfirst( $name );
+
+while ( true )
+{
+ // read the line within the loop so that we do not terminate until after we
+ // treat eof as an empty line
+ $line = str_replace( array( "\n", "\r" ), '', fgets( $fh ) );
+
+ if ( !$cur )
+ {
+ if ( substr( $line, 0, 12 ) === '@import-pkg ' )
+ {
+ $imports[] = substr( $line, 12 );
+ continue;
+ }
+
+ // we expect this line to be a territory descriptor
+ try
+ {
+ list ( $id, $desc ) = parse_tdesc( $line );
+ }
+ catch ( Exception $e )
+ {
+ fwrite( STDERR, 'Invalid territory descriptor: ' . $line );
+ exit( 1 );
+ }
+
+ $ids[] = $id;
+ $cur = $id;
+ }
+ elseif ( ( $line === '' ) || feof( $fh ) )
+ {
+ // generate param for typedef
+ $params[ $id ] = $desc;
+
+ // if there's nothing in the queue, then treat this as an 'ROS' (this
+ // should appear as the *last* territory, or it will not function as
+ // expected)
+ if ( count( $queue ) === 0 )
+ {
+ $prev = $prev_yields_all;
+ }
+ else
+ {
+ $prev = array( $prev_yields );
+ }
+
+ // generate the classification
+ $classes .= gen_classification( $id, $name, $desc, $prev, $queue, $or );
+
+ // this accomplishes two things: (1) avoids regexes if there's a
+ // previous match and (2) ensures that we cannot possibly match multiple
+ // territories
+ $prev_yields = gen_yields( $id, $name );
+ $prev_yields_all[] = $prev_yields;
+
+ $cur = '';
+ $queue = array();
+ $or = array();
+
+ if ( feof( $fh ) )
+ {
+ break;
+ }
+ }
+ elseif ( $line[0] === '=' )
+ {
+ // =foo means match on classification @yields "foo"
+ $or[] = substr( $line, 1 );
+ }
+ else
+ {
+ $queue[] = $line;
+ }
+}
+
+$param_name = 'territory_' . $name;
+?>
+
+<?php /* XXX: This is hard-coded! */ ?>
+<lv:import package="/rater/core/tdat" />
+
+<?php foreach ( $imports as $pkg ) { ?>
+ <lv:import package="<?php echo $pkg; ?>" />
+<?php } ?>
+
+<lv:extern name="zip" type="param" dtype="integer" dim="1"
+ missing="this territory package requires an available `zip' parameter; please
+ import a package that provides it" />
+
+<lv:param name="<?php echo $param_name; ?>" type="<?php echo $param_type; ?>" default="0" set="vector" desc="Territory Override" />
+
+<lv:typedef name="<?php echo $param_type; ?>" desc="<?php echo ucfirst( $name ); ?> Territories">
+ <lv:enum type="integer">
+ <?php $item_prefix = 'TERR_' . strtoupper( $name ) . '_'; ?>
+ <lv:item name="<?php echo $item_prefix; ?>_NONE" value="0" desc="No Override" />
+ <?php foreach ( $params as $id => $desc ) { ?>
+ <?php $item_name = $item_prefix . $id; ?>
+ <lv:item name="<?php echo $item_name; ?>" value="<?php echo gen_identifier_value( $id ); ?>" desc="<?php echo $desc; ?>" />
+ <?php } ?>
+ </lv:enum>
+</lv:typedef>
+
+<?php echo $classes; ?>
+
+<lv:section title="Territory Determination">
+ <?php foreach ( $ids as $id ) { ?>
+ <?php $yields = sprintf( '%sTerr%s', $name, $id ); ?>
+ <?php $class = sprintf( '%s-terr%s', $name, gen_identifier( $id ) ); ?>
+ <lv:apply-template name="_terr-code_" class="<?php echo $class; ?>" code="<?php echo gen_identifier_value( $id ); ?>" generates="<?php echo $yields; ?>" />
+ <?php } ?>
+
+ <lv:rate yields="_<?php echo $name; ?>TerrCode">
+ <c:sum of="zip" index="k" generates="<?php echo $name; ?>TerrCode" desc="Territory code">
+ <c:cases>
+ <c:case>
+ <c:when name="<?php echo $param_name; ?>" index="k">
+ <c:gt>
+ <c:const value="0" type="integer" desc="Use territory override if set" />
+ </c:gt>
+ </c:when>
+
+ <c:value-of name="<?php echo $param_name; ?>" index="k" />
+ </c:case>
+
+ <c:otherwise>
+ <c:sum label="Determine applicable territory code">
+ <?php foreach ( $ids as $id ) { ?>
+ <c:value-of name="<?php echo $name; ?>Terr<?php echo $id; ?>" index="k" />
+ <?php } ?>
+ </c:sum>
+ </c:otherwise>
+ </c:cases>
+ </c:sum>
+ </lv:rate>
+</lv:section>
+</lv:package>
diff --git a/src/current/tools/zipre b/src/current/tools/zipre
new file mode 100755
index 0000000..4e6966f
--- /dev/null
+++ b/src/current/tools/zipre
@@ -0,0 +1,23 @@
+#!/usr/bin/env php
+<?php
+/**
+ * Given a set of sorted zips, generates a regular expression to match only the
+ * given input
+ *
+ * I wanted to write this in Scheme (it's a perfect recursive application), but
+ * I figured that other developers may get annoyed having to find a Scheme impl
+ * that works for them...so...PHP it is...
+ *
+ *
+ * THIS SCRIPT EXPECTS THE DATA TO BE SORTED! This can be easily accomplished by
+ * doing the following:
+ * sort -d zipfile | ./zipre
+ */
+
+include 'lib/zipre.php';
+
+// grab input from stdin (must be sorted!)
+$data = explode( "\n", file_get_contents( 'php://stdin' ) );
+
+// build and output
+echo gen_re_quick( $data );