Jump to content

Sky Slate Blueberry Blackcurrant Watermelon Strawberry Orange Banana Apple Emerald Chocolate
Photo

Monster: evaluate math expressions in strings


  • Please log in to reply
87 replies to this topic
Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
Version 0.6 is posted. Changes: restore original number formats, to help using Monster in other scripts; removed work around AHK bugs, fixed in AHK version 1.0.46.09.

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
Version 0.7 is posted. Changes:
- scientific constants (1.2e+5) are now allowed
- several minor simplifications, cleanups
- negative Fibonacci numbers: Fib(-5)
- "·" characters araound internal function names are replaced by single quotes ', so only standard ASCII/ANSI characters (31 < code < 127)are used.

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
Version 0.8 is posted. Changes:
- Width field of binary output is supported. $b: as few bits as necessary (1st bit = sign), $b{W}: outputs the Least Significant W bits.
- Results are now rounded at binary or hex output (so 4/2 is displayed as 0x2, not 2.000000)

Veovis
  • Members
  • 389 posts
  • Last active: Mar 17 2009 12:24 AM
  • Joined: 13 Feb 2006
*Mouth gaping open* :shock:

Laszlo you are amazing! I don't think i could even dream of being as good as you are! This is a very useful script! If this was built into ahk (or people could #include), it would solve alot of complaints from people.
Posted Image
"Power can be given overnight, but responsibility must be taught. Long years go into its making."

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
Thanks, Veovis. It was originally made for my own use, but I am glad that somebody else finds it useful.

Here is Version 0.9. The main difference is the removed brackets [..] around user operators: min, max, gcd and choose. From the role and position of a name Monster determines that it is a variable, function or operator. This is not easy, because the constructs like ...min pi, min1, min abs(x) are very similar. To make the operators stand out, you can adopt your own conventions. E.g. variables and functions are written in lower case, operators are in capitals: x1 MIN sqrt(pi). (The list of the supported operators and functions is extensible, without changing the first pass, which identifies directives, constants, functions, variables and operators.)

