Compute numbers within TextEdit — MathEdit

Sometimes the calculator that ships with OS X doesn’t quiet hack it, because you want to alter the formula, and a spreadsheet doesn’t either quite fit the bill, since you also want to see the formula. And as with the calculator, the previous results disappears as easily as with the former. Now, I could have done all my calculations, at least most of them in a terminal window. But I am not in a terminal window all the time, and I would miss out on the trigonometric and expontial functions anyway, so I was reluctant to use it, and was wondering if it was time to by soulver, that I suppose does this job perfectly.

But then again, my idea was quite different, as I am lazy, I wouldn’t like to first select text, copy it, edit it, and then run the program again. Copy a calculation, edit it, and then run the script, and have the result show up below, is also too much. I figured out the uttermost laziness would be to have the script insert both the line that was computed in its original form, but with a ! (bang) in front of it, and the result on a line below, prepended by a colon.

Edit

I have the added ability to “define” variables, see the last post for an example.

Example:

(4*3)+3*(4-1)-3
! (4*3)+3*(4-1)-3
: 42

If I run it again, and it sees that the line is unaltered, then it should leave the result be, exactly like it was. -It is important that you don’t insert any newlines/returns after your calculations, unless you haven’t edited it.

Here I have edited the original calculation like so:

(4*5)+5*(6-1)-5
! (4*3)+3*(4-1)-3
: 42

After I have run the script, it looks like this:

(4*5)+5*(6-1)-5
! (4*5)+5*(6-1)-5
: 40

! (4*3)+3*(4-1)-3
: 42

I also want to be able to run the script onto documents that contains text, so we need to drop those lines from calculations, which leads to a little problem when we are dealing sines and logs in the front of our lines, to be dead sure it works, we start such lines with 1* so that the script understand that it is all about math.

Example:

1*sin(pi)
!1*sin(pi)
:0

I figured this was a good idea, but I hadn’t quite come around to implement it,
when I had a strike of luck:

While cleaning my Mac, for the upgrade to Maverick I came across a script I had downloaded for BBEdit: Calculate, that are made by Dennis Rande 3/3/7.
This works on a selection, but the core meat of it I can use in a script.

Here are his examples, but as of now, you may perform many calcuations at once
in a document. In fact, everything that looks like a calculation will be calculted, so be sure to have a character that is not amongst number and operators
before a calculation takes place (spaces and tabs) will cut it, so a ! or something is requisite in front of a “formula” you want to keep!

Further Examples:

"5+5/(3-1)"
"1*sqrt(3**2 + 4**2)"
"1*sin(3.1415926/2)"
"0x2FF7" -- converts hex to dec
"1*rand" -- returns a random number
"1*time" -- returns seconds since the Unix Epoch

Recommend short-cut key: cmd-K in TextEdit.
Enjoy


