Jump to content

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

Simple script for evaluating arithmetic expressions


  • Please log in to reply
37 replies to this topic
Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
Until AHK will have dynamic expressions, here is a simple script for evaluating arithmetic expressions in a variable or literal string. It handles +-*/ from left to right with the right precedence, but no unary minus (like -1*2 has to be written as 0-1*2), parentheses or functions, so there is plenty of room for extensions (left to the reader as an easy exercise :wink: ). It can be used to build a calculator or replace selected arithmetic expressions in texts.
a = 12 + 3 * 4 / 2 - 1
MsgBox % Eval(a)
MsgBox % Eval("10/3")

Eval(x)
{
   StringGetPos i, x, +, R
   StringGetPos j, x, -, R
   If (i > j)
      Return Left(x,i)+Right(x,i)
   If (j > i)
      Return Left(x,j)-Right(x,j)
   StringGetPos i, x, *, R
   StringGetPos j, x, /, R
   If (i > j)
      Return Left(x,i)*Right(x,i)
   If (j > i)
      Return Left(x,j)/Right(x,j)
   Return x
}

Left(x,i)
{
   StringLeft x, x, i
   Return Eval(x)
}
Right(x,i)
{
   StringTrimLeft x, x, i+1
   Return Eval(x)
}
The rightmost + or - is located first. The expression is split here and the sum or the difference of the left hand side and the right hand side expression (calculated recursively) gives the answer. If there is no + or -, the rightmost * or / is located. After splitting the expression here, the product or the ratio of the left hand side and the right hand side expression (calculated recursively) gives the answer. If there is no more arithmetic operator left in a sub-expression, it must be a number, so it is returned unchanged.

Edit 20070224: Since the scripts were posted, regular expressions were introduced to AHK. They allow great simplifications and removing some restrictions. A newer version is posted here.

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
The following version is longer, but can handle mod (%) and power (** or @) as well as unary - and unary + (-2*3, +5). Two constants, pi and e are replaced by their numerical values. Add your favorite ones, like currency exchange ratios, Kg to Lbs, miles to Km, Fahrenheit to centigrade conversion values, and life will get a little easier.

Choose this one if you need this extra functionality. Still no parentheses, because they are harder. Having parentheses, function calls, like sqrt(2) or sin(pi/3) will be easier.
; evaluate arithmetic expressions containing
; unary +,- (-2*3; +3)
; +,-; *,/,%(= mod); **(or @, = power)

Eval(x)                                ; pre-process
{
   StringReplace x, x, %A_Space%,, All
   StringReplace x, x, %A_Tab%,, All   ; remove white space
   StringReplace x, x, pi, 3.141592653589793, All
   StringReplace x, x, e,  2.718281828459045, All
   StringReplace x, x, **, @, All      ; ** -> @ for easier process
   Return Eval@(x)
}