There are a few minor internal changes: sgn, MIN and MAX are computed inline now, operator names are also enclosed internally in '...', and the search for the last operator/function name is simplified. I don't want to replace version 0.8 in the first post yet, because the identification of the new operator names might not always work. It would be safer after somebody gave it a few hours test.
; MONSTER Version 0.9 (needs AHK 1.0.46.09+)
; EVALUATE ARITHMETIC EXPRESSIONS containing HEX, Binary ('1001), scientific numbers (1.2e+5)
; (..); variables, constants: e, pi, inch, foot, mile, ounce, pint, gallon, oz, lb;
; (? :); logicals ||; &&; relationals =,<>; <,>,<=,>=; user operators GCD,MIN,MAX,Choose;
; |; ^; &; <<, >>; +, -; *, /, \ (or % = mod); ** (or @ = power); !,~;
; Functions Abs|Ceil|Exp|Floor|Log|Ln|Round|Sqrt|Sin|Cos|Tan|ASin|ACos|ATan|SGN|Fib|fac
; Output = No $: .6 digit decimal; $x,$h: Hex; $b{W}: W-bit binary; ${k}: .k-digit decimal
; "Assignments;" can preceed an expression: a:=1; b:=2; a+b

#SingleInstance Force
#NoEnv
SetBatchLines -1
Process Priority,,High

xe := 2.718281828459045, xpi := 3.141592653589793      ; referenced as "e", "pi"
xinch := 2.54, xfoot := 30.48, xmile := 1.609344       ; [cm], [cm], [Km]
xounce := 0.02841, xpint := 0.5682, xgallon := 4.54609 ; liters
xoz := 28.35, xlb := 453.59237                         ; gramms

/* -test cases
MsgBox % Eval("1e3 -.5e+2 + 100.e-1")                              ; 960.000000
MsgBox % Eval("x := y:1; x := 5*x; y := x+1")                      ; 6 if y empty, x := 1...
MsgBox % Eval("x:=-!0; x<0 ? 2*x : sqrt(x)")                       ; -2
MsgBox % Eval("tan(atan(atan(tan(1))))-exp(sqrt(1))")              ; -1.718282
MsgBox % Eval("---2+++9 + ~-2 --1 -2*-3")                          ; 15
MsgBox % Eval("x1:=1; f1:=sin(x1)/x1; y:=2; f2:=sin(y)/y; f1/f2")  ; 1.850815
MsgBox % Eval("Round(fac(10)/fac(5)**2) - (10choose5) + Fib(8)")   ; 21
MsgBox % Eval("1 min-1 min-2 min 2")                               ; -2
MsgBox % Eval("(-1>>1<=9 && 3>2)<<2>>1")                           ; 2
MsgBox % Eval("(1 = 1) + (2<>3 || 2 < 1) + (9>=-1 && 3>2)")        ; 3
MsgBox % Eval("$b6 -21/3")                                         ; 111001
MsgBox % Eval("$b ('1001 << 5) | '01000")                          ; 100101000
MsgBox % Eval("$0 194*lb/1000")                                    ; 88 Kg
MsgBox % Eval("$x ~0xfffffff0 & 7 | 0x100 << 2")                   ; 0x407
MsgBox % Eval("- 1 * (+pi -((3%5))) +pi+ 1-2 + e-ROUND(abs(sqrt(floor(2)))**2)-e+pi $9") ; 3.141592654
MsgBox % Eval("(20+4 GCD abs(2**4)) + (9 GCD (6 CHOOSE 2))")       ; 11
t := A_TickCount
Loop 1000
   r := Eval("x:=" A_Index/1000 ";atan(x)-exp(sqrt(x))")           ; simulated plot
t := A_TickCount - t
MsgBox Result = %r%`nTime = %t%                                    ; -1.932884. ~360 ms
*/

^#-::                                  ; Replace selection or `expression with result
^#=::                                  ; Append result to selection or `expression
   ClipBoard =
   SendInput ^c                        ; copy selection
   ClipWait 0.5
   If (ErrorLevel) {
      SendInput +{HOME}^c              ; copy, keep selection to overwrite (^x for some apps)
      ClipWait 1
      IfEqual ErrorLevel,1, Return
      If RegExMatch(ClipBoard, "(.*)(``)(.*)", y)
         SendInput %  "{RAW}" y1 . (A_ThisHotKey="^#=" ? y3 . " = "  : "") . Eval(y3)
   } Else
      SendInput % "{RAW}" . (A_ThisHotKey="^#=" ? ClipBoard . " = "  : "") . Eval(ClipBoard)
Return

Eval(x) {                              ; non-recursive PRE/POST PROCESSING: I/O forms, 'func', ";"
   Local FORM, FormF, FormI, i, W, y, y1, y2, y3, y4
   FormI := A_FormatInteger, FormF := A_FormatFloat

   SetFormat Integer, D                ; decimal intermediate results!
   RegExMatch(x, "\$(b|h|x|)(\d*)", y)
   FORM := y1, W := y2                 ; HeX, Bin, .{digits} output format
   SetFormat FLOAT, % y1<>""||W="" ? 0.6 : "0." . W ; Default = 6 decimal places
   StringReplace x, x, %y%             ; remove $..
   Loop
      If RegExMatch(x,"(.*?)(\d+[\.]?\d*|\d*[\.]?\d+)e([\+-]?\d+)(.*)",y) ; 1st group un-greedy: full constant
         x := y1 . y2*10**y3 . y4      ; convert scientific constants to decimal (for speed)
      Else Break
   Loop
      If RegExMatch(x, "i)(.*)(0x[a-f\d]*)(.*)", y)
         x := y1 . y2+0 . y3           ; convert hex numbers to decimal
      Else Break
   Loop
      If RegExMatch(x, "(.*)'([01]*)(.*)", y)
         x := y1 . FromBin(y2) . y3     ; convert binary numbers to decimal: sign = first bit
      Else Break

   StringReplace x, x,`%, \, All       ; %  -> \ (= MOD)
   StringReplace x, x, **,@, All       ; ** -> @ for easier process
   StringReplace x, x, -, #, All       ; # = subtraction, different from sign

   x := RegExReplace(x, "([\)\.\w]\s+|[\)\.\d])([a-z_A-Z]+)","$1'$2'") ; op -> 'op', vars remain

   x := RegExReplace(x,"\s*")          ; remove spaces, tabs, newlines

   x := RegExReplace(x,"([a-z_A-Z]\w*)\(","'$1'(") ; "func(" -> "'func'(" to avoid atan|tan conflicts

   Loop Parse, x, `;
      y := Eval1(A_LoopField)          ; work on pre-processed sub expressions
                                       ; return result of last sub-expression
   If FORM = b                         ; convert to binary
      y := W ? ToBinW(Round(y),W) : ToBin(Round(y))
   Else If (FORM="h" or FORM="x") {
      SetFormat Integer, Hex           ; convert to hex
      y := Round(y) + 0
   }
   SetFormat Integer, %FormI%          ; restore original formats
   SetFormat FLOAT,   %FormF%
   Return y
}

Eval1(x) {                             ; recursive PREPROCESSING of :=, vars, (..) [decimal, no space or ";"]
   Local i, y, y1, y2, y3
   StringGetPos i, x, :=               ; execute leftmost ":=" operator
   If (i >= 0) {
      y := "x" . SubStr(x,1,i)         ; user vars internally start with x to avoid name conflicts
      Return %y% := Eval1(SubStr(x,3+i))
   }                                   ; when here: no variable on the left of last ":="
   x := RegExReplace(x,"([a-z_A-Z]\w*)([^\w'\]]|$)","%x$1%$2") ; VAR -> %xVAR%; func', op] remains
   Transform x, Deref, %x%             ; dereference all right-hand-side %var%-s

   Loop {                              ; find last innermost (..)
      If RegExMatch(x, "(.*)\(([^\(\)]*)\)(.*)", y)
         x := y1 . Eval@(y2) . y3      ; replace "(x)" with value of x (global y3 does not change in Eval@)
      Else Break
   }
   Return Eval@(x)
}

Eval@(x) {                             ; EVALUATE PRE-PROCESSED EXPRESSIONS [decimal, NO: vars, (..), ";", ":="]
   Local i, y, y1, y2, y3, y4
   If x is number
      Return x                         ; no more operators left
                                       ; execute rightmost ?,: operator
   RegExMatch(x, "(.*)(\?|:)(.*)", y)
   IfEqual y2,?,  Return Eval@(y1) ? Eval@(y3) : ""
   IfEqual y2,:,  Return ((y := Eval@(y1)) = "" ? Eval@(y3) : y)

   StringGetPos i, x, ||, R            ; execute rightmost || operator
   IfGreaterOrEqual i,0, Return Eval@(SubStr(x,1,i)) || Eval@(SubStr(x,3+i))
   StringGetPos i, x, &&, R            ; execute rightmost && operator
   IfGreaterOrEqual i,0, Return Eval@(SubStr(x,1,i)) && Eval@(SubStr(x,3+i))
                                       ; execute rightmost =, <> operator
   RegExMatch(x, "(.*)(?<![\<\>])(\<\>|=)(.*)", y)
   IfEqual y2,=,  Return Eval@(y1) =  Eval@(y3)
   IfEqual y2,<>, Return Eval@(y1) <> Eval@(y3)
                                       ; execute rightmost <,>,<=,>= operator
   RegExMatch(x, "(.*)(?<![\<\>])(\<=?|\>=?)(?![\<\>])(.*)", y)
   IfEqual y2,<,  Return Eval@(y1) <  Eval@(y3)
   IfEqual y2,>,  Return Eval@(y1) >  Eval@(y3)
   IfEqual y2,<=, Return Eval@(y1) <= Eval@(y3)
   IfEqual y2,>=, Return Eval@(y1) >= Eval@(y3)
                                       ; execute rightmost user operator (low precedence)
   RegExMatch(x, "i)(.*)'(gcd|min|max|choose)'(.*)", y)
   IfEqual y2,choose,Return Choose(Eval@(y1),Eval@(y3))
   IfEqual y2,Gcd,   Return GCD(   Eval@(y1),Eval@(y3))
   IfEqual y2,Min,   Return (y1:=Eval@(y1)) < (y3:=Eval@(y3)) ? y1 : y3
   IfEqual y2,Max,   Return (y1:=Eval@(y1)) > (y3:=Eval@(y3)) ? y1 : y3

   StringGetPos i, x, |, R             ; execute rightmost | operator
   IfGreaterOrEqual i,0, Return Eval@(SubStr(x,1,i)) | Eval@(SubStr(x,2+i))
   StringGetPos i, x, ^, R             ; execute rightmost ^ operator
   IfGreaterOrEqual i,0, Return Eval@(SubStr(x,1,i)) ^ Eval@(SubStr(x,2+i))
   StringGetPos i, x, &, R             ; execute rightmost & operator
   IfGreaterOrEqual i,0, Return Eval@(SubStr(x,1,i)) & Eval@(SubStr(x,2+i))
                                       ; execute rightmost <<, >> operator
   RegExMatch(x, "(.*)(\<\<|\>\>)(.*)", y)
   IfEqual y2,<<, Return Eval@(y1) << Eval@(y3)
   IfEqual y2,>>, Return Eval@(y1) >> Eval@(y3)
                                       ; execute rightmost +- (not unary) operator
   RegExMatch(x, "(.*[^!\@\*\/\\\~\+\#])(\+|\#)(.*)", y) ; lower precedence ops are already handled
   IfEqual y2,+,  Return Eval@(y1) + Eval@(y3)
   IfEqual y2,#,  Return Eval@(y1) - Eval@(y3)
                                       ; execute rightmost */% operator
   RegExMatch(x, "(.*)(\*|\/|\\)(.*)", y)
   IfEqual y2,*,  Return Eval@(y1) * Eval@(y3)
   IfEqual y2,/,  Return Eval@(y1) / Eval@(y3)
   IfEqual y2,\,  Return Mod(Eval@(y1),Eval@(y3))
                                       ; execute rightmost power
   StringGetPos i, x, @, R
   IfGreaterOrEqual i,0, Return Eval@(SubStr(x,1,i)) ** Eval@(SubStr(x,2+i))
                                       ; execute rightmost function, unary operator
   If !RegExMatch(x,"i)(.*)(!|\+|\#|\~|'(Abs|Ceil|Exp|Floor|Log|Ln|Round|Sqrt|Sin|Cos|Tan|ASin|ACos|ATan|Sgn|Fib|fac)')([-\d\.]+)", y)
      Return x                         ; no more function (y1 <> "" only at multiple unaries: --+-)
   IfEqual y2,!,Return Eval@(y1 . !y4) ; unary !
   IfEqual y2,+,Return Eval@(y1 .  y4) ; unary +
   IfEqual y2,#,Return Eval@(y1 . -y4) ; unary - (they behave like functions)
   IfEqual y2,~,Return Eval@(y1 . ~y4) ; unary ~
   GoTo %y3%                           ; functions are executed last: y4 is number
Abs:
   Return Eval@(y1 . Abs(y4))
Ceil:
   Return Eval@(y1 . Ceil(y4))
Exp:
   Return Eval@(y1 . Exp(y4))
Floor:
   Return Eval@(y1 . Floor(y4))
Log:
   Return Eval@(y1 . Log(y4))
Ln:
   Return Eval@(y1 . Ln(y4))
Round:
   Return Eval@(y1 . Round(y4))
Sqrt:
   Return Eval@(y1 . Sqrt(y4))
Sin:
   Return Eval@(y1 . Sin(y4))
Cos:
   Return Eval@(y1 . Cos(y4))
Tan:
   Return Eval@(y1 . Tan(y4))
ASin:
   Return Eval@(y1 . ASin(y4))
ACos:
   Return Eval@(y1 . ACos(y4))
ATan:
   Return Eval@(y1 . ATan(y4))
Sgn:
   Return Eval@(y1 . (y4>0)-(y4<0)) ; Sign of x = (x>0)-(x<0)
Fib:
   Return Eval@(y1 . Fib(y4))
Fac:
   Return Eval@(y1 . Fac(y4))
}

ToBin(n) {      ; Binary representation of n. 1st bit is SIGN: -8 -> 1000, -1 -> 1, 0 -> 0, 8 -> 01000
   Return n=0||n=-1 ? -n : ToBin(n>>1) . n&1
}
ToBinW(n,W=8) { ; LS W-bits of Binary representation of n
   Loop %W%     ; Recursive (slower): Return W=1 ? n&1 : ToBinW(n>>1,W-1) . n&1
      b := n&1 . b, n >>= 1
   Return b
}
FromBin(bits) { ; Number converted from the binary "bits" string, 1st bit is SIGN
   n = 0
   Loop Parse, bits
      n += n + A_LoopField
   Return n - (SubStr(bits,1,1)<<StrLen(bits))
}

GCD(a,b) {      ; Euclidean GCD
   Return b=0 ? Abs(a) : GCD(b, mod(a,b))
}
Choose(n,k) {   ; Binomial coefficient
   p := 1, i := 0, k := k < n-k ? k : n-k
   Loop %k%                   ; Recursive (slower): Return k = 0 ? 1 : Choose(n-1,k-1)*n//k
      p *= (n-i)/(k-i), i+=1  ; FOR INTEGERS: p *= n-i, p //= ++i
   Return Round(p)
}

Fib(n) {        ; n-th Fibonacci number (n < 0 OK, iterative to avoid globals)
   a := 0, b := 1
   Loop % abs(n)-1
      c := b, b += a, a := c
   Return n=0 ? 0 : n>0 || n&1 ? b : -b
}
fac(n) {        ; n!
   Return n<2 ? 1 : n*fac(n-1)
}


majkinetor
  • Moderators
  • 4512 posts
  • Last active: Jul 29 2016 12:40 AM
  • Joined: 24 May 2006

It was originally made for my own use, but I am glad that somebody else finds it useful.

keep up the good work. Its getting better and better.
BTW, are you still planing user functions ?
Posted Image

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
I am flirting with the following ideas:

- iterators, to print tables
- arrays
- macros (as a way to implement user functions and operators)

They are hard. Which one seems to be the most useful?

washboard
  • Guests
  • Last active:
  • Joined: --
At first absolutely superb !!!
Even if I don't know the usage and the purpose of some (maths) functions (Choose, Fib, ...) !!!
For me I'd like to see macros and, qhy not, array.
Is there a way to :
1) determine the decimal separator ("," in France and Germany, "." in UK and USA)
2) specify the allowed number of decimals allowed after the decimal separator, and if the result is rounded or not (if 12,345 gives 12,34 or 12,35 with the "," séparator and 2 decimals allowed)
Thanks Lazlo for this superb script.

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
Decimal separator: Unfortunately, allowing comma interferes with possible future extensions: lists and multivariate functions. Round(x,3) is clear, but what should Round(2,3) mean (2 or 2,000)? We could completely swap the roles of commas and periods, but Round(x.3) looks weird, as lists {1.2}. One could use semicolons in place of some commas, and define another statement terminator, like colons, but it will be confusing. Nevertheless, some pre- and post processing could achieve what you want. If you can come up with a consistent scheme, not confusing for programmers, please post it.

