Skip to content

Instantly share code, notes, and snippets.

@earthdiver
Last active April 29, 2024 06:12
Show Gist options
  • Save earthdiver/2ccc2991db9b05b11b9080110becd445 to your computer and use it in GitHub Desktop.
Save earthdiver/2ccc2991db9b05b11b9080110becd445 to your computer and use it in GitHub Desktop.
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment