#!/usr/bin/perl
package PerlPowerTools::units;

use strict;
use warnings;
use open qw(:std :utf8);

=begin metadata

Name: units
Description: conversion program
Author: Mark-Jason Dominus, mjd-perl-units@plover.com
License: gpl

=end metadata

=cut

use Config;

# Usage:
# units [-f unittab]
our $VERSION = '1.02';

BEGIN {
    require Data::Dumper;
	sub dumper { Data::Dumper->new([@_])->Indent(1)->Sortkeys(1)->Terse(1)->Useqq(1)->Deparse(1)->Dump }

	foreach my $letter ( qw( d p o l t ) ) {
	  no strict 'refs';
	  my $env_var = uc( "UNITS_DEBUG_$letter" );
	  my( $debugging ) = grep { defined and length } ( $ENV{$env_var}, $ENV{UNITS_DEBUG}, 0 );
	  *{"debug_$letter"} = ! $debugging ? sub {} : sub {
		my $indent;
		my $m = join '', @_;
		$indent = $1 if $m =~ s/\A(\s*)//;
		print "$indent$letter>>> $m\n";
	  }
	}
}

our %unittab;            # Definitions loaded here

# Metric prefixes.  These must be powers of ten or change the
# token_value subroutine
our %PREF;
our $PREF;
our $PARSE_ERROR;

BEGIN {
  %PREF = (
    yotta => -24,
    zetta => -21,
    atto  => -18,
    femto => -15,
    pico  => -12,
    nano  => - 9,
    micro => - 6,
    milli => - 3,
    centi => - 2,
    deci  => - 1,
    deca  =>   1,
    deka  =>   1,
    hecto =>   2,
    hect  =>   2,
    kilo  =>   3,
    myria =>   4,
    mega  =>   6,
    giga  =>   9,
    tera  =>  12,
    zepto =>  15,
    yocto =>  18,
  );
  $PREF = join '|', sort {$PREF{$a} <=> $PREF{$b}} (keys %PREF);
}

# run if called directly, indirectly, directly par-packed, undirectly par-packed
__PACKAGE__->run(@ARGV) if !caller() || caller(0) =~ /^(PerlPowerTools::Packed|PAR)$/ || caller(1) eq 'PAR'; # modulino

sub run {
  my( $class, @args ) = @_; local @ARGV;

  my $args = $class->process_args( @args );

  $class->read_unittab( $args->{unittabs}[0] );

  if (@{ $args->{args} }) {
    my ($have, $want) = @{ $args->{args} };
    my $have_hr = $class->unit_have($have);
    my $want_hr = $class->unit_want($want);
    my %r = $class->unit_convert($have_hr, $want_hr);
    print_result(%r);
  } else {
    while (1) {
      print "You have: ";
      my $have = <>;
      exit 0 unless defined($have) && $have =~ /\S/;
      my $have_hr = $class->unit_have($have);
      next if is_Zero($have_hr->{hu});

      print "You want: ";
      my $want = <>;
      exit 0 unless defined($want) && $want =~ /\S/;
      my $want_hr = $class->unit_want($want);
      next if is_Zero($want_hr->{wu});

      my %r = $class->unit_convert($have_hr, $want_hr);
      print_result(%r);
    }
  }

  exit 0;
}

sub test {
    my ($class, $have, $want) = @_;

    $class->read_unittab();
    my $have_hr = $class->unit_have($have);
    my $want_hr = $class->unit_want($want);
    my %r = $class->unit_convert($have_hr, $want_hr);
    return %r;
}

sub default_unittabs {
  grep { -e } qw(/usr/lib/unittab);
}

sub env_unittabs {
  no warnings 'uninitialized';
  split /$Config{path_sep}/, $ENV{UNITTAB};
}

sub process_args {
    my( $class, @args ) = @_;

    my @unittabs;
    while (@args and $args[0] =~ /^-/) {
      my $flag = shift @args;
      if ($flag =~ s/\A\-f//) {
        my $file = $flag;
        $file = shift @args if (length($file) == 0);
        $class->usage() unless defined $file;
        push @unittabs, $file;
      } elsif ($flag eq '--') {
        last;
      } elsif ($flag =~ /^--version$/) {
        print "perl units version $VERSION.\n";
        exit 0;
      } else {
        warn "Unknown flag: $flag.\n";
        $class->usage();
      }
    }

    $class->usage() if @args == 1 || @args > 2;

    @unittabs = $class->env_unittabs unless @unittabs;
    @unittabs = $class->default_unittabs unless @unittabs;

    { unittabs => \@unittabs, args => \@args }
}

sub read_unittab {
  my( $class, $file ) = @_;

  my $fh;
  if (defined $file) {
    unless (-d $file) {
      open $fh, '<:encoding(UTF-8)', $file or do {
        die "Could not open <$file>: $!\n";
      };
      $class->read_defs($file, $fh);
      return;
    }
  }
  debug_d('Reading from DATA');
  open $fh, '<:encoding(UTF-8)', __FILE__ or die;
  while (<$fh>) {
    last if /\A__(?:DATA|END)__/;
  }
  $class->read_defs('DATA', $fh);
}