Eval@(x)
{
   If (Asc(x) = Asc("-"))
      Return Eval@("0" x)              ; -x -> 0 - x
   If (Asc(x) = Asc("+"))
      Return Right(x,0)                ; +x -> x
   StringGetPos i, x, +, R             ; i = -1 if no + is found
   StringGetPos j, x, -, R
   If (i > j)
      Return Left(x,i)+Right(x,i)
   If (j > i)                          ; i = j only if no + or - found
      Return Left(x,j)-Right(x,j)
   StringGetPos i, x, *, R
   StringGetPos j, x, /, R
   StringGetPos k, x,`%, R
   If (i > j && i > k)
      Return Left(x,i)*Right(x,i)
   If (j > i && j > k)
      Return Left(x,j)/Right(x,j)
   If (k > i && k > j)
      Return Mod(Left(x,k),Right(x,k))
   StringGetPos i, x, @, R
   If (i >= 0)
      Return Left(x,i)**Right(x,i)
   Return x
}

Left(x,i)
{
   StringLeft x, x, i
   Return Eval@(x)
}
Right(x,i)
{
   StringTrimLeft x, x, i+1
   Return Eval@(x)
}


Chris
  • Administrators
  • 10727 posts
  • Last active:
  • Joined: 02 Mar 2004
I always enjoy posts like this where you apply your extensive algorithm expertise to create elegant, efficient scripts and functions.

Thanks for providing another solution for something AutoHotkey lacks.

Nemroth
  • Members
  • 278 posts
  • Last active: Dec 31 2011 10:53 PM
  • Joined: 07 Sep 2004
Thanks a lot, Laszlo.
Of course a brillant script, as every time.
I will try to understand it and if I can, to implement parentheses and functions call !!!
thanks again for all you very clever contribution for AHK.

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
Here is a still larger version with parenthesis, function and variable handling. There could be a problem with expressions like 2*(1-2), which is reduced to 2*-1, an invalid expression. To combat that the subtraction operator is changed internally to # (so it could not be used in function names, as @ is forbidden, used as the power operator). After the replacement the negative sign is not considered an operator but part of a number.

The script also recognizes 3 functions, abs(), sqrt() and floor(), as examples. You can add as many to the list as you wish, even your private functions of one parameter. (If a function name contains another, it has to be listed first, like abs0, MyAbs, ABS.)

Constants might conflict with function names, like e is in Ceil(). To avoid problems, constants have to be in [ ], like [e] or [pi]. Now these can be any variable name, defined in the function Eval(). (Some vars are used, like x, i, j, L,and R. If you want to use them in expression, rename the ones used by the function to some obscure string, like appending a $ sign to them.)

Bitwise operators (~,&,|,^,<<,>>) can be easily added, just follow the pattern of the other operators. Still missing is multi parameter functions, like round([pi],3). It would require handling the "," as operator, which is not hard, but seldom needed.
; AHK 1.0.37
; evaluate arithmetic expressions containing
; unary +,- (-2*3; +3)
; +,-(or # = subtract); *,/,%(= mod); **(or @ = power)
; (,); [var] (like [pi],[e]); abs(),sqrt(),floor()

Eval(x)                                ; top-level process
{
   pi = 3.141592653589793
   e  = 2.718281828459045              ; add below your constants

   StringReplace x, x, %A_Space%,, All ; remove white space
   StringReplace x, x, %A_Tab%,, All
   StringReplace x, x, -, #, All       ; # = subtraction
   StringReplace x, x, (#, (0#, All    ; (-x -> (0-x
   If (Asc(x) = Asc("#"))
      x = 0%x%                         ; leading -x -> 0-x
   StringReplace x, x, (+, (, All      ; (+x -> (x
   If (Asc(x) = Asc("+"))
      StringTrimLeft x, x, 1           ; leading +x -> x
   StringReplace x, x, **, @, All      ; ** -> @ for easier process
   Loop
   {                                   ; replace constants
      StringGetPos i, x, [             ; find [
      IfLess i,0, Break
      StringGetPos j, x, ], L, i+1     ; closest ]
      StringMid  y, x, i+2, j-i-1      ; variable in []
      StringLeft L, x, i
      StringTrimLeft R, x, j+1
      x := L . %y% . R                 ; replace [var] with value of var
   }
   Loop
   {                                   ; finding an innermost (..)
      StringGetPos i, x, (, R          ; rightmost (
      IfLess i,0, Break
      StringGetPos j, x, ), L, i+1     ; closest )
      StringMid  y, x, i+2, j-i-1      ; expression in ()
      StringLeft L, x, i
      StringTrimLeft R, x, j+1
      x := L . Eval@(y) . R            ; replace (x) with value of x
   }
   Return Eval@(x)
}

Eval@(x)
{
   StringGetPos i, x, +, R             ; i = -1 if no + is found
   StringGetPos j, x, #, R
   If (i > j)
      Return Left(x,i)+Right(x,i)
   If (j > i)                          ; i = j only if no + or - found
      Return Left(x,j)-Right(x,j)
   StringGetPos i, x, *, R
   StringGetPos j, x, /, R
   StringGetPos k, x,`%, R
   If (i > j && i > k)
      Return Left(x,i)*Right(x,i)
   If (j > i && j > k)
      Return Left(x,j)/Right(x,j)
   If (k > i && k > j)
      Return Mod(Left(x,k),Right(x,k))
   StringGetPos i, x, @, R             ; power
   If (i >= 0)
      Return Left(x,i)**Right(x,i)
   StringGetPos i1, x, abs, R          ; no more operators
   StringGetPos i2, x, sqrt, R         ; look for functions
   StringGetPos i3, x, floor, R        ; insert further functions below
   m := Max1(i1,i2,i3)
   If (m = i1)                         ; apply the rightmost function
      Return abs(Right(x,i1+2))        ; only one function is applied
   Else If (m = i2)                    ; in one recursion
      Return sqrt(Right(x,i2+3))
   Else If (m = i3)
      Return floor(Right(x,i3+4))      ; offset j + StrLen(func) - 2
   Return x
}