Decimal places can be specified now with $3 somewhere in the expression. It sets the decimal places used in the calculations and in the output. It is not perfect, because intermediate results should be more accurate than the output. I just hope AHK will support exponential notations (1.2e6), which would allow using maximum precision inside, and only change the output to the desired format.

washboard
  • Guests
  • Last active:
  • Joined: --
Thanks for the explanation for the decimal separator. I'm agree !!!
I didn't understood that "$" was for floating point values. I thought it was for hex and binary values. Thanks for the info.

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
Version 1.0 is posted, with the main missing feature: user defined functions in the expressions. It turned out relatively easy, by saving the function variable and the function definition as text, and replace the variable with the parameter at function call, and finally evaluate the resulting expression with the script, recursively. This way recursive functions can be defined, too. Global variables have their last assigned values, they need not exist, when the function is defined.

So far there were no bugs reported. Please try the script with complicated expressions, to be able to spot problems.

SKAN
  • Administrators
  • 9115 posts
  • Last active:
  • Joined: 26 Dec 2005
Astounding! :shock:

My spouse ( who is very good at Maths ) is helping me to understand the script's capabilities.

Many Thanks. :)

majkinetor
  • Moderators
  • 4512 posts
  • Last active: Jul 29 2016 12:40 AM
  • Joined: 24 May 2006
Thx, that is really something.

Did you thinking about creating some better help file, with some examples and more friendly organisation in doc or html or something?
Posted Image

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
Thanks, Skan, for the nice words. If you have a question, don't hesitate to post it here.

better help file, with some examples and more friendly organization

Examples are included in the script, but I wait with a better help, until AHK offers some tools for fixing the main limitation: precision (fix point intermediate values). Avoiding the conversion of each partial result to fix point can be done if AHK will support scientific number formats (2.007e3), hex-floats or binary forms of numbers. I don't know which one will be supported in the next main AHK release, and when.

Joy2DWorld
  • Members
  • 562 posts
  • Last active: Jun 30 2014 07:48 PM
  • Joined: 04 Dec 2006
@Laszlo,


Very, very Nice.
Joyce Jamce