psCalc.psm1

using namespace System.Numerics
<#
    .SYNOPSIS
        A tool that enables the use of the PowerShell console as a scientific calculator.
 
    .DESCRIPTION
        Accepts standard mathematical expressions as input and outputs the results on the console by
        internally converting the expressions into forms that PowerShell can parse.
        If no expression is specified as an argument, a read-eval-print-loop (REPL) session will start.
        Intended for interactive use.
     
    .PARAMETER Expr
        Specifies a mathematical expression to evaluate (optional).
        It is recommended to use this in combination with the stop-parsing token (--%).
     
    .INPUTS
        Expressions can be passed as the first argument or specified at REPL prompts. This function
        does not accept inputs from a pipeline.
 
    .OUTPUTS
        This function does not return a value. Outputs will be redirected to the host device.
 
    .NOTES
        Commands : exit, quit, verbose on, verbose off, ? (the last invoked expression), err (the last error)
        Constants: e (=2.71828...), pi (=3.14159...)
        img. unit: i (=[Comprex]::new(0,1))
        Functions: Abs, Acos, Acosh, Asin, Asinh, Atan, Atan2, Atanh, Ceiling, Cos, Cosh, Exp, Floor,
                   Log, Log10, Log2, Max, Min, Pow, Round, Sign, Sin, Sinh, Sqrt, Tan, Tanh, Truncate
        UserFunc.: f1, f2, f3, g1, g2, g3, h1, h2, h3
        Operators: ! (factorial), ^/** (powers), % (remainder), *, +, -, /,
                   and other arithmetic operators that start with '-'
        Variables: $result (the result of the previous expression; 'ans' can be used instead.)
        Remarks : - Two constants, 'pi' and 'e' (the base of the natural logarithms), are available.
                   - There is no need to prefix math functions such as 'Abs' with [Math]:: or [Bigint]::,
                     as [Math]:: or [Math_]:: are automatically added internally. Methods with the same
                     name that call math functions contained in the System.Math, System.Numerics.BigInteger,
                     and System.Numerics.Complex classes are overloaded in the Math_ class.
                   - The power operator can be written as '^' or '**', and the factorial operator as '!'.
                   - PowerShell's arithmetic operators (starting with a minus sign, such as '-band')
                     can be used.
                   - PowerShell functions can be defined and used. For example, you can define a function
                     as 'function f($x,$y){$x+$y}', and call it as 'f 3 4'. You can also write a function
                     definition as 'f($x,$y):=$x+$y'. If operations are performed on the result of a
                     user-defined function, it must be enclosed in parentheses (e.g., '2 * pi * (f 3 4)').
                   - The following 9 functions can be used as usual, because they are rewritten as
                     class methods internally: f1, g1, h1 (functions with one argument), f2, g2, h2
                     (functions with two arguments), and f3, g3, h3 (functions with three arguments).
                     For example, you can define a function as 'f2($x,$y):=$x^2+$y^2' and then call it
                     as 'f2(3,4)'.
                   - PowerShell variables (beginning with $) can be defined and used. Only alphanumeric
                     characters and underscores (_) can be used in user-defined variable names. Using other
                     characters in variable names may cause incorrect behavior.
                   - The result of the previous expression is stored in the $result variable and can be
                     referenced in the next expression. 'ans' can also be used to specify the last result.
                   - If you enter 'verbose on', subsequent sessions will output the internally converted
                     formulas. In addition, additional error messages are displayed when a syntax error
                     occurs. If you enter 'verbose off', the output will stop. Also, the last invoked
                     expression can be displayed using the '?' command.
                   - Suffixes for numeric literals such as 'd' (Decimal), 'l' (Long), and 'n' (BigInteger
                     *in PowerShell 7.0 or later) can be specified (e.g. '0.1d'). If you want to
                     explicitly specify the 'Double' type, you can use casting such as '[Double]0.1'.
                   - You can represent hexadecimal numbers in the format of '0xFFFFFFFF'. In addition,
                     in PowerShell 7.0 or later, you can represent binary numbers in the format of
                     '0b11111111' and specify a suffix 'u' to indicate unsigned.
                   - Instead of creating an object like [Numerics.Complex]::new(1,-2), complex numbers
                     can also be written such as '1 - 2i'.
                   - If a real number is specified for an operation targeting integers, the number will
                     be rounded to the nearest integer before the operation.
                   - You can execute many of the PowerShell cmdlets.
 
        Author : earthdiver1
        Version: 1.11
        Licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
         
    .EXAMPLE
        PS> pscalc --% sqrt(2)
        => invoked expr.: [Math_]::sqrt(2)
        1.4142135623731
 
        PS> Invoke-MathExpression
        Calc> (1 + 2/3) * 4
        6.66666666666667
        Calc> sqrt( 5 )
        2.23606797749979
        Calc> 5^(2^-1)
        2.23606797749979
        Calc> $a = pi/4
        Calc> $a
        0.785398163397448
        Calc> sin( $a )
        0.707106781186547
        Calc> (sin( $a ))^2 + (cos( $a ))^2
        1
        Calc> tan( pi/4 )
        1
        Calc> atan( 1 )
        0.785398163397448
        Calc> ans * 4
        3.14159265358979
        Calc> 10!
        3628800
        Calc> ?
        => last invoked exp. : [Math_]::Fac(10)
        Calc> f1( $n ) := switch( $n ){ 0{1} 1{1} default{ $n * f1($n - 1) } }
        Calc> f1( 10 )
        3628800
        Calc> verbose on
        Calc> f2( $x, $y ) := $x * $x + $y * $y
        => invoked expression: function f2( $x, $y ) { $x * $x + $y * $y }
        Calc> sqrt( f2( 3 , 4 ) )
        => invoked expression: [Math_]::sqrt( [Math_]::f2( 3 , 4 ) )
        5
        Calc> verbose off
        Calc> f1( $x ) := f2( sin( $x ), cos( $x ) )
        Calc> f1( $a )
        1
        Calc> 2^2^2^2
        Syntax error
        Calc> ((2^2)^2)^2
        256
        Calc> 2^(2^(2^2))
        65536
        Calc> 2^256
        1.15792089237316E+77
        Calc> ([bigint]2)^256 # can be written as '2n^256' on PowerShell 7.0
        115792089237316195423570985008687907853269984665640564039457584007913129639936
        Calc> (1-2i)^3
        -11 + 2i
        Calc> exp(i)
        0.54030230586814 + 0.841470984807897i
        Calc> cos(1) + sin(1) * i
        0.54030230586814 + 0.841470984807897i
#>

function Invoke-MathExpression {
[Alias('psCalc')]
param( [Parameter(Position=0,ValueFromRemainingArguments)][string]$Expr )

class Math_ {
    static  [bigint] Abs(  [bigint]$x )             { return  [bigint]::Abs( $x )     }
    static  [object] Abs(  [object]$x )             { return    [Math]::Abs( $x )     }
    static  [double] Abs( [complex]$x )             { return [complex]::Abs( $x )     }
    static  [double] Acos(  [double]$x )            { return    [Math]::Acos( $x )    }
    static [complex] Acos( [complex]$x )            { return [complex]::Acos( $x )    }
    static  [double] Asin(  [double]$x )            { return    [Math]::Asin( $x )    }
    static [complex] Asin( [complex]$x )            { return [complex]::Asin( $x )    }
    static  [double] Atan(  [double]$x )            { return    [Math]::Atan( $x )    }
    static [complex] Atan( [complex]$x )            { return [complex]::Atan( $x )    }
    static  [double] Cos(  [double]$x )             { return    [Math]::Cos( $x )     }
    static [complex] Cos( [complex]$x )             { return [complex]::Cos( $x )     }
    static  [double] Cosh(  [double]$x )            { return    [Math]::Cosh( $x )    }
    static [complex] Cosh( [complex]$x )            { return [complex]::Cosh( $x )    }
    static  [double] Exp(  [double]$x )             { return    [Math]::Exp( $x )     }
    static [complex] Exp( [complex]$x )             { return [complex]::Exp( $x )     }
    static  [double] Log(  [bigint]$x )             { return  [bigint]::Log( $x )     }
    static  [double] Log(  [double]$x )             { return    [Math]::Log( $x )     }
    static [complex] Log( [complex]$x )             { return [complex]::Log( $x )     }
    static  [double] Log(  [bigint]$x, [double]$y ) { return  [bigint]::Log( $x, $y ) }
    static  [double] Log(  [double]$x, [double]$y ) { return    [Math]::Log( $x, $y ) }
    static [complex] Log( [complex]$x, [double]$y ) { return [complex]::Log( $x, $y ) }
    static  [double] Log10(  [bigint]$x )           { return  [bigint]::Log10( $x )   }
    static  [double] Log10(  [double]$x )           { return    [Math]::Log10( $x )   }
    static [complex] Log10( [complex]$x )           { return [complex]::Log10( $x )   }
    static  [bigint] Log2( [bigint]$x )             { return  [bigint]::Log2( $x )    }
    static  [double] Log2( [double]$x )             { return    [Math]::Log2( $x )    }
    static  [bigint] Max( [bigint]$x, [bigint]$y )  { return  [bigint]::Max( $x, $y ) }
    static  [object] Max( [object]$x, [object]$y )  { return    [Math]::Max( $x, $y ) }
    static  [bigint] Min( [bigint]$x, [bigint]$y )  { return  [bigint]::Min( $x, $y ) }
    static  [object] Min( [object]$x, [object]$y )  { return    [Math]::Min( $x, $y ) }
    static  [bigint] Pow(  [bigint]$x,    [int]$y ) { return  [bigint]::Pow( $x, $y ) }
    static  [double] Pow(  [double]$x, [double]$y ) { return    [Math]::Pow( $x, $y ) }
# static [complex] Pow( [complex]$x, [double]$y ) { return [complex]::Pow( $x, $y ) }
    static [complex] Pow( [complex]$x, [double]$y ) {
        if ( $y -eq [Math]::Truncate($y) -and [Math]::Abs($y) -le 1023.0 ) {
            $n = [int]$y
            if ( $n -eq 0 ) { return [complex]::One }
            if ( $n -gt 0 ) {
                $p = [complex]::One
                while ( $n -gt 0 ) {
                    if ( $n -band 1 ) { $p *= $x }
                    $x *= $x
                    $n = $n -shr 1
                }
                return $p
            } else {
                return 1.0 / [Math_]::Pow( $x, -$y )
            }
        }
        return [complex]::Pow( $x, $y )
    }
    static  [double] Sin(  [double]$x )             { return    [Math]::Sin( $x )     }
    static [complex] Sin( [complex]$x )             { return [complex]::Sin( $x )     }
    static  [double] Sinh(  [double]$x )            { return    [Math]::Sinh( $x )    }
    static [complex] Sinh( [complex]$x )            { return [complex]::Sinh( $x )    }
# static [double] Sqrt( [double]$x ) { return [Math]::Sqrt( $x ) }
    static  [object] Sqrt(  [double]$x )            {
        if ( $x -lt 0 ) { return [complex]::new( 0, [Math]::Sqrt( -$x ) ) }
        return [Math]::Sqrt( $x )
    }
    static [complex] Sqrt( [complex]$x )            { return [complex]::Sqrt( $x )    }
    static  [double] Tan(  [double]$x )             { return    [Math]::Tan( $x )     }
    static [complex] Tan( [complex]$x )             { return [complex]::Tan( $x )     }
    static  [double] Tanh(  [double]$x )            { return    [Math]::Tanh( $x )    }
    static [complex] Tanh( [complex]$x )            { return [complex]::Tanh( $x )    }
    static  [bigint] Fac(  [bigint]$n ) {
        if ( $n -lt [bigint]::Zero ) { throw "Negative number specified for factorial." }
        $maxdigits = 100000.
        $x = [double]$n
        if ( [Math]::Abs( 0.5 * [Math]::Log10(  2 * [Math]::PI * $x ) + 
                           $x * [Math]::Log10( $x / [Math]::E       )   ) -gt $maxdigits ) {
            throw "Number of digits exceeded the upper limit(=$maxdigits)."
        }
        $f = [bigint]::One
        while ( $n -gt [bigint]::One ) {
            $f *= $n
            $n -= [bigint]::One
        }
        return $f
    }
    static [object] Fac( [object]$x ) {
        $n = [long]$x
        if ( $n -lt 0 ) { throw "Negative number specified for factorial." }
        if ( $n -gt 170 ) {
            return [double]::PositiveInfinity
        } elseif ( $n -gt 27 ) {
            return 1..$n | & { begin{ $f = 1.0 } process{ $f *=  [double]$_ } end{ $f } }
        } elseif ( $n -gt 1 ) {
            return 1..$n | & { begin{ $f = 1.d } process{ $f *= [decimal]$_ } end{ $f } }
        } else {
            return 1d
        }
    }
    static [object] f1( [object]$x )                         { return (f1 $x)       }
    static [object] g1( [object]$x )                         { return (g1 $x)       }
    static [object] h1( [object]$x )                         { return (h1 $x)       }
    static [object] f2( [object]$x, [object]$y )             { return (f2 $x $y)    }
    static [object] g2( [object]$x, [object]$y )             { return (g2 $x $y)    }
    static [object] h2( [object]$x, [object]$y )             { return (h2 $x $y)    }
    static [object] f3( [object]$x, [object]$y, [object]$z ) { return (f3 $x $y $z) }
    static [object] g3( [object]$x, [object]$y, [object]$z ) { return (g3 $x $y $z) }
    static [object] h3( [object]$x, [object]$y, [object]$z ) { return (h3 $x $y $z) }
}

$params = @{
    TypeName   = 'System.Numerics.Complex'
    MemberType = 'ScriptMethod'
    MemberName = 'ToString2'
    Value      = {
        param( [string]$format = "" )
        if ( $this.Real -ne 0.0 ) {
            $cmplx = "$($this.Real.ToString($format))"
            if ( $this.Imaginary -gt 0.0 ) { $cmplx += " + $( $this.Imaginary.ToString($format))i" }
            if ( $this.Imaginary -lt 0.0 ) { $cmplx += " - $(-$this.Imaginary.ToString($format))i" }
        } else {
            $cmplx = "0"
            if ( $this.Imaginary -ne 0.0 ) { $cmplx = "$($this.Imaginary.ToString($format))i"      }
        }
        return $cmplx -replace '(?<![.0-9])1i$','i'
    }
}
Update-TypeData @params -Force

$options = [Text.RegularExpressions.RegexOptions]'ECMAScript, IgnoreCase'
$iunit   = [complex]::ImaginaryOne
$e       = [Math]::E
$pi      = [Math]::PI
$binhex  = '\b(?:0b[01]+|0x[0-9a-f]+)[inu]?\b'
$integer = '(?<![\w.])[0-9]+[iln]?(?![\w.])'
$decimal = '(?<![\w.])(?:[0-9]+(?:\.[0-9]*|(?=[de]))|\.[0-9]+)(?:e[+-]?[0-9]+)?[din]?(?![\w.])'
$number  = "(?:$binhex|$integer|$decimal)"
$usrvar  = '\$[0-9_a-z]+(?:\.[0-9_a-z]+|\[[^\]]+\])?(?![\w.[])'
#$strwip = '\((?>(?:[^()]+|(?<o>\()|(?<-o>\)))*)(?(o)(?!))\)' # string within parenthesis (similar to '(?<p>\((?>(?:[^()]+|(?&p))*)\))' or '(?<p>\((?>[^()]*(?:(?&p)[^()]*)*)\))' with PCRE)
$strwip  = '\((?>[^()]*(?:(?:(?<o>\()[^()]*)+(?:(?<-o>\))[^()]*)+)*)(?(o)(?!))\)'  # string within parenthesis
$operand = "(?:$number|$usrvar|\B$strwip)"
$mathfn1 = "(?<!]::)\b((?:Acosh|Asinh|Atan2|Atanh|Ceiling|Floor|Round|Sign|Truncate)$strwip)"
$mathfn2 = "(?<!]::)\b((?:Abs|Acos|Asin|Atan|Cos|Cosh|Exp|Log|Log10|Log2|Max|Min|Pow|Sin|Sinh|Sqrt|Tan|Tanh)$strwip)"
$factor  = "(?<!(?:\*\*|\^)\s*)($operand)\s*!(?!\s*(?:\*\*|[!^]))"
$powers  = "(?<!(?:\*\*|\^)\s*)($operand)\s*(?:\*\*|\^)\s*([+-]?\s*$operand)(?!\s*(?:\*\*|[!^]))"
$usrfdef = '^\s*([a-z][0-9_a-z]*\s*(?:\([^)]*\))?)\s*:=\s*(.+)\s*$'
$usrfn   = "(?<!(?:\bfunction\s+|]::))\b([fgh][123]$strwip)"
$vars    = @('$e','$pi','$binhex','$cmplx','$decimal','$expression','$factor','$integer','$iunit',
             '$lastex','$mathfn1','$mathfn2','$number','$operand','$options','$powers','$strwip',
             '$usrfdef','$usrfn','$usrvar','$verbose','$var','$vars')

$eval = {
    $expression = [regex]::Replace($expression, '\bans\b', '$$result', $options)
    $expression = [regex]::Replace($expression, '\bi\b', '$$iunit', $options)
    $expression = [regex]::Replace($expression, "($number)(?<=i)", "(`$1`0*`$`$iunit)", $options).Replace("i`0",'')
    $expression = [regex]::Replace($expression, '(?<!]::)\bPi\b', '$$pi',$options)
    $expression = [regex]::Replace($expression, '(?<!]::)\bE\b', '$$e' ,$options)
    $expression = [regex]::Replace($expression, $usrfdef, 'function $1 { $2 }', $options)
    while ( [regex]::IsMatch($expression, $mathfn1, $options) ) {
        $expression = [regex]::Replace($expression, $mathfn1, '[Math]::$1', $options)
    }
    while ( [regex]::IsMatch($expression, $mathfn2, $options) ) {
        $expression = [regex]::Replace($expression, $mathfn2, '[Math_]::$1', $options)
    }
    while ( [regex]::IsMatch($expression, $usrfn, $options) ) {
        $expression = [regex]::Replace($expression, $usrfn, '[Math_]::$1', $options)
    }
    while ( [regex]::IsMatch($expression, $factor, $options) ) {
        $expression = [regex]::Replace($expression, $factor, '[Math_]::Fac($1)', $options)
    }
    while ( [regex]::IsMatch($expression, $powers, $options) ) {
        $expression = [regex]::Replace($expression, $powers, '[Math_]::Pow($1,$2)', $options)
    }
    $expression = $expression.Replace('[Math_]::Pow($e,', '[Math_]::Exp(')
    if ( $verbose ) { Write-Host "=> invoked expr.: $expression" }
    try {
        $lastex = $expression
        ( $result = Invoke-Expression $expression ) | ForEach-Object {
            if ( $_ -and $_.GetType().Name -eq "Complex" ) { $_.ToString2() } else { $_ }
        } | Out-Host
    } catch {
        if ( $_.Exception.WasThrownFromThrowStatement ) {
            Write-Host $_.Exception.Message -ForegroundColor Red
        } else {
            Write-Host "Syntax error" -ForegroundColor Red
            if ( $verbose ) {
                Write-Host $_.Exception.Message -BackgroundColor Black -ForegroundColor Yellow
            }
        }
    }
}

$expression = $Expr.Replace("--% ","") -replace '^-E(x(pr?)?)? '
$verbose = $expression -ne ""

if ( $expression ) {
    . $eval
} else {
    $lastex = ""
    $result = ""
:loop 
    while ( $true ) {
        Write-Host "Calc> " -NoNewLine
        $expression = ( $Host.UI.ReadLine() -replace '#.*$' ).Trim()
        foreach ( $var in $vars ) {
            if ( $expression -match "\$var\b" ) {
                Write-Host "Invalid variable name. '$var' is reserved for the program." -ForegroundColor Red
                continue loop
            }
        }
        switch -Regex ( $expression ) {
            '^(exit|quit)$'   { break loop }
            '^$'              { continue loop }
            '^verbose\s+on$'  { $verbose = $true ; continue loop }
            '^verbose\s+off$' { $verbose = $false; continue loop }
            '^\?$'            { Write-Host "=> prev. expr. : $lastex" }
            '^err$'           { Write-Host "=> last error : $($global:Error[0].Exception.Message)" }
            default           { . $eval }
        }
    }
}
} # end of function block
Export-ModuleMember -Function Invoke-MathExpression -Alias psCalc