Left(x,i)
{
   StringLeft x, x, i
   Return Eval@(x)
}
Right(x,i)
{
   StringTrimLeft x, x, i+1
   Return Eval@(x)
}
Max1(x0,x1="",x2="",x3="",x4="",x5="",x6="",x7="",x8="",x9="",x10="",x11="",x12="",x13="",x14="",x15="",x16="",x17="",x18="",x19="",x20="")
{
   x := x0
   Loop 20
   {
      IfEqual   x%A_Index%,, Break
      IfGreater x%A_Index%, %x%
           x := x%A_Index%
   }
   IfLess x,0, Return -2               ; prevent match with -1
   Return %x%
}
The script grew quite complex, so there might be some bugs left. Please experiment with it and report any abnormal behavior.

Nemroth
  • Members
  • 278 posts
  • Last active: Dec 31 2011 10:53 PM
  • Joined: 07 Sep 2004
Laszlo, you're very fast !!!
Of course I will try it, and if I can extend it I will post.
But I need first to carrefully read and study the script.
Thanks Laszlo for that superb work.
Why didn't like too mutch maths at school ?

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
I am sure you got a bad teacher. A good one can make all the kids just love math.

Nemroth
  • Members
  • 278 posts
  • Last active: Dec 31 2011 10:53 PM
  • Joined: 07 Sep 2004
May be there are some problems, may be my understanding is not OK :
My first tests :

-1+45/2 = 21.500
I think it is equivalent to -1+45 = 44 then 44/2 = 22
but
(-1+45)/2 = 22.000 OK

45-1/2 = 44.500
but
(45-1)/2 = 22.000 OK

-1+45*2 = 89
but
(-1+45)*2 = 88 OK

45-1*2 = 43
but
(45-1)*2 = 88 OK

abs(12.6) = 12.600

may be I made mistakes.

Thanks for your time.

Nemroth
  • Members
  • 278 posts
  • Last active: Dec 31 2011 10:53 PM
  • Joined: 07 Sep 2004

I am sure you got a bad teacher. A good one can make all the kids just love math.

I think I had bad ones and good ones. But my studies ar far from now...
I think the first time I really begins to have some interest in maths was with my first computer : a ZX81, 16K RAM, working with TV set and using a tape recoder as "hard disk", my first tests to programm in Z80 ASM, but there is a long time ago !!!

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
These come from the differences between how a cheap calculator reacts to keystrokes and how a computer language handles operator precedence: First the power is calculated, then *,/ and %, from left to right, then +,- also fro left to right. In case -1+45/2 first 45/2 is computed (22.5) then -1 is added to it, so the right answer is 21.500. Also, in 45-1/2 first 1/2 is computed (0.5) and then it is subtracted form 45, giving 44.500. Similarly with multiplication: in 45-1*2 first 1*2 is computed (2) and it gets subtracted from 45, giving 43 as the result.

The cheap calculator order of operations is easier, but confusing. If you want that, make the search flat in the Eval@() function, that is, find out the rightmost operator, any of the +-*/% or **, and perform that.

Nemroth
  • Members
  • 278 posts
  • Last active: Dec 31 2011 10:53 PM
  • Joined: 07 Sep 2004
