Converting exponential numbers to real numbers

Hi, chaps.

Thanks for your interest. My numToStr handler’s here in ScriptBuilders. I haven’t looked at it or thought about the problem for a couple of years, so I can’t remember why it’s done the way it is. It’s a general purpose number-to-string converter. You don’t have to know in advance if the number’s exponential or otherwise. It works with reals, integers, positives, negatives, and numbers between -1 and 1. It also contains a few shakes to iron out the floating-point difficulties that come with certain numbers “ such as 8483.38 and its similar, near neighbours. (Try compiling a few and you’ll see what I mean.)

I think there’s some way of formatting numeric strings in Unix, but I haven’t looked into it. The Satimage OSAX has a format command that allows some reasonably short code:

on numToStr(n)
	set n to n as number
	set ns to (format n into "############.###########") -- Needs Satimage OSAX.
	if (n's class is integer) then set ns to text 1 thru -3 of ns

	return ns
end numToStr

And I’ve finally fixed mine for positive exponents, positive numbers:


getTfromN(9.87654321E+5)

to getTfromN(X)
	set n to X as text
	if n contains "+" then
		set O to offset of "+" in n -- find the exponent
		set p to text (O + 1) thru -1 of n -- the exponent value
		tell n to set T to text item 1 & text items 3 thru (O - 2) -- get the numbers
		tell T to set nT to text 1 thru (p + 1) & "." & text (p + 2) thru -1 -- stick in "."
		return nT
	else
		return n
	end if
end getTfromN

Hi Adam,

I dared to add the function to add zeros if the decimal places are less than the exponent :wink:

getTfromN(9.8765E+5)

to getTfromN(X)
	set n to X as text
	if n contains "+" then
		set O to offset of "+" in n -- find the exponent
		set p to text (O + 1) thru -1 of n -- the exponent value
		tell n to set T to text item 1 & text items 3 thru (O - 2) -- get the numbers
		if ((p as integer) + 1) ≥ (count T) then
			set d to ((p as integer) + 1) - (count T)
			set nT to T
			repeat d times
				set nT to nT & "0"
			end repeat
		else
			tell T to set nT to text 1 thru (p + 1) & "." & text (p + 2) thru -1 -- stick in "."
		end if
		return nT
	else
		return n
	end if
end getTfromN

:slight_smile: Thanks. Didn’t think of it. Tunnel vision getting the decimal point in the right place. (for you a comma would be appropriate)

I know this topic has been on the shelf a while but I was inspired to give it a go. This includes a super-simple shell script conversion of exponential numbers in line one, and then some if-thens for placing some commas when the numbers get big. The largest number it spit out during tests with the included random number generator was this…

“‘2,012,009,890,299,000,053,078,911,865,592,313,973,011,672,255,274,113,821,952,938,596,545,074,236,404,122,939,660,629,558,622,321,119,207,482,003,043,218,878,507,374,303,208,823,402,681,339,224,450,318,609,019,554,750,335,013,720,074,481,739,669,129,534,518,591,488.00’”

If there’s a good reason not to do it this way, let me know. (I should note that it currently rounds off to two spaces but that adjustable**)


------------------------GENERATE A RANDOM NUMBER------------------------
set nmbr to rndm()
on rndm()
	try
		set {intgrs, oprtrs} to {{1, 2, 3, 4, 5, 6, 7, 8, 9, pi}, {"+", "-", "*", "÷", "^"}}
		set {a, b, c} to {some item of intgrs, some item of intgrs, some item of intgrs}
		set {x, y, z} to {some item of oprtrs, some item of oprtrs, some item of oprtrs}
		set scpt to "  " & a & "     " & x & "    " & b & "   " & y & "   " & c & "  " & z & " " & some item of {a, b, c}
		set nmbr to run script scpt
	on error err ----->When result is too large this error handler will unnecessarily show you the problem.
		display dialog "Oops! " & err & return & scpt with title "skip this attempt!"
		return my rndm()
	end try
end rndm

------------------------CONVERT THE NUMBER TO READABLE TEXT------------------------
set n to do shell script "printf \"%1.2f\" " & (nmbr as text) ---This converts scientific notation to real numbers.**change the '.2' to '.3' for '.xxx', or to '.6' for  '.xxxxxx', etc.
set {strng, thrd} to {"", 0}
repeat with chr in (reverse of (every character of n))
	set {strng, thrd} to {chr & strng, thrd + 1}
	if (length of strng) ≥ (offset of "." in ((reverse of (every character of n)) as text)) then --add commas!
		if (chr as text is ".") then set thrd to 0
		if (length of strng > 2) and ((characters 1 thru 2 of strng) as text) is ".," then set strng to "." & (characters 3 thru -1 of strng)
		if (thrd = 3) then set {strng, thrd} to {"," & strng, 0}
	end if
end repeat
if ((characters 1 thru 2 of strng) as text) is "-," then set strng to "-" & (characters 3 thru -1 of strng) as string -->prevents '-,xxx'
if character 1 of strng is "," then set strng to (characters 2 thru -1 of strng) as string -->prevents ',xxx'
return strng


Model: Mac Pro, Yosemite
AppleScript: 2.7
Browser: Safari 601.2.7
Operating System: macOS 10.14

Hi Mr. Science.

Looking at your code quickly, the two things which spring to mind immediately are that different countries use different decimal point characters and thousands separators and that coercions of lists to text should be done with AppleScript’s text item delimiters explicitly set (to “” in this script, but substring extractions like ((characters 1 thru 2 of strng) as text) should instead be done with (text 1 thru 2 of strng)).

Later: Here’s a first attempt at an ASObjC solution:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

------------------------GENERATE A RANDOM NUMBER------------------------
set nmbr to (random number from -9.9E+24 to 9.9E+24) / ((random number (9.9E+12 - 1)) + 1)


------------------------CONVERT THE NUMBER TO READABLE TEXT------------------------
set decimalPlaces to 2 -- Adjust to taste.

set theFormatter to current application's class "NSNumberFormatter"'s new()
tell theFormatter to setNumberStyle:(current application's NSNumberFormatterDecimalStyle)
tell theFormatter to setMinimumFractionDigits:(decimalPlaces)
tell theFormatter to setMaximumFractionDigits:(decimalPlaces)
tell theFormatter to setLocalizesFormat:(true)
return (theFormatter's stringFromNumber:(nmbr)) as text

Hello Nigel

In fact, the error dialog is issued every time when the value pi is used because when the main string is built, the decimal separator in pi is replaced - here in France - by a comma.

Here is what I got :

tell current application
run script " 7 - 8 ^ 3,14159265359 ^ 3,14159265359"
→ error “« « , » » ne peut pas se trouver après « numéro ».” number -2740
end tell
tell application “Script Editor”
display dialog “Oops! « « , » » ne peut pas se trouver après « numéro ».
7 - 8 ^ 3,14159265359 ^ 3,14159265359” with title “skip this attempt!”
→ {button returned:“OK”}
end tell
tell current application
run script " 3,14159265359 + 9 ^ 1 - 1"
→ error “« « , » » ne peut pas se trouver après « numéro ».” number -2740
end tell
tell application “Script Editor”
display dialog “Oops! « « , » » ne peut pas se trouver après « numéro ».
3,14159265359 + 9 ^ 1 - 1” with title “skip this attempt!”
→ {button returned:“OK”}
end tell

Yvan KOENIG running High Sierra 10.13.6 in French (VALLAURIS, France) samedi 9 février 2019 12:05:04

Thanks, Yvan. So pi needs to be left out of the list of — er — ‘intgrs’. :wink: I’ll see to that immediately. (Edit: In fact I’ve now replaced the random number generation process completely.)

Hello Nigel.

Now, when I get an error, it’s really because the number is too large.

tell current application
run script " 9 ^ 7 ^ 9 + 9"
→ error “Le résultat d’une opération numérique était trop élevé.” number -2702
end tell
tell application “Script Editor”
display dialog “Oops! Le résultat d’une opération numérique était trop élevé.
9 ^ 7 ^ 9 + 9” with title “skip this attempt!”
→ {button returned:“OK”}

When I enter the formula in Numbers, I get : 1310020508637620000000000000000000000000000000000000000000000

In both cases the problem is not the conversion of the number, it’s its creation.

Yvan KOENIG running High Sierra 10.13.6 in French (VALLAURIS, France) samedi 9 février 2019 12:44:52

What about:


-- Generate a random number (borrowed from Nigel Garvey's reply:
set nmbr to (random number from -9.9E+24 to 9.9E+24) / ((random number (9.9E+12 - 1)) + 1)

-- Return the number in both its original scientific notation
-- and a string representing its full decimal notation
{nmbr, nmbr as yards as string}

It’s a neat trick, but it doesn’t include thousands separators, and it doesn’t give you any control over the number of decimals.

Even if I ask it to display as meters, I gel a decimal period while it’s supposed to be a comma here in France.

{-1.00364941532576E+12, “-1003649415325.764”}

Yvan KOENIG running High Sierra 10.13.6 in French (VALLAURIS, France) dimanche 10 février 2019 11:58:15

When the input’s a very precise real, the different methods can produce different levels of precision in their results. The following script is a version of the ASObjC script from post #15 which doesn’t impose a fixed number of decimal places and which returns a list containing the original number as displayed in an editor, the ASObjC result, and the as-yards-as-text result:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

------------------------GENERATE A RANDOM NUMBER------------------------
set nmbr to (random number from -9.9E+24 to 9.9E+24) / ((random number (9.9E+12 - 1)) + 1)

------------------------CONVERT THE NUMBER TO READABLE TEXT------------------------
set theFormatter to current application's class "NSNumberFormatter"'s new()
tell theFormatter to setNumberStyle:(current application's NSNumberFormatterDecimalStyle)
tell theFormatter to setMinimumFractionDigits:(0)
tell theFormatter to setMaximumFractionDigits:(100)
tell theFormatter to setLocalizesFormat:(true)
return {nmbr, (theFormatter's stringFromNumber:(nmbr)) as text, nmbr as yards as text}

Ignoring the formatting, I’m finding in Mojave that the ASObjC result usually contains all the same digits as the displayed original number, but may occasionally end with a group of digits that can be rounded to the last digit of the AS version. On the other hand, the as-yards-as-text result more commonly ends with a roundable group and only occasionally has all the same digits as the displayed original.

Very occasionally, an as-yards-as-text result will appear to break the “half-step to nearest even” rounding rule:

This is probably because the results are all being rounded from an even more precise figure like 220109764541.18747, which rounds up to 220109764541.1875, but down to 220109764541.187.

But it could be that the “extra precision” is a manifestation of the imprecision of reals! I don’t know.

Here’s a script containing both vanilla and ASObjC handlers. With the given parameters, they return identical results. But with ridiculously high input numbers, or with ten or more decimal places, the vanilla one gives the impression of being more precise:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

------------------------GENERATE A RANDOM NUMBER------------------------
set nmbr to (random number from -9.9E+24 to 9.9E+24) / ((random number (9.9E+12 - 1)) + 1)

------------------------CONVERT THE NUMBER TO READABLE TEXT------------------------
set decimalPlaces to 2 -- Adjust to taste.

return {numberToTextAS(nmbr, decimalPlaces), numberToTextASObjC(nmbr, decimalPlaces)}

-- Handler with vanilla code.
on numberToTextAS(nmbr, decimalPlaces)
	set decimalPlaces to decimalPlaces div 1 -- In case the user's a smartarse.
	-- Get (ie. guess) the user's decimal point and thousands separator.
	if (character 2 of (1 / 2 as text) is ".") then
		set {decimalPoint, separator} to {".", ","}
	else
		set {decimalPoint, separator} to {",", " "} -- Not guaranteed to be correct for all non-point countries.
	end if
	-- Get the number's sign and work with positives for convenience.
	if (nmbr < 0) then
		set {nmbr, sign} to {-nmbr, "-"}
	else
		set {nmbr, sign} to {nmbr, ""}
	end if
	-- Get the number's whole-number and fractional parts.
	set {wholeNumberPart, fractionalPart} to {nmbr div 1, nmbr mod 1}
	
	set collector to {}
	-- Add 1 to the fractional part (to preserved any leading zeros) and left-shift it by the required number of decimal places.
	set fractionalPart to (1 + fractionalPart) * (10 ^ decimalPlaces)
	-- Round the result to the nearest integer. It may be too big to coerce to integer directly at this stage.
	set fractionalPart to fractionalPart div 1 + (fractionalPart mod 1 as integer)
	if (decimalPlaces > 0) then
		-- If it contains more than eight digits, coerce eight of them to text at a time and add the results to the collector.
		repeat until (fractionalPart < 100000000)
			set beginning of collector to text 2 thru -1 of (100000000 + fractionalPart mod 100000000 as integer as text)
			set fractionalPart to fractionalPart div 100000000
		end repeat
		-- Coerce and collect whatever's left over at the end, prepending a decimal point character.
		set fractionalPart to fractionalPart as text
		set beginning of collector to decimalPoint & text 2 thru -1 of fractionalPart
		-- If the 1 added above increased to 2 during the rounding, add the carry to the whole-number part.
		if (fractionalPart begins with "2") then set wholeNumberPart to wholeNumberPart + 1
	else
		-- Deal with any carry from the rounding this way.
		if (fractionalPart as integer is 2) then set wholeNumberPart to wholeNumberPart + 1
	end if
	
	-- Now add text versions of each group of three whole-number digits to the collector, interspersed with the separator character.
	repeat until (wholeNumberPart < 1000)
		set beginning of collector to text 2 thru -1 of (1000 + wholeNumberPart mod 1000 div 1 as text)
		set beginning of collector to separator
		set wholeNumberPart to wholeNumberPart div 1000
	end repeat
	set beginning of collector to wholeNumberPart
	
	-- Coerce the collector's contents to a single text and return the result.
	set astid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to ""
	set numberText to sign & collector
	set AppleScript's text item delimiters to astid
	
	return numberText
end numberToTextAS

-- Handler with ASObjC code.
on numberToTextASObjC(nmbr, decimalPlaces)
	set decimalPlaces to decimalPlaces div 1
	set theFormatter to current application's class "NSNumberFormatter"'s new()
	tell theFormatter to setNumberStyle:(current application's NSNumberFormatterDecimalStyle)
	tell theFormatter to setMinimumFractionDigits:(decimalPlaces)
	tell theFormatter to setMaximumFractionDigits:(decimalPlaces)
	tell theFormatter to setLocalizesFormat:(true)
	
	return (theFormatter's stringFromNumber:(nmbr)) as text
end numberToTextASObjC

Thank you Nigel

“Funny” feature, If I inserts the result of the first handler in cell A1 of a table in Numbers then inserts the formula = A1 * 1 in cell B1, I get the value returned bt the second handler.

18 409 688 740 693,76 → 18409688740693,8
In an other attempt,
-20 843 851 413 465,18 → -20843851413465,18 while the 2nd handler returned “-20 843 851 413 465,20”

I tried to do the same in Libre Office where both values were displaid as : 18409688740693,8

Are we reaching the limits of doublevalue() ?

Yvan KOENIG running High Sierra 10.13.6 in French (VALLAURIS, France) dimanche 10 février 2019 16:18:10

Nigel,

Doubles are supposed to be accurate to be between 15 and 17 decimal digits. Would trusting only the first 15 digits explain your results?

Here’s a new top to your script, in case it makes things any clearer.

------------------------GENERATE A RANDOM NUMBER------------------------
set x to (random number from -9.9E+24 to 9.9E+24)
set y to ((random number (9.9E+12 - 1)) + 1)
set nmbr to x / y
set decNmbr to nmbrFromDecimals(x, y)
set decNmbr2 to numberToDecimalNumber(nmbr)

------------------------CONVERT THE NUMBER TO READABLE TEXT------------------------
set decimalPlaces to 2 -- Adjust to taste.

return {numberToTextAS(nmbr, decimalPlaces), numberToTextASObjC(nmbr, decimalPlaces), numberToTextASObjC(decNmbr, decimalPlaces), decNmbr's stringValue() as text, numberToTextASObjC(decNmbr2, decimalPlaces), decNmbr2's stringValue() as text, nmbr}

on numberToDecimalNumber(nmbr)
	return current application's NSDecimalNumber's numberWithDouble:nmbr
end numberToDecimalNumber

on nmbrFromDecimals(x, y)
	set xd to current application's NSDecimalNumber's numberWithDouble:x
	set yd to current application's NSDecimalNumber's numberWithDouble:y
	return xd's decimalNumberByDividingBy:yd
end nmbrFromDecimals

Hi Shane.

I see what you mean! All the approaches begin to produce different figures from or after the fifteenth digit. And coercing a 14- or 15-digit AppleScript E number directly to text produces an E number string rounded to thirteen digits!

The NSNumberFormatter approach seems to be the most reliable for the current purpose, with NSDecimalNumber being used for calculations where the precision may go beyond the capability of AS reals and where that precision is actually needed in the final string.

Ironically, the one real-world place where doubles lack sufficient precision in AppleScript is in accurately representing large integers.

I was just looking up the same topic (not sure why I never did in the last 15 years).

I came upon at a much easier solution on Ask Different - it looks like we can just convert a number to a measurement then to string:

1.2345E+9 as inches as string

will return “1234500000”.

Any measurement can be used (kilograms, gallons etc.)

Looks like it saves a few lines of code.

Or is it well known by now?

To give proper credit credit - here’s the link (the answer at the bottom):

https://apple.stackexchange.com/questions/304371/how-do-i-prevent-applescript-from-using-short-numbers

You’ll find it back in message #19 of this thread.

oops missed it - thanks Shane