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.0 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 ) { $x = [double]$n $maxdigits = 100000. if ( $n -lt [bigint]::Zero ) { throw "Negative number specified for factorial." } 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)." } if ( $n -gt [bigint]::One ) { $f = [bigint]::One while ( $n -gt [bigint]::One ) { $f *= $n $n -= [bigint]::One } return $f } else { return [bigint]::One } } 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 |