Thanks for your answer, Laszlo.
I understand the operator precedence topic but in fact I thought that the analysis of the entered calculation would be "naturally'" parsed from left to right, like in cheap calculators.
I thought that the calculation is similar in its behaviour as the one of the Windows Calc : an operation, then the other witch modify the prefious result and so on, but on one line...
I think that the parenthesis (in this way to do the things) allow to change the order of the calculations, if it is what the user wants to do.
I will try when I can to make modifications in ths way, as I think that for the use I have of this functionnality, this basic behaviour is better.
Thanks again for the fabulous work

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
Actually, if you use the Windows Calculator, you don't need all these (unless you need your own functions or constants). The calculator buttons all have keyboard shortcuts, so you only have to write the expressions conform to the AHK send command, that is, {F4}, ^p, `% etc. For the XP calculator:
Button    Key       Button    Key

%         %         Hyp       h

(         (         Int       ;

)         )         Inv       i

*         *         ln        n

+         +         log       l

+/-       F9        Lsh       <

-         -         M+        CTRL+P

.         . or ,    MC        CTRL+L

/         /         Mod       %

0-9       0-9       MR        CTRL+R

1/x       r         MS        CTRL+M

=         ENTER     n!        !

A-F       A-F       Not       ~

And       &         Oct       F7

Ave       CTRL+A    Or        |(pipe)

Backspace BACKSPACE pi        p

Bin       F8        Qword     F12

Byte      F4        Radians   F3

C         ESC       s         CTRL+D

CE        DEL       sin       s

cos       o         sqrt      @

Dat       INS       Sta       CTRL+S

Dec       F6        Sum       CTRL+T

Degrees   F2        tan       t

dms       m         Word      F3

Dword     F2        Xor       ^

Exp       x         x^2       @

F-E       v         x^3       #

Grads     F4        x^y       y

Hex       F5
The following script copies the selected expression to the clipboard, starts the Windows Calculator, pastes the clipboard into it, waits a second, copies the result to the clipboard, closes Calculator and finally pastes the result back to the original application, overwriting the selected expression.
!z::

   Send ^c

   WinGet ID, ID, A

   Run Calc.exe

   WinWaitActive Calculator

   Send ^v=

   Sleep 1000

   Send ^c

   WinClose Calculator

   WinActivate ahk_id %ID%

   WinWaitActive ahk_id %ID%

   Send ^v

Return
It is just a proof of the concept, 1 sec waiting time might not always be enough. To be sure wait some 30 ms, copy the result again and see if it changed. The same result can be just bad luck, so wait until the result is the same at several times, like
!z::

   Send ^c

   WinGet ID, ID, A

   Run Calc.exe

   WinWaitActive Calculator

   Send ^v=

   Loop

   {

      Sleep 30

      Send ^c

      If (OldClip = clipboard)

         IfGreater cnt,5, Break

         Else cnt++

      Else {

         OldClip = %clipboard%

         cnt =

     }

   }

   WinClose Calculator

   WinActivate ahk_id %ID%

   WinWaitActive ahk_id %ID%

   Send ^v

Return
Also, you could pre-process the original expression, like replacing F4 by {F4}, so the user need not remember the AHK send syntax.

Nemroth
  • Members
  • 278 posts
  • Last active: Dec 31 2011 10:53 PM
  • Joined: 07 Sep 2004
Hi Lazslo.
I use the Calculator to do the calculations, and I "borrowed" you an other of your scripts to do that. But I'd like to do the calculations internally, as
1) I can't launch the calc hidden
2) the Windows calculator doesn't support function calls.
Here is the script :
FICH_G_Base = D:\Dossiers\Dossier Personnel\Gdr\
FICH_G_SvgClipKC = %FICH_G_Base%Gest\Temp\SvgClipKC.cb		; sauvegarde clipboard Keep Calc

NPACnt =
Return

:*:++::				; I want the calculation to be saved but not displayed
	PP_Typ = 0
	Goto EE_Suite
:*:%%::				; I want the calculation to be saved and displayed
	PP_Typ = 1
EE_Suite:
	PP_Svgde = %clipboard%

