#!/usr/bin/env python3

import argparse,os,sys
from math import *
from datetime import date,datetime,time,timedelta,tzinfo
from functools import reduce

progname=os.path.basename(sys.argv[0])

def die(msg,rc=1):
  print('%s: %s'%(progname,msg))
  sys.exit(rc)

# Return the value of the given Python expression, or gracefully die trying.
# This is the default --eval function.
def evaluate(expr):
  try:
    return eval(expr)
  except Exception as e:
    die('%s: %r\n'%(str(e),expr))

# Handy functions to use with --map:
def noop(a): return a # <-- default for --map.
def sqr(a): return a**2

# Handy functions to use with --func:
def add(a,b): return a+b # <-- default for --func.

default_format="{result}"

ap=argparse.ArgumentParser(
  formatter_class=argparse.RawDescriptionHelpFormatter,
  description="""\
This command works much like Python's reduce() function. It is, in fact, a
fairly simple wrapper for that function. It basically applies a function of two
variables cumulatively to a list of values. Values are computed from Python
expressions given as command line arguments and/or on standard input (one value
per line). The command line options precede the standard input value if both are
given.""",
  epilog="""
All the functions in Python's math module (ceil, floor, exp, factorial, etc.)
are imported so you'll have access to them. The %(prog)s command also imports
date, datetime, time, timedelta, and tzinfo from Python's datetime module. In
addition, %(prog)s also provides a few handy functions for adding things
together and for squaring numbers:

  add(a,b) - Returns a+b. This is the default FUNC function.
  noop(a)  - Returns a. This is the default MAP function.
  sqr(a)   - Returns a**2.
  
Examples:

1) Add all the numbers on the command line.

    $ %(prog)s 2 4 6 8
    20

   Since "add" is the default value of FUNC, the above is exactly like this:

    $ %(prog)s --func add 2 4 6 8
    20

2) If, for some reason, you want to multiply all the numbers on the command
   line together, there's no provided function for that. You have to use
   Python's lambda syntax:

    $ %(prog)s --func 'lambda a,b:a*b' 2 4 6 8
    384

   The "a,b" before the colon tells lambda what the arguments the function are.
   The "a*b" expression after the colon tells lambda how to compute the return
   value.

3) The input items are actually Python expressions, so so you can (for
   instance) add up the differences between pairs of values.

    $ %(prog)s 10-2 6-4 6-20 5-8
    -7

4) %(prog)s runs the MAP function on each list item before cumulatively calling
   calling FUNC on list items. This lets us, for instance, replace the list's
   items with their absolute values, so we can do the same thing as above, but
   now we're summing the absolute values of those differences.

    $ %(prog)s --map abs 10-2 6-4 6-20 5-8
    27

   The preperatory function abs(x) is applied to each expression before the
   reduce operation is performed. Summing the square of the differences is just
   as easy. Just use the provided sqr(x) function rather than abs(x).

5) You can filter out input items that fail to meet some criterion. Say you
   want to use only the expressions that result in a positive value in the
   example above.

    $ reduce --filter 'lambda a:a>0' 10-2 6-4 6-20 5-8
    10

   Only the values for which FILTER returns True are allowed to participate in
   the reduce operation. All others are discarded.

6) Count the expressions given on the command line (for some reason).

    $ %(prog)s --map 'lambda x:1' these values do not matter
    %(prog)s: name 'these' is not defined: 'these'

   Woops! Remember our input "values" MUST be valid Python expressions. Let's
   try again.

    $ %(prog)s --map 'lambda x:1' "'these'" "'values'" "'do'" "'not'" "'matter'"
    5

   So the values don't matter, but how you express them does.

7) All those quotes are clumsy. Isn't there a better way? Why, yes!

    $ %(prog)s --eval str --map 'lambda x:1' these values do not matter
    5

   The default EVAL function is evaluate(x), which requires x to be a valid
   Python expression. By replacing that with str(x), all that's required of each
   list item is that the argument can be expressed as a Python string. Now that
   we know this, we can be even more brief with our argument counter:

    $ %(prog)s --eval 'lambda x:1' these values do not matter
    5

   Since we don't care about the values of our arguemnts, we just let each one
   "evaluate" to 1, and then add up all the 1s.

   But all that's just to demonstrate --eval and --map. If all you want to do is
   count what are already valid python expressions, just output the variable n,
   which is availble to the FUNC and POST functions.

    $ %(prog)s --post 'lambda x:n' <data
    87

    n is simply the number of ARG values on the command line plus the number of
    entries read from standard input, which is why it's only available
    once all the data have been read.

8) As hinted at above, the POST function accepts and returns one value and lets
   you "post-process" whatever FUNC ends up returning. Use this for rounding,
   formatting, or whatever.

    $ %(prog)s --post 'lambda x:"$%%0.2f"%%(x/3)' 22.21 15.21 14.65 20.05 18.96
    $30.36

   Something like this could be useful if you have to add up Uber payments and
   then split the total three ways. :-)

9) As mentioned above, the variable n is available to both the FUNC and POST
   functions and is set to the total number of expressions given on the command
   line and standard input. So you can use that for computing things like
   averages or standard deviations (albeit in two passes).

    m=$(%(prog)s --post 'lambda x:x/n' <values)
    %(prog)s --map "lambda x:(x-$m)**2" --post 'lambda x:"$%%0.2f"%%sqrt(x/n)' <values
    $2.89

   The first line computes the average and stores that in environment variable
   m. The second line line sums the squares of the distances from that mean
   and then reports the square root of the average of those values, formatting
   it as dollars and cents, rounding to the nearest penny. Pretty nifty! The
   "$2.89" result is what you get if the "values" file contains the values
   from example 8.
"""
)
ap.add_argument('--eval','-e',action='store',default='evaluate',help="""This and the options below are applied in the order given here. EVAL is function of one argument, returning the actual value to be processed. EVAL is typically for regularizing the type of values (e.g. string to numeric) in the given data. (default: %(default)s, which evaluates its argument as a Python expression and returns the result, or it gives a helpful error message and terminates %(prog)s in error if the expression is invalid.)""")
ap.add_argument('--map',dest='map',action='store',default='noop',help="""A Python function of one argument, returning some computed value from each given value, replacing each value in the given data. These preprocessed values are what get fed to FUNC (below). The default (%(default)s) is a function that performs no operation at all, simply returning the value it was given. Since squaring numeric input is sometimes desirable, there's also a an sqr function that will do this (e.g. "--map sqr").""")
ap.add_argument('--filter',dest='filter',action='store',default='None',help="""A Python function of one argument that returns True for each input value to be kept and False for any other value. If given, the FILTER function is applied immediately after processing with MAP (above), before FUNC is cumulatively applied to the given values. For example, "--filter 'lambda a:a>0'" allows only positive numeric values to participate in the reduce operation.""")
ap.add_argument('--intermediate',dest='intermediate',action='store_true',default=False,help="Output all arguments after running any EVAL, MAP, and/or FILTER functions on them. This is the list of values the reduce operation (FUNC) is performed on.")
ap.add_argument('--init','-i',dest='init',default='None',help="""If present, this value is placed before any other arguments in the calculation, and serves as a default when the there are no arguments. If not given and there is only one argument, that argument is simply returned. For example, "--init 0" is helpful when summing a list that might be empty.""")
ap.add_argument('--func','-f',dest='func',action='store',default='add',help="""A python function FUNC(a,b) that accepts two arguments and returns a value of the same type. The returned value is the first argument when FUNC(a,b) is called on the next data value. This function is applied iteratively to each data value in the order given. See examples below. Two predefined functions, "add" and "mult", can be used for these common operations. (default: %(default)s)""")
ap.add_argument('--post',dest='post',default='None',help="""You can use this option to provide a "conditioning" function for the final, reduced value. You can do anything from type coercion (e.g. "--post int") to producing a hash value (e.g. "--post 'lambda x:int(x)%%24'") or even formatting (with a lambda function something like lambda x:"Answer=%%d"%%x).""")
ap.add_argument('--format',default=None,help="""This is a python f-string to be used to output the result. (default=%rs)"""%(default_format,))
ap.add_argument('args',metavar='ARG',action='store',nargs='*',help="Apply FUNC cumulatively to command line arguments (and/or standard input), from left to right, so as to reduce the items to a single value.")
opt=ap.parse_args()
opt.eval=evaluate(opt.eval)
opt.map=evaluate(opt.map)
opt.filter=evaluate(opt.filter)
opt.init=evaluate(opt.init)
opt.func=evaluate(opt.func)
opt.post=evaluate(opt.post)
if opt.format==None:
  opt.format=default_format

# Add standard input (if available) to args (if given).
if not sys.stdin.isatty():
  opt.args.extend(sys.stdin.readlines())
n=len(opt.args)

# Pre-process (EVAL) and adjust (MAP) whatever Python expressions we've been given.
opt.args=[opt.map(opt.eval(arg)) for arg in opt.args]

# Apply any FILTER function to our list of values.
if opt.filter!=None:
  opt.args=filter(opt.filter,opt.args)
n=len(opt.args)
if opt.intermediate:
  print('\n'.join([str(x) for x in opt.args]))

if n==0 and opt.init==None:
  ap.print_usage()
  die('No expressions found on command line or standard input.')

# Run the reduce operation (FUNC).
if opt.init==None:
  result=reduce(opt.func,opt.args)
else:
  result=reduce(opt.func,opt.args,opt.init)

# Post-process our data using the POST function.
if opt.post!=None:
  result=opt.post(result)

# Format our result.
result=eval(f'f"{opt.format}"')

print(result)