-- © 2013 McUsr after idea of Dennis Rande and put into public domain.
script mathEdit
	property funcNames : {"sin(", "cos(", "ln(", "exp(", "tan(", "asin(", "acos(", "atan(", "atan2(", ")"}
	local varlist
	
	set varlist to {{"pi", pi as text}, {"degrad", (pi / 180) as text}}
	
	# 	set end of varList to 
	set AppleScript's text item delimiters to ""
	global valchars, tids
	set valchars to "()1234567890.*-+/"
	local csep
	set csep to text 2 of ((1 / 2) as text)
	if csep is "," then adjustValidChars()
	
	tell application "TextEdit" to tell its document 1
		-- We do keep track of the index of the paragraph we are working on.
		set {Ix, Pc} to {1, count paragraphs of text of it}
		if last character of its text is not return then
			make new character at end of its text with data return
		end if
		if Pc = 0 then error number -128
		-- Do some math on the paragraphs that are applicable in doc 1!
		repeat
			local isFound
			set isFound to false
			-- we grab the current paragraph.
			local curMeat
			set curMeat to paragraph Ix as text
			
			-- sifts out whitespace
			set workMeat to my cleansed(curMeat, tids)
			-- Checks if there is something to be done with this par.
			set aVar to my lookForVariableDef(workMeat, tids)
			-- a variable is the only thing that is supposed to be on a line together with its value.
			if item 1 of aVar is not "" then
				set end of varlist to aVar
				set workMeat to ""
			else
				
				-- check to see if the workMeat contains any variable names to be substituted!
				set chunks to words of workMeat
				set isFound to false
				
				repeat with curChunk in chunks
					if first character of curChunk is not in valchars then
						repeat with kval in varlist
							local tst
							set tst to item 1 of kval
							if contents of curChunk is item 1 of kval then
								set isFound to true
								set workMeat to my changeText(workMeat, item 2 of kval, item 1 of kval)
								exit repeat
							end if
						end repeat
					end if
				end repeat
				
				if csep = "," then set workMeat to my changeText(workMeat, ".", ",")
			end if
			-- we figure out if it is a valid execution line.
			if my isALineWeCanCalculate(workMeat, funcNames, valchars) then
				log "We can"
				-- check to see if we are having a result line below, and if it is alike when the "!"
				-- is removed.
				local nextPar
				if Ix < Pc then
					if isFound and (Ix + 1) < Pc then
						set nextPar to my cleansed((paragraph (Ix + 2) as text), tids)
					else
						set nextPar to my cleansed((paragraph (Ix + 1) as text), tids)
					end if
					-- does it start with a bang?
					tell me -- !
						set bangPos to offset of "!" in nextPar
					end tell
					if bangPos > 0 then
						set nextPar to text (bangPos + 1) through -1 of nextPar
						if csep = "," then set nextPar to my changeText(nextPar, ".", ",")
					end if
				else
					set nextPar to ""
				end if
				
				if workMeat is not nextPar then
					local calcResult, calcError, failureMsg
					try
						tell me
							set calcResult to do shell script "perl -MMath::Trig -e 'print 0+" & workMeat & "'"
						end tell
					on error calcError
						set calcResult to ""
						if calcError ends with " at -e line 1." then set calcError to text 1 thru -15 of calcError
					end try
					
					if length of calcResult is 0 then
						set failureMsg to ": Could not evaluate \"" & curMeat & "\""
						if length of calcError is not 0 then
							set failureMsg to failureMsg & "- Reason: " & calcError & return
						end if
						my insertPar(failureMsg, Ix)
						set {Ix, Pc} to {Ix + 1, Pc + 1}
					else
						if csep = "," then set calcResult to my changeText(calcResult, ",", ".")
						my insertPar("! " & curMeat, Ix)
						set Ix to Ix + 1
						if isFound then
							if csep = "," then set workMeat to my changeText(workMeat, ",", ".")
							my insertPar("! " & workMeat, Ix)
							set Ix to Ix + 1
							my insertPar(": " & calcResult, Ix)
							set {Ix, Pc} to {Ix + 1, Pc + 3}
						else
							my insertPar(": " & calcResult, Ix)
							set {Ix, Pc} to {Ix + 1, Pc + 2}
						end if
					end if
				else if Ix < Pc then
					-- we'll skip those two!
					set Ix to Ix + 2
					if Ix ≥ Pc then exit repeat
				end if
				if Ix ≥ Pc then exit repeat
			else if Ix < Pc then
				-- look at the next one
				set Ix to Ix + 1
			else
				exit repeat
			end if
		end repeat
	end tell
	
	on isALineWeCanCalculate(lineToAssert, validFunctions, validChars)
		-- if the line we was to assert wasn't empty
		-- And the new line contains something valid after we have sifted it for
		-- functions that are valid in perl. Then we return true.
		if lineToAssert = "" then
			return false
		else
			local tids
			tell (a reference to AppleScript's text item delimiters)
				set {tids, contents of it} to {contents of it, my funcNames}
				set lineToAssert to text items of lineToAssert
				set contents of it to ""
				set lineToAssert to lineToAssert as text
				set contents of it to tids
			end tell
			if first character of lineToAssert is in validChars then
				return true
			else
				return false
			end if
		end if
	end isALineWeCanCalculate
	on lookForVariableDef(atextpar, tids)
		local probe
		set probe to words of atextpar
		if (length of probe > 1) and item 2 of probe = "=" then
			local theValue
			set {theValue, isValid} to {cleansed(text ((offset of "=" in atextpar) + 1) thru -1 of atextpar, tids), true}
			repeat with aChar in every character in theValue
				if aChar is not in "-1234567890.," then
					set isValid to false
					exit repeat
				end if
			end repeat
			if isValid then
				return {item 1 of probe, theValue}
			else
				return {"", 0}
			end if
		else
			return {"", 0}
		end if
	end lookForVariableDef
	
	on changeText(atextpar, new, old)
		local tids
		tell (a reference to AppleScript's text item delimiters)
			set tids to contents of it
			set contents of it to old
			set atextpar to text items of atextpar
			set contents of it to new
			set atextpar to atextpar as text
			set contents of it to tids
		end tell
		return atextpar
	end changeText
	
	on cleansed(atextpar, tids)
		tell (a reference to AppleScript's text item delimiters)
			set contents of it to {space, tab, return, linefeed}
			set atextpar to text items of atextpar
			set contents of it to ""
			set atextpar to atextpar as text
			set contents of it to tids
		end tell
		return atextpar
	end cleansed
	
	on insertPar(thePar, Ix)
		local probe
		tell application "TextEdit"
			tell text of its front document
				set font_nm to (font of paragraph Ix)
				set font_sz to (size of its paragraph Ix)
				local ofs
				-- if we are getting a line ending with return
				make new paragraph at after its paragraph Ix with data thePar & return
				tell (paragraph (Ix + 1))
					set font of its every character to font_nm
					set size of its every character to font_sz
				end tell
			end tell
		end tell
	end insertPar
	
	on adjustValidChars()
		tell (a reference to AppleScript's text item delimiters)
			set {tids, contents of it} to {contents of it, "."}
			set {my valchars, contents of it} to {text items of my valchars, ","}
			set {my valchars, contents of it} to {my valchars as text, tids}
		end tell
	end adjustValidChars
end script
tell mathEdit to run



It now returns the result with the correct decimal separator, according to locale. :confused:

Hello.

I have increased the robustness of the text-output a little, so it comes out like I want it to, also in edge-cases.

Hello.

I fixed one case, while I ruined the other, now both should be fixed.

:slight_smile: Fixed an exit condition of the repeat loop, that “suddenly kicked in”.

I’m sorry, but now it should be thoroughly tested in between fixes.

Hello.

I have added the ability to “define” variables.

Hello.

I fixed setting new paragraph count.

Anyways, I think it works great as it works now, the ability to perform calculations of formulae in Text Edit with variables! That is something I have missed at least.

Enjoy!

Hello.

I have fixed an unforeseen side-effect of duplicating calculations when I introduced variables. This is now fixed.

Hello.

I added pi, and degrad (pi/180) as a constants, to round it off for now.

Hello.

I fixed an issue due to the localized decimalseparator not being changed before comparing with an old calculation.

This led to extranous output, when there were decimal separators in the formulas, and is now fixed.

Hello.

:slight_smile: I have added the Math::Trig module to the perl command line, so that acos, asin etc. works, (together with cosh tanh etc.) You just add modules by -MMainModule::SubModule, maybe after you have googled for something.

Oddly enough, sin, cos and tan, is included in the “standard” math module.

Hello

I’ll just state it here: it was never my intention to use more than say 5-6 variables with this. and it is one pass (you can’t have any nested assignment of functions, and the script reads the text files, and acts from top to bottom of it in one pass, so any variables must be initialized before they are used), so you can’t really assign the result of functions to variables.

Should you reach this stage, that it is either unusable for your purpose, or too slow, due to too many lines, then in the first case I suggest you move the whole thing into AppleScript and device some kind of logging for what variable values, caused what results. Your calculations are then so big, that they don’t classify as a side-task anyway. If it is just too slow, because of too many variables, or calculations, then you should try to split your calculation file into two if that is an option, and remove any results/calculations you don’t need anymore to that second file, it is funny very often the same calculations pop up again and again.

I hope the script proves to be useful anyway, as a middle man between spotlight, calculator, AppleScript and Spreadsheets, save Mathlab, Maple, and Mathematica.

It is meant to be really easy and fast to use, and in my own I eyes at least I succeeded in that.

If you have guessed by now that the script won’t be optimized in any way, then you are right.:slight_smile:

Thanks.

Hello.

Starting off lines with 1* to be able to use functions defined in perl, wasn’t something I liked to look at after all.

Now the input/output are looking like this:

I might do some more cosmetic changes in the near future.

I had made the variable list into a property, which was saved with the script, so that changed variables wouldn’t be considered, since they would be added after the previous one. It’s fixed.