sub unit_have {
  my ($class, $have) = @_;

  trim($have);

  my $is_negative = 0;
  if ($have =~ /^[-]/) {
    $is_negative = 1;
    $have =~ s/^[-]//;  # remove minus sign
  }

  my $is_quantified = $have =~ /^[\d.]+/;

  if ($have =~ s/^\s*\#\s*//) {
    if ($class->definition_line($have)) {
      print "Defined.\n";
    } else {
      print "Error: $PARSE_ERROR.\n";
    }
    return;
  }
  return unless $have =~ /\S/;

  my $hu = $class->parse_unit($have);

  if (is_Zero($hu)) {
    print $PARSE_ERROR, "\n";
    return;
  }

  return { have => $have, hu => $hu, neg => $is_negative, quan => $is_quantified };
}

sub unit_want {
    my ($class, $want) = @_;

    trim($want);
    return unless $want =~ /\S/;

    my $wu = $class->parse_unit($want);

    if (is_Zero($wu)) {
      print $PARSE_ERROR, "\n";
    }
    return { want => $want, wu => $wu };
}

sub unit_convert {
    my ($class, $have_hr, $want_hr ) = @_;

    my $have = $have_hr->{have};
    my $hu = $have_hr->{hu};
    my $is_negative = $have_hr->{neg};
    my $is_quantified = $have_hr->{quan};

    my $want = $want_hr->{want};
    my $wu = $want_hr->{wu};

    debug_t('have unit', dumper($hu));
    debug_t('want unit', dumper($wu));

    my $is_temperature = 0;
    $is_temperature++ if $hu->{Temperature};
    $is_temperature++ if $wu->{Temperature};

    my $quot
        = $is_temperature == 2
        ? undef
        : unit_divide($hu, $wu);

    my %retval;

    if ($is_temperature == 2) {
        # we have temperature units
        $have =~ s/^[-]?[\d.]*\s*//;
        my $v = $hu->{'_'};
        $v *= -1 if $is_negative;
        $v  =  0 if not $is_quantified;
        my $k
            = exists $hu->{hof}
            ? $hu->{hof}->{to}->($v)
            : $v;
        my $t
            = exists $wu->{hof}
            ? $wu->{hof}->{from}->($k)
            : $k;
        %retval = ( type => 'temperature', v => $v, have => $have, t => $t, want => $want );
    }
    elsif (is_dimensionless($quot)) {
      my $q = $quot->{_};
      my $p = 1/$q;
      %retval = ( type => 'dimless', q => $q, p => $p);
    }
    else {
      %retval = ( type=> 'error', msg =>
        "conformability (Not the same dimension)\n" .
        "\t" . $have . " is " . text_unit($hu) . "\n" .
        "\t" . $want . " is " . text_unit($wu) . "\n"
        );
    }

    return %retval;
}

sub print_result {
    my (%r) = @_;
    printf "\t%.6g %s is %.6g %s\n", $r{v}, $r{have}, $r{t}, $r{want}
        if $r{type} eq 'temperature';
    printf "\t* %.6g\n\t/ %.6g\n", $r{q}, $r{p}
        if $r{type} eq 'dimless';
    print $r{msg} if $r{type} eq 'error';
}

################################################################

sub usage {
  my( $class ) = @_;
  require Pod::Usage;
  Pod::Usage::pod2usage({ -exitval => 1, -verbose => 0 });
}

sub read_defs {
  my ($class, $filename, $fh) = @_;
  while (<$fh>) {
  	next if m|\A/|;  # comment line
    trim($_);
    next unless /\S/;

    debug_d( "$_" );
    my $hash = $class->definition_line($_);
    foreach my $key ( keys %$hash ) {
      $unittab{$key} = $hash->{$key};
    }
  }
}