;	Gosub, ActiveExtra	; To activate the app in witch I want to use this procedure
				; A IBM3270 Windows emulator from Attachmate I use at work

	Loop
		{
		Input, PP_OutVar, L1 M V, {Space}{Enter}{Tab}{BackSpace}
		IfInString, ErrorLevel, EndKey:Space
			{
			Break
			}
		else IfInString, ErrorLevel, EndKey:Tab
			{
			Break
			}
		else IfInString, ErrorLevel, EndKey:Enter
			{
			Break
			}
		else IfInString, ErrorLevel, EndKey:BackSpace
			{
			Continue
			}
		else
			{
			PP_Res = %PP_Res%%PP_OutVar%
			Continue
			}
		}

	clipboard = %PP_Res%
	Run, Calc
	WinWaitActive Calculatrice
	Send, ^v
	Send,`n
	Sleep, 500
	Send, ^c
	WinClose, Calculatrice
	FileDelete, %FICH_G_SvgClipKC%
	FileAppend, %clipboard%, %FICH_G_SvgClipKC%	; I save the result of the last calculation
;	Send, {Home}{End}				; Keys used in Extra! to del the command line content
	Send, +{Home}{Del}				; For Windows use and test
	If PP_Typ					; I display the result (or not)
		{
		Send, ^v
		}
	PP_Res =
	clipboard = %PP_Svgde%
Return


~^NumpadAdd::					
						; ^+ Send ^v 
						; ^++ Send content last calculation
	HotKey ~^NumpadAdd, Off			; No self interrupt

	Loop
	{
		NPACnt++
		Input key, L1 M V, {NumpadAdd}			; Next key
		IfEqual NPACnt,,				; Counter was reset outside
			{
			IfEqual ErrorLevel,EndKey:NumpadAdd, 
				{
				GetKeyState st, Ctrl, P
				IfEqual st,D, SetEnv NPACnt,1	; New Ctrl-Enter arrived
				}
			break
			}
		IfEqual ErrorLevel,EndKey:NumpadAdd,continue	; At Enter: keep on counting
		GoSub NPAAct
		break
	}

	HotKey ~^NumpadAdd, On
	return

~Ctrl Up::				; Ctrl released: Stop counting
	IfEqual NPACnt,,return		; Exit if no Ctrl-Enter was pressed yet
	GoSub NPAAct
	return

NPAAct:
	Cnt := NPACnt
	NPACnt =			; Reset while MsgBox is still active
	If Cnt = 1
		{
		FileRead, NPA_DernCalc, %FICH_G_SvgClipKC%
		Send, %NPA_DernCalc%
		}
	If Cnt = 2
		{
		NPA_Svgde = %clipboard%
		FileRead, clipboard, %FICH_G_SvgClipKC%
		Send, ^v
		clipboard = %NPA_Svgde%
		}
	return
You can recognize your slightly modified work...

For the moment I haven't the time but I'll work your "Simple script for evaluating arithmetic expressions" to make it do the calculations in the Windows Calc way (but without calling the Windows calc) and to support multi parameters function call, if I can.

I think that the use of the operator precedence isn't very efficient for a "standard" user and that the Windows Calc's behavior is the one witch is expected in this case.
But of course your script is perfectly correct.
Thanks again for your fabulous work.

Invalid User
  • Members
  • 447 posts
  • Last active: Mar 27 2012 01:04 PM
  • Joined: 14 Feb 2005
Laszlo, do you plan on making a Evaluation function for handling undefined variables or irrationals, or 'solve for...' equations? or fractions
my lame sig :)

Laszlo
  • Moderators
  • 4713 posts
  • Last active: Mar 31 2012 03:17 AM
  • Joined: 14 Feb 2005
You could add yourself checking for undefined functions or variables, that is easy. On the other hand, making it a full blown math package is hard. AHK was not made for that. For example, numbers are practically fix point ([pi]/10000000000*10000000000 <> [pi]), and if you work with large or small numbers the precision of the intermediate results ruin the accuracy. Maybe the best is to interface AHK to GMP, the GNU multi precision numerical library, compiled to a Windows DLL.

Keeping fractions unevaluated to get rational results can be done, like complex number handling, but I don't know how you want to handle irrationals, especially transcendent numbers (pi, e), which are not roots of integer coefficient polynomials. Symbolic math packages keep expressions unevaluated, and manipulate them symbolically. It is hard.

Symbolic solution of general equations is also hard. Numerical root finding with an iterative method, like Regula Falsi, can be done for a single variable, otherwise it is hard. But then you could ask for optimization, too: finding the minimum. In the multivariate case, you guessed, is hard, too.