sub definition_line {
  my ($class, $line) = @_;
  my ($name, $data) = split /\s+/, $line, 2;
  my $value = $class->parse_unit($data);
  debug_t("$name => $data") if $data =~ /^\{\s/;
  my $rc = do {
    if ($data =~ /^\{\s/) {
        my $hof = eval $data;   # hash of functions
        +{ $name => { _ => 1, hof => $hof, Temperature => 1 } }
    }
    elsif (is_Zero($value))        { undef                              }
    elsif (is_fundamental($value)) { +{ $name => {_ => 1, $name => 1} } }
    else                           { +{ $name => $value               } }
  };

  unless( defined $rc ) {
    $line =~ s/\s+/ => /;
    warn "Parse error: $PARSE_ERROR in $line.  Skipping.\n";
    $rc = {};
  }

  return $rc;
}

sub trim {              # note that trim() is a L-value sub
  $_[0] =~ s/\#.*$//;;
  $_[0] =~ s/\s+$//;
  $_[0] =~ s/^\s+//;
}

sub Zero () { +{ _ => 0 } }

# here we guard the zero test by first checking to see
# if we're dealing with a temperature
sub is_Zero { !$_[0]{Temperature} && !$_[0]{_} }

sub unit_lookup {
  my ($name) = @_;
  debug_l( "Looking up unit '$name'" );
  return $unittab{$name} if exists $unittab{$name};
  if ($name =~ /s$/) {
    my $shortname = $name;
    $shortname =~ s/s$//;
    return $unittab{$shortname} if exists $unittab{$shortname};
  }
  my ($prefix, $rest) = ($name =~ /^($PREF-?)(.*)/o);
  unless ($prefix) {
    $PARSE_ERROR = "Unknown unit '$name'";
    return Zero;
  }
  my $base_unit = unit_lookup($rest); # Recursive
  con_multiply($base_unit, 10**$PREF{$prefix});
}

sub unit_multiply {
  my ($a, $b) = @_;
  debug_o( "Multiplying @{[%$a]} by @{[%$b]}: " );
  my $r = {%$a};
  $r->{_} *= $b->{_};
  for my $u (keys %$b) {
    next if $u eq '_';
    $r->{$u} += $b->{$u};
  }
  debug_o( "\tResult: @{[%$r]}" );
  $r;
}

sub unit_divide {
  my ($a, $b) = @_;
  my $r = {%$a};
  die "Division by zero error" if $b->{_} == 0;
  $r->{_} /= $b->{_};
  for my $u (keys %$b) {
    next if $u eq '_';
    $r->{$u} -= $b->{$u};
  }
  $r;
}

sub unit_power {
  my ($p, $u) = @_;
  debug_o( "Raising unit @{[%$u]} to power $p." );
  my $r = {%$u};
  $r->{_} **= $p;
  for my $d (keys %$r) {
    next if $d eq '_';
    $r->{$d} *= $p;
  }
  debug_o( "\tResult: @{[%$r]}" );
  $r;
}

sub unit_dimensionless {
  debug_o( "Turning $_[0] into a dimensionless unit." );
  return +{_ => $_[0]};
}

sub con_multiply {
  my ($u, $c) = @_;
  debug_o( "Multiplying unit @{[%$u]} by constant $c." );
  my $r = {%$u};
  $r->{_} *= $c;
  debug_o( "\tResult: @{[%$r]}" );
  $r;
}

sub is_dimensionless {
  my ($r) = @_;
  for my $u (keys %$r) {
    next if $u eq '_';
    return if $r->{$u} != 0;
  }
  return 1;
}

# Generate bogus unit value that signals that a new fundamental unit
# is being defined
sub new_fundamental_unit {
  return +{__ => 'new', _ => 1};
}

# Recognize this bogus value when it appears again.
sub is_fundamental {
  exists $_[0]{__};
}

sub text_unit {
  my ($u) = @_;
  my (@pos, @neg);
  my $c = $u->{_};
  for my $k (sort keys %$u) {
    next if $k eq '_' or $k eq 'hof';
    push @pos, $k if $u->{$k} > 0;
    push @neg, $k if $u->{$k} < 0;
  }
  my $text = ($c == 1 ? '' : $c);
  for my $d (@pos) {
    my $e = $u->{$d};
    $text .= " $d";
    $text .= "^$e" if $e > 1;
  }

  $text .= ' per' if @neg;
  for my $d (@neg) {
    my $e = - $u->{$d};
    $text .= " $d";
    $text .= "^$e" if $e > 1;
  }

  $text;
}
################################################################
#
# I'm the parser
#

our @actions;
BEGIN {
  sub sh { ['shift', $_[0]]  };
  sub go { ['goto', $_[0]] };

  my $eof_state = 98;

  @actions =
    (
     # Initial state
     {
     PREFIX      => sh(1),
     NUMBER      => sh(2),
     NAME        => sh(3),
     FUNDAMENTAL => sh(4),
     FRACTION    => sh(5),
     POWER       => sh(17),
     '('         => sh(6),
     'unit'      => go(7),
     'topunit'   => go($eof_state),
     'constant'  => go(8),
     },

     # State 1:   constant -> PREFIX .
     { _ => ['reduce', 1, 'constant']},

     # State 2:   constant -> NUMBER .
     { _ => ['reduce', 1, 'constant']},

     # State 3:   unit -> NAME .
     { _ => ['reduce', 1, 'unit', \&unit_lookup ]},

     # State 4:   unit -> FUNDAMENTAL .
     { _ => ['reduce', 1, 'unit', \&new_fundamental_unit ]},

     # State 5:   constant -> FRACTION .
     { _ => ['reduce', 1, 'constant']},

     # State 6:   unit -> '(' . unit ')'
     {PREFIX      => sh(1),
      NUMBER      => sh(2),
      NAME        => sh(3),
      FUNDAMENTAL => sh(4),
      FRACTION    => sh(5),
      '('         => sh(6),
      'unit'      => go(9),
      'constant'  => go(8),
     },

     # State 7:   topunit -> unit .
     #            unit  ->  unit . TIMES unit
     #            unit  ->  unit . DIVIDE unit
     #            unit  ->  unit . NUMBER
     {NUMBER => sh(10),
      TIMES  => sh(11),
      DIVIDE => sh(12),
      POWER  => sh(17),
      _      =>  ['reduce', 1, 'topunit'],
     },

     # State 8:   unit -> constant . unit
     #            unit -> constant .
     {PREFIX      => sh(1),
      NUMBER      => sh(2), # Shift-reduce conflict resolved in favor of shift
      NAME        => sh(3),
      FUNDAMENTAL => sh(4),
      FRACTION    => sh(5),
      '('         => sh(6),
      _           => ['reduce', 1, 'unit', \&unit_dimensionless],
      'unit'      => go(13),
      'constant'  => go(8),
     },

     # State 9:   unit -> unit . TIMES unit
     #            unit -> unit . DIVIDE unit
     #            unit -> '(' unit . ')'
     #            unit -> unit . NUMBER
     {NUMBER => sh(10),
      TIMES  => sh(11),
      DIVIDE => sh(12),
      POWER  => sh(17),
      ')'    => sh(14),
     },

     # State 10:  unit -> unit NUMBER .
     { _ => ['reduce', 2, 'unit',
         sub {
           unless (int($_[1]) == $_[1]) {
             ABORT("Nonintegral power $_[1]");
             return Zero;
           }
           unit_power(@_);
         }
        ],
     },

     # State 11:  unit -> unit TIMES . unit
     {PREFIX      => sh(1),
      NUMBER      => sh(2),
      NAME        => sh(3),
      FUNDAMENTAL => sh(4),
      FRACTION    => sh(5),
      '('         => sh(6),
      'unit'      => go(15),
      'constant'  => go(8),
     },

     # State 12:  unit -> unit DIVIDE . unit
     {PREFIX      => sh(1),
      NUMBER      => sh(2),
      NAME        => sh(3),
      FUNDAMENTAL => sh(4),
      FRACTION    => sh(5),
      '('         => sh(6),
      'unit'      => go(16),
      'constant'  => go(8),
     },

     # State 13:  unit -> unit . TIMES unit
     #            unit -> unit . DIVIDE unit
     #            unit -> constant unit .
     #            unit -> unit . NUMBER
     {NUMBER => sh(10),  # Shift-reduce conflict resolved in favor of shift
      TIMES  => sh(11),  # Shift-reduce conflict resolved in favor of shift
      DIVIDE => sh(12),  # Shift-reduce conflict resolved in favor of shift
      POWER  => sh(17),
      _      => ['reduce', 2, 'unit', \&con_multiply],
     },

     # State 14: unit => '(' unit ')' .
     { _ => ['reduce', 3, 'unit', sub {$_[1]}] },

     # State 15: unit  ->  unit . TIMES unit
     #           unit  ->  unit TIMES unit .
     #           unit  ->  unit . DIVIDE unit
     #           unit  ->  unit . NUMBER
     {NUMBER => sh(10), # Shift-reduce conflict resolved in favor of shift
      _ => ['reduce', 3, 'unit', sub {unit_multiply($_[0], $_[2])}],
     },

     # State 16: unit  ->  unit . TIMES unit
     #           unit  ->  unit DIVIDE unit .
     #           unit  ->  unit . DIVIDE unit
     #           unit  ->  unit . NUMBER
     {NUMBER => sh(10), # Shift-reduce conflict resolved in favor of shift
      _ => ['reduce', 3, 'unit', sub{unit_divide($_[2], $_[0])}],
     },

     # State 17:  unit -> unit POWER . unit
     {
      NUMBER      => sh(2),
      'constant'  => go(18),
     },

     # State 18: unit -> unit POWER
     {
     NUMBER => sh(2),
     _ => ['reduce', 3, 'unit', sub{ unit_power($_[0], $_[2])}],
     },

    );

    $actions[98] = {EOF => go($eof_state + 1),};
    $actions[99] = {_   => ['accept']};
}

sub ABORT {
  $PARSE_ERROR = shift;
}

sub parse_unit {
  my ($class, $s) = @_;
  my $tokens = lex($s);
  my $STATE = 0;
  my (@state_st, @val_st);

  $PARSE_ERROR = undef;

  debug_p(  '-' x 50 . "\n" );
  # Now let's run the parser
  for (;;) {
    return Zero if $PARSE_ERROR;

    debug_p( "Tokens: " . join( ' ', map { "<$_>" } @$tokens ) );
    my $la = @$tokens ? token_type($tokens->[0]) : 'EOF';
    debug_p( "Now in state $STATE.  Lookahead type is $la." );
    debug_p( "State stack is (@state_st)." );
    my $actiontab = $actions[$STATE];
    my $action = $actiontab->{$la} || $actiontab->{_};
    unless ($action) {
      $PARSE_ERROR = 'Syntax error';
      return Zero;
    }

    my ($primary, @actargs) = @$action;
    debug_p( "Next thing: $primary (@actargs)" );
    if ($primary eq 'accept') {
      return $val_st[0];    # Success!
    } elsif ($primary eq 'shift') {
      my $token = shift @$tokens;
      my $val = token_value($token);
      debug_p( "shift: token: <$token> val: <$val>" );
      push @val_st, $val;
      push @state_st, $STATE;
      $STATE = $actargs[0];
      debug_p( "shift: state: <$STATE>" );
    } elsif ($primary eq 'goto') {
      $STATE = $actargs[0];
    } elsif ($primary eq 'reduce') {
      my ($n_args, $result_type, $semantic) = @actargs;
      my @arglist;
      while ($n_args--) {
        push @arglist, pop @val_st;
        $STATE = pop @state_st;
      }
      my $result = $semantic ? &$semantic(@arglist) : $arglist[0];
      push @val_st, $result;
      push @state_st, $STATE;
    debug_p( "reduce: Value stack is " . dumper( \@val_st ) );

      debug_p( "Post-reduction state is $STATE." );

      # Now look for 'goto' actions
      my $goto = $actions[$STATE]{$result_type};
      unless ($goto && $goto->[0] eq 'goto') {
        die "No post-reduction goto in state $STATE for $result_type.\n";
      }
      debug_p( "goto $goto->[1]" );
      $STATE = $goto->[1];
    } else {
      die "Bad primary $primary";
    }
  }
}


sub lex {
  my ($s) = @_;
  my $N = '(?:\d+\.\d+|\d+|\.\d+)(?:[eE][-+]?\d+)?';

  my @t = split /(
                   (?: \*.\* | !.! ) # Special 'new unit' symbol
                |  [()*-]                    # Symbol
                |  \s*(?:\/|\bper\b)\s*      # Division
                |  (?:$N\|$N)                # Fraction
                |  $N                        # Decimal number
                |  \d+                       # Integer
                |  [A-Za-z_][A-Za-z_.]*      # identifier
                |  \s+                       # White space
                )/ox, $s;
  @t = grep {defined and $_ ne ''} @t;  # Discard empty and all-white tokens
  debug_p( "Input: $s Tokens: @t" );
  \@t;
}

sub token_type {
  my ($token) = @_;
  return $token->[0]   if ref $token;
  return $token        if $token =~ /[()]/;
  return 'TIMES'       if $token =~ /^\s+$/;
  return 'FUNDAMENTAL' if $token =~ m/\A(!.!|\*.\*)\z/;
  return 'DIVIDE'      if $token =~ /^\s*(\/|\bper\b)\s*$/;
  return 'TIMES'       if $token eq '*' || $token eq '-';
  return 'FRACTION'    if $token =~ /^\d+\|\d+$/;
  return 'NUMBER'      if $token =~ /^[.\d]/;
  return 'POWER'       if $token eq '^';
  return 'NAME';
}

sub token_value {
  my ($token) = @_;
  debug_p( "TOKEN VALUE: <$token>" );

  my $rc = do {
       if( $token =~ /^([()*\/-]|\s*\bper\b\s*)$/ ) { $token }
    elsif( $token =~ /(\d+)\|(\d+)/ ) {
      if( $2 == 0 ) {
        ABORT("Zero denominator in fraction '$token'");
        return 0;
      }
      $1/$2;
    }
    else { $token }
  };

  return $rc;        # Perl takes care of the others.
}

=encoding utf8

=head1 NAME

units - conversion program

=head1 SYNOPSIS

    % units
	You have: in
	You want: cm
		* 2.54
		/ 0.393701

    % units [-f /path/to/unittab] [want_unit have_unit]

=head1 OPTIONS

    -f           Use specified definition file
    --version    Display version information

=head1 DESCRIPTION

NOTE: This does not handle the Gnu units format (https://www.gnu.org/software/units/).

The units program converts quantities expressed in various scales to their
equivalents in other scales.  The units program can only handle multiplicative
or affine scale changes (except for temperature).  It works in one of
two ways.  If given two units as command line arguments, it reports
the conversion.  Otherwise, it operates interactively by prompting the user
for inputs:

    % units
    You have: meters
    You want: feet
        * 3.2808399
        / 0.3048

    You have: cm3
    You want: gallons
        * 0.00026417205
        / 3785.4118

    You have: meters/s
    You want: furlongs/fortnight
        * 6012.8848
        / 0.00016630952

    You have: 1|2 inch
    You want: cm
        * 1.27
        / 0.78740157

    You have: 98.6 F
    You want: C
        98.6 F is 37 C

    You have: -40 C
    You want: F
        -40 C is -40 F

Powers of units can be specified using the '^' character as shown in the
example, or by simple concatenation: 'cm3' is equivalent to 'cm^3'.

Multiplication of units can be specified by using spaces, a dash or an asterisk.

Division of units is indicated by the slash ('/').  Note that multiplication has
a higher precedence than division, so 'm/s/s' is the same as 'm/s^2' or 'm/s s'.
Division of numbers must be indicated using the vertical bar ('|').  To convert
half a meter, you would write '1|2 meter'.  If you write '1/2 meter' then the
units program would interpret that as equivalent to '0.5/meter'.

If you enter incompatible unit types, the units program will print a message
indicating that the units are not conformable and it will display the reduced
form for each unit:

    You have: ergs/hour
    You want: fathoms kg^2 / day
    conformability error
         2.7777778e-11 kg m^2 / sec^3
         2.1166667e-05 kg^2 m / sec

The conversion information is read from a units data file.  The default file
includes definitions for most familiar units, abbreviations and metric
prefixes.  Some constants of nature included are:

    pi         ratio of circumference to diameter
    c          speed of light
    e          charge on an electron
    g          acceleration of gravity
    force      same as g
    mole       Avogadro's number
    water      pressure per unit height of water
    mercury    pressure per unit height of mercury
    au         astronomical unit

The unit 'pound' is a unit of mass.  Compound names are run together so 'pound
force' is a unit of force.  The unit 'ounce' is also a unit of mass.  The fluid
ounce is 'floz'.  British units that differ from their US counterparts are
prefixed with 'br', and currency is prefixed with its country name:
'belgiumfranc', 'britainpound'.  When searching for a unit, if the specified
string does not appear exactly as a unit name, then units will try to remove a
trailing 's' or a trailing 'es' and check again for a match.

To find out what units are available read the standard units file.  If you want
to add your own units you can supply your own file.  If no standard file
exists and you do not supply your own file, this program uses internal
data.

A unit is specified on a single line by giving its name and an equivalence.  Be
careful to define new units in terms of old ones so that reductions leads to
primitive units.  Primitive (a.k.a. fundamental) units are defined
with a string of three characters which begin and end with '*' or '!'.
Note that the units program will not detect infinite loops that could be
caused by careless unit definitions.

Comments in the unit definition file begin with a '/' or '#' character at
the beginning of a line.  Once the parser has successfully parsed a
unit name and it's definition, the remainder of the line is ignored.
This makes it safe to include in-line comments.

Prefixes are defined in the same way as standard units, but with a
trailing dash at the end of the prefix name.  If a unit is not found even
after removing trailing 's' or 'es', then it will be checked against the
list of prefixes.  Prefixes will be removed until a legal base unit is
identified.

Here is an example of a short units file that defines some basic units.

    m           !a!
    sec         ***
    Temperature ***
    micro-      1e-6
    minute      60 sec
    hour        60 min
    inch        0.0254 m
    ft          12 inches
    mile        5280 ft

If a "Temperature" dimension is defined in the units table, then
you can define various temperature scales as units by specifying
the code needed to convert the unit to or from Kelvin.
The built-in units table has definitions for Kelvin (K), Celsius (C),
Fahrenheit (F) and Rankine (R).

The code consists of a perl hash containing the keys 'to' and 'from'.
The values are the subroutine definitions necessary to convert a
value from Kelvin to the specified unit, or to Kelvin from the the
specified unit.  See the built-in unit table for examples.

A temperature unit entered at "You have" without any constant
preceding it will default to zero units.  This is in contrast to
non-temperature units, where a bare unit name is assumed to mean 1
unit.  Also, for temperatures only, negative constants are allowed.
This enables, for example, a conversation between -40C and F.

=head1 AUTHOR

Mark-Jason Dominus, C<< <mjd-perl-units@plover.com> >>

Temperature support by Gary Puckering, C<< <jgpuckering@rogers.com> >>

Currently maintained in https://github.com/briandfoy/PerlPowerTools

=head1 BUGS

=head1 COPYRIGHT and LICENSE

This program is copyright (c) M-J. Dominus (1996, 1999).

This program is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation; either version 2 of the License, or (at your option) any later
version, or under Perl's 'Artistic License'.

This program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.  See the GNU General Public License for more details.

=cut

no warnings qw(void);
__PACKAGE__;

__END__

# Notes (from Gary Puckering)
#  1.  The comment indicator is "/" at the start of a line
#  2.  However, the parser ignores trailing unrecognized trailing tokens
#      and so # seems to be an effective in-line comment indicator
#  3.  Temperature entries in version 1.034 and prior caused parsing errors
#  4.  Temperature support has now been added, and the entries have been updated
#  5.  If you have your own units file and it has temperatures, you'll need to update it

# dimensions
m                     ***
kg                    ***
sec                   ***
coul                  ***
candela               ***
radian                ***
bit                   ***
erlang                ***
Temperature           ***

# constants

unity                 1
pi                    3.14159265358979323846 unity
c                     2.997925e+8 m/sec unity
g                     9.80665 m/sec2
au                    1.49597871e+11 m unity
mole                  6.022169e+23 unity
e                     1.6021917e-19 coul unity
energy                c2
force                 g
mercury               1.33322e+5 kg/m2-sec2
hg                    mercury

# dimensionless

degree                1|180 pi-radian
circle                2 pi-radian
turn                  2 pi-radian
grade                 .9 degree
arcdeg                1 degree
arcmin                1|60 arcdeg
ccs                   1|36 erlang
arcsec                1|60 arcmin

steradian             radian2
sphere                4 pi-steradian
sr                    steradian

# Time

second                sec
s                     sec
minute                60 sec
min                   minute
hour                  60 min
hr                    hour
day                   24 hr
da                    day
week                  7 day
year                  365.24219879 day unity
yr                    year
month                 1|12 year
ms                    millisec
us                    microsec

# Mass

gram                  millikg
gm                    gram
mg                    milligram
metricton             kilokg

# Temperature
C                     { to => sub { $_[0] + 273.15 }, from => sub { $_[0] - 273.15 } }
F                     { to => sub { ( $_[0] + 459.67 ) * 5/9 }, from => sub { $_[0] * 9/5 - 459.67 } }
R                     { to => sub { $_[0] * 5/9 }, from => sub { $_[0] * 9/5 } }
K                     Temperature
Celsius               C
Fahrenheit            F
Kelvin                K
Rankine               R
celsius               C
fahrenheit            F
kelvin                K
rankine               R

# Avoirdupois

lb                    .45359237 kg
lbf                   lb g
ounce                 1|16 lb
oz                    ounce
dram                  1|16 oz
dr                    dram
grain                 1|7000 lb
gr                    grain
shortton              2000 lb
ton                   shortton
longton               2240 lb

# Apothecary

scruple               20 grain
apdram                60 grain
apounce               480 grain
appound               5760 grain

# Length

meter                 m
cm                    centimeter
mm                    millimeter
km                    kilometer
nm                    nanometer
micron                micrometer
angstrom              decinanometer

inch                  2.54 cm
in                    inch
foot                  12 in
feet                  foot
ft                    foot
yard                  3 ft
yd                    yard
rod                   5.5 yd
rd                    rod
mile                  5280 ft
mi                    mile

british               1200|3937 m/ft
nmile                 1852m

acre                  4840 yd2

cc                    cm3
liter                 kilocc
ml                    milliliter

# US Liquid

gallon                231 in3
imperial              1.20095
gal                   gallon
quart                 1|4 gal
qt                    quart
pint                  1|2 qt
pt                    pint

floz                  1|16 pt
fldr                  1|8 floz

# US Dry

dry                   268.8025 in3/gallon unity
peck                  8 dry-quart
pk                    peck
bushel                4 peck
bu                    bushel

# British

brgallon              277.420 in3 unity
brquart               1|4 brgallon
brpint                1|2 brquart
brfloz                1|20 brpint
brpeck                554.84 in3 unity
brbushel              4 brpeck

# Energy Work

newton                kg-m/sec2
nt                    newton
joule                 nt-m
cal                   4.1868 joule

# Electrical

coulomb               coul
ampere                coul/sec
amp                   ampere
watt                  joule/sec
volt                  watt/amp
ohm                   volt/amp
mho                   1/ohm
siemens               mho
farad                 coul/volt
henry                 sec2/farad
weber                 volt-sec

# Light

cd                    candela
lumen                 cd sr
lux                   cd sr/m2


# Trivia

%                     1|100
admiraltyknot         6080 ft/hr
apostilb              cd/pi-m2
are                   1e+2 m2
arpentcan             27.52 mi
arpentlin             191.835 ft
astronomicalunit      au
atmosphere            1.01325e+5 nt/m2
atm                   atmosphere
atomicmassunit        1.66044e-27 kg unity
amu                   atomicmassunit
bag                   94 lb
bakersdozen           13
bar                   1e+5 nt/m2
barie                 1e-1 nt/m2
barleycorn            1|3 in
barn                  1e-28 m2
barrel                42 gal
barye                 1e-1 nt/m2
bev                   1e+9 e-volt
biot                  10 amp
blondel               cd/pi-m2
boardfoot             144 in3
bolt                  40 yd
bottommeasure         1|40 in
britishthermalunit    1.05506e+3 joule unity
btu                   britishthermalunit
refrigeration         12000 btu/ton-hour
cable                 720 ft
caliber               1e-2 in
calorie               cal
carat                 205 mg
cental                100 lb
centesimalminute      1e-2 grade
centesimalsecond      1e-4 grade
century               100 year
cfs                   ft3/sec
chain                 66 ft
circularinch          1|4 pi-in2
circularmil           1e-6|4 pi-in2
clusec                1e-8 mm-hg m3/s
coomb                 4 bu
cord                  128 ft3
cordfoot              cord
crith                 9.06e-2 gm
cubit                 18 in
cup                   1|2 pt
curie                 3.7e+10 /sec
dalton                amu
decade                10 yr
dioptre               1/m
displacementton       35 ft3
doppelzentner         100 kg
dozen                 12
drop                  .03 cm3
dyne                  cm-gm/sec2
electronvolt          e-volt
ell                   45 in
engineerschain        100 ft
engineerslink         100|100 ft
equivalentfootcandle  lumen/pi-ft2
equivalentlux         lumen/pi-m2
equivalentphot        cd/pi-cm2
erg                   cm2-gm/sec2
ev                    e-volt
faraday               9.652e+4 coul
fathom                6 ft
fermi                 1e-15 m
fifth                 4|5 qt
finger                7|8 in
firkin                9 gal
footcandle            lumen/ft2
footlambert           cd/pi-ft2
fortnight             14 da
franklin              3.33564e-10 coul
frigorie              kilocal
furlong               220 yd
galileo               1e-2 m/sec2
gamma                 1e-9 weber/m2
gauss                 1e-4 weber/m2
geodeticfoot          british-ft
geographicalmile      1852 m
gilbert               7.95775e-1 amp
gill                  1|4 pt
gross                 144
gunterschain          22 yd
hand                  4 in
hectare               1e+4 m2
hefnercandle          .92 cd
hertz                 1/sec
hogshead              2 barrel
hd                    hogshead
homestead             1|4 mi2
horsepower            550 ft-lb-g/sec
hp                    horsepower
hyl                   gm force sec2/m
hz                    hertz
imaginarycubicfoot    1.4 ft3
jeroboam              4|5 gal
karat                 1|24
kcal                  kilocal
kcalorie              kilocal
kev                   1e+3 e-volt
key                   kg
khz                   1e+3 /sec
kilderkin             18 gal
knot                  nmile/hr
lambert               cd/pi-cm2
langley               cal/cm2
last                  80 bu
league                3 mi
lightyear             c-yr
line                  1|12 in
link                  66|100 ft
longhundredweight     112 lb
longquarter           28 lb
lusec                 1-6 mm-hg m3/s
mach                  331.46 m/sec
magnum                2 qt
marineleague          3 nmile
maxwell               1e-8 weber
metriccarat           200 mg
mev                   1e+6 e-volt
mgd                   megagal/day
mh                    millihenry
mhz                   1e+6 /sec
mil                   1e-2 in
millenium             1000 year
minersinch            1.5 ft3/min
minim                 1|60 fldr
mo                    month
mpg                   mile/gal
mph                   mile/hr
nail                  1|16 yd
nauticalmile          nmile
nit                   cd/m2
noggin                1|8 qt
nox                   1e-3 lux
ns                    nanosec
oersted               2.5e+2 pi-amp/m
oe                    oersted
pace                  36 in
palm                  3 in
parasang              3.5 mi
parsec                au-radian/arcsec
pascal                nt/m2
pc                    parsec
pennyweight           1|20 oz
percent               %
perch                 rd
pf                    picofarad
phot                  lumen/cm2
pica                  1|6 in
pieze                 1e+3 nt/m2
pipe                  4 barrel
point                 1|72 in
poise                 gm/cm-sec
pole                  rd
poundal               ft-lb/sec2
pdl                   poundal
proof                 1|200
psi                   lb-g/in2
quarter               9 in
quartersection        1|4 mi2
quintal               100 kg
quire                 25
rad                   100 erg/gm
ream                  500
registerton           100 ft3
rehoboam              156 floz
rhe                   10 m2/nt-sec
rontgen               2.58e-4 curie/kg
rood                  1.21e+3 yd2
rope                  20 ft
rutherford            1e+6 /sec
rydberg               1.36054e+1 ev
sabin                 1 ft2
sack                  3 bu
seam                  8 bu
section               mi2
shippington           40 ft3
shorthundredweight    100 lb
shortquarter          25 lb
sigma                 microsec
skein                 120 yd
skot                  1e-3 apostilb
slug                  lb-g-sec2/ft
span                  9 in
spat                  4 pi sr
spindle               14400 yd
square                100 ft2
stere                 m3
sthene                1e+3 nt
stilb                 cd/cm2
stoke                 1e-4 m2/sec
stone                 14 lb
strike                2 bu
surveyfoot            british-ft
surveyorschain        66 ft
surveyorslink         66|100 ft
tablespoon            4 fldr
teaspoon              4|3 fldr
tesla                 weber/m2
therm                 1e+5 btu
thermie               1e+6 cal
timberfoot            ft3
tnt                   4.6e+6 m2/sec2
tonne                 1e+6 gm
torr                  mm hg
township              36 mi2
tun                   8 barrel
water                 .22491|2.54 kg/m2-sec2
wey                   40 bu
weymass               252 lb
Xunit                 1.00202e-13m

# The following entry looks like it might be the Boltzmann constant?
# If so, then it is incorrect.  It should be 1.3807e-16 erg/degK.  In
# any event, the temperature logic doesn't support mixed affine/linear
# reductions.
# - Gary Puckering
// k                     1.38047e-16 erg/degC
