Tuesday, July 17, 2018

You are not logged in.

## #1 2015-08-11 10:21:39 pm

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 280

### Rounding numbers with arbitrary precision

This handler utilizes the shell calculator bc to round a number or numerical expression in any of five different ways, mimicking Applescript's rounding commands but without Applescript's bit precision limit of 2^52 - 1 (approximately 15 or 16 digits).  It returns the rounded number as well as its text string representation in decimal and exponential forms.

The input argument is a record of the form {theNumber:xxx, roundingMethod:xxx, nDecimalPlaces:xxx}, where:

theNumber may be any integer, real number, or text string whose content is a valid number or bc calculator numerical expression

roundingMethod is one of the following text strings:
"school"  (the default value if omitted from the input argument)
- Applescript's "rounding as taught in school"
- Rounds the fractional portion away from zero if â‰¥ 0.5 and toward zero if < 0.5
"nearest"
- Applescript's "rounding to nearest"
- Rounds the fractional portion away from zero if > 0.5, or = 0.5 and the preceding digit is odd
- Rounds the fractional portion toward zero if < 0.5, or = 0.5 and the preceding digit is even
"truncate"
- Applescript's "rounding toward zero"
- Rounds the fractional portion toward zero (i.e., truncates the fractional portion)
"down"
- Applescript's "rounding down"
- Rounds a non-zero fractional portion toward minus infinity
"up"
- Applescript's "rounding up"
- Rounds a non-zero fractional portion toward plus infinity

nDecimalPlaces is an integer â‰¥ 0 indicating the number of decimal places to which the number is rounded
- The default value is 0 if omitted from the input argument, which rounds to an integer value

The value returned by the handler is a record of the form {roundedNumber:xxx, decimalForm:xxx, exponentialForm:xxx}, where:

roundedNumber is an integer or real number, or the text string "too large" if the value exceeds Applescript's maximum allowed value of 1.79769313486231 * 10^308
- A number exceeding Applescript's bit precision limit of 2^52 - 1 = 4.50359962737 * 10^15 (approximately 15 or 16 digits) will likely be an inexact value

decimalForm is a text string representation of the exact value of the rounded number in decimal form with no limits on the number's size or precision

exponentialForm is a text string representation of the exact value of the rounded number in exponential form with no limits on the number's size or precision

The returned values will be set to null if the input number or expression does not resolve to a valid number.

If more than 1000 decimal places of precision is needed, increase the value of maximumScale in the assignment statement "set maximumScale to 1000"   :-)

Examples:
tell roundNumber({theNumber:0.12344, roundingMethod:"up", nDecimalPlaces:4}) to return {its roundedNumber, its decimalForm, its exponentialForm}
--> {0.1235, "0.1235", "1.235E-1"}
tell roundNumber({theNumber:1.5, roundingMethod:"nearest", nDecimalPlaces:0}) to return {its roundedNumber, its decimalForm, its exponentialForm}
--> {2, "2", "2.0E+0"}
tell roundNumber({theNumber:2.5, roundingMethod:"nearest", nDecimalPlaces:0}) to return {its roundedNumber, its decimalForm, its exponentialForm}
--> {2, "2", "2.0E+0"}
tell roundNumber({theNumber:3.5, roundingMethod:"nearest", nDecimalPlaces:0}) to return {its roundedNumber, its decimalForm, its exponentialForm}
--> {4, "4", "4.0E+0"}
tell roundNumber({theNumber:4.5, roundingMethod:"nearest", nDecimalPlaces:0}) to return {its roundedNumber, its decimalForm, its exponentialForm}
--> {4, "4", "4.0E+0"}
tell roundNumber({theNumber:"1.000000000000000000005", roundingMethod:"down", nDecimalPlaces:20}) to return {its roundedNumber, its decimalForm, its exponentialForm}
--> {1, "1", "1.0E+0"}
tell roundNumber({theNumber:"1.000000000000000000005", roundingMethod:"up", nDecimalPlaces:20}) to return {its roundedNumber, its decimalForm, its exponentialForm}
--> {1, "1.00000000000000000001", "1.00000000000000000001E+0"}  too many digits for Applescript to represent exactly!!
tell roundNumber({theNumber:"1.000000000000000000005", roundingMethod:"school", nDecimalPlaces:20}) to return {its roundedNumber, its decimalForm, its exponentialForm}
--> {1, "1.00000000000000000001", "1.00000000000000000001E+0"}  too many digits for Applescript to represent exactly!!
tell roundNumber({theNumber:"1 + 1", roundingMethod:"school", nDecimalPlaces:0}) to return {its roundedNumber, its decimalForm, its exponentialForm}
--> {2, "2", "2.0E+0"}
tell roundNumber({theNumber:"2e-901+3e-901", roundingMethod:"school", nDecimalPlaces:900}) to return {its roundedNumber, its decimalForm, its exponentialForm}
--> {"too large", "0.[...899 0's...]1", "1.0E-900"}
tell roundNumber({theNumber:"2e-901+3e-901", roundingMethod:"down", nDecimalPlaces:900}) to return {its roundedNumber, its decimalForm, its exponentialForm}
--> {0, "0", "0.0E+0"}
tell roundNumber({theNumber:"1e900+1e-900", roundingMethod:"school", nDecimalPlaces:900}) to return {its roundedNumber, its decimalForm, its exponentialForm}
--> {"too large", "1[...900 0's...].[...899 0's...]1", "1.[...1799 0's...]1E+900"}

#### Applescript:

on roundNumber(inputArgument)
-- Constant
set maximumScale to 1000
-- Process the input arguments and set default values for missing properties
tell (inputArgument & {roundingMethod:"school", nDecimalPlaces:0}) to set {theNumber, roundingMethod, nDecimalPlaces} to {its theNumber, its roundingMethod, its nDecimalPlaces}
if theNumber's class = text then
set theNumberAsText to theNumber
else if theNumber's class is in {integer, real} then
try
-- Generates an error message containing a text representation of the number with greater bit precision than a simple coercion to text
-- E.g.: 2^45 as text -> "3.518437208883E+13"; || of 2^45 method -> "3.5184372088832E+13", which is exactly correct
|| of theNumber
on error errorMessage
set {o1, o2} to {6 + (offset of "|| of " in errorMessage), -2}
set theNumberAsText to errorMessage's text o1 thru o2
end try
else
error "The input number must be an integer, real number, or text string."
end if
set nDecimalPlaces to nDecimalPlaces as integer
if nDecimalPlaces < 0 then set nDecimalPlaces to 0
-- Execute a shell script that utilizes the bc calculator to round the input number or expression into decimal form with arbitrary bit precision and then output the decimal form's components
set bcCalculatorOutput to do shell script "
## Get the input number or expression, and convert any terms in E exponential format to bc calculator exponential format (e.g., '1.2E+3' -> '1.2*10^3')

input_expression=\$(sed -E 's/([0-9]+)[eE]([+]?([0-9]+)|([-][0-9]+))/\\1*10^\\3\\4/g' <<<"
& theNumberAsText's quoted form & ")

## Create an error test variable that is set to '1/0' if the input number or expression contains undeclared variable names, or to the empty string if it does not
## This step gets around bc's behavior of assigning a default value of 0 to undeclared variables rather than flagging them as invalid

error_test=\$(sed -En '
H
\${
x
s/[^a-z_]*(break|continue|else|for|halt|if|length|quit|read|return|scale|sqrt|while)[^a-z_]*//g
s/[^a-z_]*(a|c|e|j|l|s)[(]([^)]*)[)]/\\2/g
/[a-z_]/!s/.*//p
/[a-z_]/s/.*/1\\/0/p
}
' <<<\"\$input_expression\")

## Run the bc calculator script

bc -l <<<\"

## Run the error test which, if positive, causes bc to send an error message to standard error and thus flags the input number as invalid

\$error_test

## Get the input number at full scale

max_scale="
& maximumScale & "
scale=max_scale
input_number=\$input_expression

## Get the nDecimalPlaces and roundingMethod input argument values

n_decimal_places="
& nDecimalPlaces & "
rounding_method="
& (offset of roundingMethod in "schoolÂ¦nearestÂ¦truncateÂ¦downÂ¦up") & "

## Extract the sign of the input number, then transform the number to its absolute value with the decimal point shifted nDecimalPlaces to the right

number_sign=1-2*(input_number<0)
shifted_number=number_sign*(10^n_decimal_places)*input_number

## Round the input number according to the method specified by the input argument by adding 0, 0.5, or 1 as appropriate to the positive-valued shifted number

if (rounding_method==1) { ## school
shifted_number=shifted_number+0.5
} else if (rounding_method==8) { ## nearest
scale=0
final_digit=(shifted_number%10)/1
final_digit_is_odd_number=final_digit%2
fractional_part_after_final_digit=shifted_number-(shifted_number/1)
scale=max_scale
if ((fractional_part_after_final_digit>0.5) || ((fractional_part_after_final_digit==0.5) && (final_digit_is_odd_number==1))) shifted_number=shifted_number+0.5
} else if (rounding_method==16) { ## truncate
## do nothing
} else if (rounding_method==25) { ## down
if (number_sign==1) {
## do nothing
} else if (number_sign==-1) {
scale=0
fractional_part=shifted_number-(shifted_number/1)
if (fractional_part>0) shifted_number=shifted_number+1
}
} else if (rounding_method==30) { ## up
if (number_sign==1) {
scale=0
fractional_part=shifted_number-(shifted_number/1)
if (fractional_part>0) shifted_number=shifted_number+1
} else if (number_sign==-1) {
## do nothing
}
}

## Remove the fractional part of the shifted number, shift the decimal point back to its original position (nDecimalPlaces to the left), and restore the number sign

scale=0
shifted_number=shifted_number/1
scale=n_decimal_places
rounded_number=number_sign*shifted_number/(10^n_decimal_places)

## Print the rounded number in decimal form

print rounded_number

## Capture any error message (2>&1), and remove any reverse slashes or linefeeds from bc's output of the rounded number

\" 2>&1 | tr -d '\\\\\\n' | sed -E '

## Output the following components of the rounded number on separate lines of output:
## Number sign
## First nonzero digit of the whole number part
## Remaining digits of the whole number part
## Leading zeros of the fractional part
## First nonzero digit of the fractional part
## Remaining digits of the fractional part
## Alternatively, if the input number or expression is invalid (i.e., the bc output contains an error message), flag it as such by outputting the letter 'I' followed by five empty lines

/[^+-.0-9]/s/.*/I/
/^[I+-]/!s/^/+/
/[.]/!s/\$/./
s/([+-])0*[.]/\\1./
s/0+\$//
s/([+]|([I-]))([1-9]?)([0-9]*)[.](0*)([1-9]?)([0-9]*)/\\2\\"
& linefeed & "\\3\\" & linefeed & "\\4\\" & linefeed & "\\5\\" & linefeed & "\\6\\" & linefeed & "\\7/'
"

-- Construct the text representation of the rounded number in decimal and exponential forms from the components parts
-- Also, coerce the decimal form into an Applescript integer or real number, or assign the value "too large" if the number exceeds Applescript's maximum allowed value of 1.79769313486231 * 10^308
-- If the number is invalid, set the results to the null value instead
set {numberSign, wholeNumberFirstNonzeroDigit, wholeNumberRemainingDigits, fractionLeadingZeros, fractionFirstNonzeroDigit, fractionRemainingDigits} to bcCalculatorOutput's paragraphs
set {roundedNumber, decimalForm, exponentialForm} to {null, null, null}
if numberSign â‰  "I" then -- i.e., if the item is a valid number
set decimalVersion to wholeNumberFirstNonzeroDigit & wholeNumberRemainingDigits & "." & fractionLeadingZeros & fractionFirstNonzeroDigit & fractionRemainingDigits
tell decimalVersion to if it starts with "." then set decimalVersion to "0" & it
tell decimalVersion to if it ends with "." then set decimalVersion to text 1 thru -2
tell (numberSign & decimalVersion)
try
set roundedNumber to it as number
on error
set roundedNumber to "too large" -- if the number exceeds Applescript's maximum allowed value of 1.79769313486231 * 10^308
end try
set decimalForm to it
end tell
if wholeNumberFirstNonzeroDigit = "" then
set {theMantissa, theExponent} to {fractionFirstNonzeroDigit & "." & fractionRemainingDigits, (-1 * ((fractionFirstNonzeroDigit's length) + (fractionLeadingZeros's length))) as text}
else
set {theMantissa, theExponent} to {wholeNumberFirstNonzeroDigit & "." & wholeNumberRemainingDigits & fractionLeadingZeros & fractionFirstNonzeroDigit & fractionRemainingDigits, wholeNumberRemainingDigits's length as text}
end if
tell theMantissa to if it starts with "." then set theMantissa to "0" & it
tell theMantissa to if it ends with "." then set theMantissa to it & "0"
tell theExponent to if it does not start with "-" then set theExponent to "+" & it
set exponentialForm to numberSign & theMantissa & "E" & theExponent
end if
-- Return the rounded number in three different forms: Applescript number, text representation in decimal form, and text representation in exponential form
return {roundedNumber:roundedNumber, decimalForm:decimalForm, exponentialForm:exponentialForm}
end roundNumber

Last edited by bmose (2015-08-11 11:47:19 pm)

Filed under: Arbitrary, Rounding, precision, bc

Offline

## #2 2015-08-12 12:38:40 am

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 5408

### Re: Rounding numbers with arbitrary precision

Nice. If you wanted to set your range a little lower -- coping with numbers with a mantissa of up to 38 digits and an exponent from -128 to 127 -- You can do something similar using AppleScriptObjC (under Mavericks or later):

#### Applescript:

use AppleScript version "2.3.1"
use scripting additions
use framework "Foundation"

on roundNumber(inputArgument)
tell (inputArgument & {roundingMethod:"school", nDecimalPlaces:0}) to set {theNumber, roundingMethod, nDecimalPlaces} to {its theNumber, its roundingMethod, its nDecimalPlaces}
if roundingMethod = "school" then
set roundingMethod to (current application's NSRoundPlain)
else if roundingMethod = "up" then
set roundingMethod to (current application's NSRoundUp)
else if roundingMethod = "down" then
set roundingMethod to (current application's NSRoundDown)
else if roundingMethod = "nearest" then
set roundingMethod to (current application's NSRoundBankers)
end if
-- make decimal number
if theNumber's class = text then
set theNSDecimalNumber to current application's NSDecimalNumber's decimalNumberWithString:theNumber
else
set theNSDecimalNumber to current application's NSDecimalNumber's numberWithDouble:theNumber
end if
-- round it
set theHandler to current application's NSDecimalNumberHandler's decimalNumberHandlerWithRoundingMode:roundingMethod scale:nDecimalPlaces raiseOnExactness:false raiseOnOverflow:true raiseOnUnderflow:true raiseOnDivideByZero:true
set theRoundedNumber to theNSDecimalNumber's decimalNumberByRoundingAccordingToBehavior:theHandler
-- get AS number
try
set roundedNumber to theRoundedNumber's doubleValue()
on error
set roundedNumber to "too large"
end try
-- get formatted strings
set theFormatter to current application's NSNumberFormatter's alloc()'s init()
theFormatter's setNumberStyle:(current application's NSNumberFormatterDecimalStyle)
theFormatter's setMinimumFractionDigits:nDecimalPlaces
theFormatter's setMaximumFractionDigits:nDecimalPlaces
theFormatter's setHasThousandSeparators:false
set decimalForm to (theFormatter's stringFromNumber:theRoundedNumber) as text
theFormatter's setNumberStyle:(current application's NSNumberFormatterScientificStyle)
theFormatter's setFormat:"0.#E+0;0.#E-0"
set exponentialForm to (theFormatter's stringFromNumber:roundedNumber) as text
return {roundedNumber:roundedNumber, decimalForm:decimalForm, exponentialForm:exponentialForm}
end roundNumber

It might be useful if you're processing a lot of numbers, because it's considerably faster.

Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/
latenightsw.com

Offline

## #3 2015-08-12 05:48:00 am

DJ Bazzie Wazzie
Member
From:: the Netherlands
Registered: 2004-10-20
Posts: 2776
Website

### Re: Rounding numbers with arbitrary precision

Very useful script Bmose.

Small note, the AppleScript rounding value is always "too large" because there are more countries using a decimal comma instead of a decimal point. Coercing string containing an decimal point will return into an error or will be handled as an thousand separator. One way to solve that is using using the run script command. Replacing the tell (numberSign & decimalVersion) block with the code below and it will work no matter what kind of number formatter your system  uses.

#### Applescript:

try
set roundedNumber to run script numberSign & decimalVersion
on error
set roundedNumber to "too large" -- if the number exceeds Applescript's maximum allowed value of 1.79769313486231 * 10^308
end try
set decimalForm to numberSign & decimalVersion

Last edited by DJ Bazzie Wazzie (2015-08-12 05:50:14 am)

Offline

## #4 2015-08-12 08:37:03 am

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 280

### Re: Rounding numbers with arbitrary precision

Thank you for the nice comments.

Shane, your script is a concise, fast, and elegant ASOC solution, as usual. It would be particularly useful when crunching large numbers of values within the bit precision limits you describe. My handler's fortes are that it can handle arbitrary number sizes and precisions and can take as input numerical expressions of arbitrary complexity (exponentiation, trigonometric terms, grouping with parentheses, ...) as long as they are valid bc calculator expressions. The tradeoff is that it runs about 16 to 20 times slower than your ASOC solution in my initial tests. On my aging 2012 Powerbook, after reconfiguring the handler to accept a list of input numbers which are processed in a repeat loop inside the handler, it takes about 0.04 + (0.007 x #input numbers) seconds, or about 3/4 of a second for 100 numbers.

DJ, this is my first exposure to locale issues in number formatting. I appreciate your "run script" solution. I was wondering if there might be a faster alternative.  One idea that comes to mind is to get the separator character with

#### Applescript:

set decimalChar to (1 as real as text)'s text 2

then replace any literal periods in my handler with the decimalChar variable. What are your thoughts about this approach? Also, can you please tell me if the shell script part of the handler, i.e., the code inside

#### Applescript:

set bcCalculatorOutput to do shell script "..."

would also need to be adjusted for locale, or are shell scripts somehow magically locale-indifferent? Thanks again for your helpful comments.

Last edited by bmose (2015-08-12 08:37:46 am)

Offline

## #5 2015-08-12 09:09:27 am

DJ Bazzie Wazzie
Member
From:: the Netherlands
Registered: 2004-10-20
Posts: 2776
Website

### Re: Rounding numbers with arbitrary precision

bmose wrote:

DJ, this is my first exposure to locale issues in number formatting. I appreciate your "run script" solution. I was wondering if there might be a faster alternative.  One idea that comes to mind is to get the separator character with

#### Applescript:

set decimalChar to (1 as real as text)'s text 2

then replace any literal periods in my handler with the decimalChar variable. What are your thoughts about this approach?

I think it's an better approach. It requires more lines of code but would run definitely faster.

bmose wrote:

Also, can you please tell me if the shell script part of the handler, i.e., the code inside

#### Applescript:

set bcCalculatorOutput to do shell script "..."

would also need to be adjusted for locale, or are shell scripts somehow magically locale-indifferent? Thanks again for your helpful comments.

On my machine bc handles and returns only decimal points no matter what locale is set in the shell itself (tried it with different locale settings). I think it's safe to say you don't have to worry about that.

Offline

## #6 2015-08-12 09:24:01 am

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 280

### Re: Rounding numbers with arbitrary precision

Thanks on both counts. I breathed a sigh of relief with your news about not having to change the shell script code. I will post the changes when I get free time later.

Offline

## #7 2015-08-12 06:01:29 pm

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 5408

### Re: Rounding numbers with arbitrary precision

bmose wrote:

The tradeoff is that it runs about 16 to 20 times slower than your ASOC solution in my initial tests.

That matches what I saw. If performance is an issue, you could always add a check to your handler, and pass values that are within the lesser range to the other handler.

Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/
latenightsw.com

Offline

## #8 2015-08-12 08:35:36 pm

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 280

### Re: Rounding numbers with arbitrary precision

The hybrid approach is an interesting idea.  It would certainly be worth considering under the right circumstances and if the need for speed were paramount.

Last edited by bmose (2015-08-12 08:35:51 pm)

Offline

## #9 2015-08-15 07:25:50 am

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 280

### Re: Rounding numbers with arbitrary precision

Several improvements were made:

(1) The input argument theNumber can now be a list of numbers and/or bc calculator numerical expressions. In this case, the returned properties will be lists of corresponding values, rather than the values themselves.
(2) Locale differences in number formatting (i.e., decimal separator character) should now be handled properly.
(3) Informative error messages are now returned for input items that failed to round properly.
(4) The sed algorithm for detecting undeclared bc calculator variable names has been refined.
(5) User-defined variables, which must be of the form lowercase x followed by one or more digits, may now be included in bc calculator numerical expressions.
(6) The input argument nDecimalPlaces can now be a negative number, which allows rounding at any digit to the left of the decimal point, specifically the (|nDecimalPlaces|+1)'th digit to the left of the decimal point

The following example demonstrates the input of a list of items and purposely invalid entries to demonstrate list entry and the error message feature:

#### Applescript:

set numberList to {1, 2.2, "3.3", {4, 5, 6}, "xx + 7", "8/0", "9+("}

tell roundNumber({theNumber:numberList, roundingMethod:"school", nDecimalPlaces:1})
its roundedNumber --> {1, 2.2, 3.3, null, null, null, null}
its decimalForm --> {"1", "2.2", "3.3", null, null, null, null}
its exponentialForm --> {"1.0E+0", "2.2E+0", "3.3E+0", null, null, null, null}
its errorMessage --> {null, null, null, "Error: The input item is not an integer, real number, or text string", "Error: The input item contains one or more undeclared variables", "Runtime error (func=(main), adr=176): Divide by zero", "(standard_in) 20: parse error(standard_in) 1: parse error"}
end tell

The following examples demonstrate a bc numerical expression including user-defined variables. The result is trivial, reducing to the number e rounded to 49 decimal places, but it shows how an arbitrarily complex expression could be entered:

#### Applescript:

set bcExpression to "
x1=3
x2=0
while (x2<4) {
x2++
}
x3=sqrt(x1^(1+1) + x2^(e(0) + 10^0))
print x3 - 5 + e(1)"

tell roundNumber({theNumber:bcExpression, roundingMethod:"down", nDecimalPlaces:49})
its roundedNumber --> 2.718281828459 (too large for Applescript to represent exactly)
its decimalForm --> "2.7182818284590452353602874713526624977572470936999"
its exponentialForm --> "2.7182818284590452353602874713526624977572470936999E+0"
its errorMessage --> null
end tell

tell roundNumber({theNumber:bcExpression, roundingMethod:"up", nDecimalPlaces:49})
its roundedNumber --> 2.718281828459 (too large for Applescript to represent exactly)
its decimalForm --> "2.7182818284590452353602874713526624977572470937"
its exponentialForm --> "2.7182818284590452353602874713526624977572470937E+0"
its errorMessage --> null
end tell

The following example demonstrates setting nDecimalPlaces to a negative number to round at the (|nDecimalPlaces|+1)'th (3rd in the current example) digit to the left of the decimal point:

#### Applescript:

tell roundNumber({theNumber:"123456.789", roundingMethod:"school", nDecimalPlaces:-2})
its roundedNumber --> 123500
its decimalForm --> "123500"
its exponentialForm --> "1.23500E+5"

#### Applescript:

(*
USAGE NOTES:

This handler utilizes the shell calculator bc to round a number or bc calculator numerical expression, or a list of such numbers or expressions, in any of five different ways, mimicking Applescript's rounding commands but without Applescript's bit precision limit of 2^52 - 1 (approximately 15 or 16 digits). It returns the rounded number or numbers as well as their text string representation in decimal and exponential forms.

The input argument is a record of the form {theNumber:xxx, roundingMethod:xxx, nDecimalPlaces:xxx}, where:

theNumber may be any integer, real number, or text string whose content is a valid number or bc calculator numerical expression, or a list of such numbers and text strings

NOTES:
- For bc calculator numerical expressions, user-defined variable names must begin with lowercase x followed by one or more digits; any others will be flagged as invalid
- This restriction allows the handler to detect erroneous undeclared variables, which the bc calculator would otherwise initialize to zero rather than flag as invalid

roundingMethod is one of the following text strings:
"school" (the default value if omitted from the input argument)
- Applescript's "rounding as taught in school"
- Rounds the fractional portion away from zero if â‰¥ 0.5 and toward zero if < 0.5
"nearest"
- Applescript's "rounding to nearest"
- Rounds the fractional portion away from zero if > 0.5, or = 0.5 and the preceding digit is odd
- Rounds the fractional portion toward zero if < 0.5, or = 0.5 and the preceding digit is even
"truncate"
- Applescript's "rounding toward zero"
- Rounds the fractional portion toward zero (i.e., truncates the fractional portion)
"down"
- Applescript's "rounding down"
- Rounds a non-zero fractional portion toward minus infinity
"up"
- Applescript's "rounding up"
- Rounds a non-zero fractional portion toward plus infinity

nDecimalPlaces is an integer indicating the number of decimal places to which the number is rounded
- A positive value rounds to that many decimal places; a negative value rounds at the (|nDecimalPlaces|+1)'th digit to the left of the decimal point
- The default value is 0 if omitted from the input argument, which results in rounding to an integer value
- A real value will be coerced to an integer

The value returned by the handler is a record of the form {roundedNumber:xxx, decimalForm:xxx, exponentialForm:xxx, errorMessage:xxx}, where:

roundedNumber is an integer or real number, or the text string "too large" if the value exceeds Applescript's maximum allowed value of 1.79769313486231 * 10^308
- A number exceeding Applescript's bit precision limit of 2^52 - 1 = 4.50359962737 * 10^15 (approximately 15 or 16 digits) will likely be an inexact value

decimalForm is a text string representation of the exact value of the rounded number in decimal form with no limits on the number's size or precision

exponentialForm is a text string representation of the exact value of the rounded number in exponential form with no limits on the number's size or precision

errorMessage is the bc calculator error message if an error occurred during rounding of the input number or expression

NOTES:
- The property values will be returned as lists of values if the input argument theNumber is a list of numbers and expressions
- roundedNumber, decimalForm, and exponentialForm will be set to the null value for any input number or expression that failed to round due to an error
- errorMessage will be set to the null value for any number that rounded without an error

Locale differences in number formatting (comma vs period decimal separator) should be handled properly.

If more than 1000 decimal places of precision is needed, increase the value of maximumScale in the assignment statement "set maximumScale to 1000" :-)
*)

on roundNumber(inputArgument)
-- Set the number of decimal places of precision for the intermediary calculations
set maximumScale to 1000
-- Process the input arguments and set default values for missing properties
tell (inputArgument & {roundingMethod:"school", nDecimalPlaces:0}) to set {theNumber, roundingMethod, nDecimalPlaces} to {its theNumber, its roundingMethod, its nDecimalPlaces}
tell theNumber to if its class â‰  list then set theNumber to {it}
if roundingMethod is not in {"school", "nearest", "truncate", "down", "up"} then error "The input argument roundingMethod must be one of the following text strings: \"school\", \"nearest\", \"truncate\", \"down\", \"up\""
tell nDecimalPlaces
if its class is not in {integer, real} then error "The input argument nDecimalPlaces must be an integer or real number."
set nDecimalPlaces to it as integer
end tell
-- Transform the input numbers and expressions into lines of text, and flag as invalid any item that is not an integer, real number, or text string
set tid to AppleScript's text item delimiters
try
set AppleScript's text item delimiters to ";"
set numbersAsTextStrings to {}
repeat with currNumber in theNumber
set currNumber to currNumber's contents
if currNumber's class = text then
-- Transform a multi-line expression into a single line of semicolon-delimited text
set end of numbersAsTextStrings to currNumber's paragraphs as text
else if currNumber's class is in {integer, real} then
try
-- Generate an error message containing a text representation of the number with greater bit precision than a simple coercion to text
-- E.g.: 2^45 as text -> "3.518437208883E+13"; || of 2^45 method -> "3.5184372088832E+13", which is exactly correct
|| of currNumber
on error errorMessage
set {o1, o2} to {6 + (offset of "|| of " in errorMessage), -2}
set end of numbersAsTextStrings to errorMessage's text o1 thru o2
end try
else
set end of numbersAsTextStrings to "invalid class"
end if
end repeat
-- Transform the list of input numbers into lines of text
set AppleScript's text item delimiters to linefeed
set numbersAsLinesOfText to numbersAsTextStrings as text
end try
set AppleScript's text item delimiters to tid
-- For each input number or expression, output the decimal components of the rounded number on six separate lines of text
set decimalComponents to paragraphs of (do shell script "

## Cycle through the input numbers and expressions

while read input_item
do

## Validate and format the current input number or bc numerical expression

input_expression=\$(sed -E '

## If the input item was flagged as being of an invalid class, mark the expression as invalid by setting its value to an invalid class text string, then skip to the end to output the expression

/^invalid class\$/s/(.)/\\1/
t

## Convert any terms in E exponential format to bc calculator exponential format (e.g., 1.2E+3 -> 1.2*10^3), then store the expression in the hold space

s/([0-9]+)[eE]([+]?([0-9]+)|([-][0-9]+))/\\1*10^\\3\\4/g
h

## Recursively replace all bc reserved words, including function names and special variable names (i.e., names beginning with a lowercase x followed by one or more digits), with a unique token string (Â¦Â§Â§Â¦)

:again
s/auto|break|continue|define|else|for|halt|ibase|if|last|length|limits|obase|print|quit|read|return|scale|sqrt|warranty|while|x[0-9]+/Â¦Â§Â§Â¦/g
s/(a|c|e|j|l|s)([(][^)]*[)])/Â¦Â§Â§Â¦\\2/g
t again

## If the expression contains any bc reserved words abutting one another, mark the expression as invalid by setting its value to an undeclared variable text string, then skip to the end to output the expression

/Â¦Â§Â§Â¦Â¦Â§Â§Â¦/s/^.+\$/undeclared variable/
t

## Replace all tokens with spaces

s/Â¦Â§Â§Â¦/ /g

## If the expression contains any remaining alphabetic characters, treat them as undeclared variables, mark the expressioon as invalid by setting its value to an undeclared variable text string, then skip to the end to output the expression
## Prior to doing this, reset the t command so that it can recognize if a substitution takes place in the s command

t reset
:reset
/[a-zA-Z]/s/^.+\$/undeclared variable/
t

## If the expression does not have undeclared variable names, get the uncorrupted version of the expression from the hold space, trim leading and trailing spaces, and output the expression

g
s/^[[:space:]]+//
s/[[:space:]]+\$//

' <<<\"\$input_item\")

## If the input expression was flagged as being an invalid input item or having undeclared variables, set the invalid flag to a unique positive integer and input_expression to the empty string
## Otherwise, set the invalid flag to 0

if [[ \$input_expression = 'invalid class' ]]; then
invalid_flag=1
elif [[ \$input_expression = 'undeclared variable' ]]; then
invalid_flag=2
else
invalid_flag=0
fi
[[ invalid_flag -gt 0 ]] && input_expression=''

## Run the bc calculator script

bc -l <<<\"
if (\$invalid_flag==1) {

## If the input item is of an invalid class, flag it as such by printing an error message to stdout and end any further processing of the input expression

print \\\"
Error: The input item is not an integer, real number, or text string.\\\"

} else if (\$invalid_flag==2) {

## If the input expression has undeclared variables, flag it as such by printing an error message to stdout and end any further processing of the input expression

print \\\"
Error: The input item contains one or more undeclared variables.\\\"

} else {

## Get the value of the input expression at maximum precision (with enclosing curly braces so that multiple statements are executed as a group), print the result to stdout (which happens automatically in the input_expression statement), and save the result to the input_value variable for subsequent rounding

max_scale="
& maximumScale & "
scale=max_scale
{\$input_expression}
input_value=last

## Get the nDecimalPlaces and roundingMethod input argument values

n_decimal_places="
& nDecimalPlaces & "
rounding_method="
& (offset of roundingMethod in "schoolÂ¦nearestÂ¦truncateÂ¦downÂ¦up") & "

## Extract the sign of the input number, then transform the number to its absolute value with the decimal point shifted nDecimalPlaces to the right (to the left if nDecimalPlaces < 0)

number_sign=1-2*(input_value<0)
if (n_decimal_places>=0) {
shifted_number=number_sign*input_value*(10^n_decimal_places)
} else {
### This formulation gets around bc's inability to handle negative exponents
shifted_number=number_sign*input_value/(10^-n_decimal_places)
}
## Round the input number according to the method specified by the input argument by adding 0, 0.5, or 1 as appropriate to the positive-valued shifted number

if (rounding_method==1) { ## i.e., if rounding_method==school
shifted_number=shifted_number+0.5
} else if (rounding_method==8) { ## i.e., if rounding_method==nearest
scale=0
final_digit=(shifted_number%10)/1
final_digit_is_odd_number=final_digit%2
fractional_part_after_final_digit=shifted_number-(shifted_number/1)
scale=max_scale
if ((fractional_part_after_final_digit>0.5) || ((fractional_part_after_final_digit==0.5) && (final_digit_is_odd_number==1))) shifted_number=shifted_number+0.5
} else if (rounding_method==16) { ## i.e., if rounding_method==truncate
## do nothing
} else if (rounding_method==25) { ## i.e., if rounding_method==down
if (number_sign==1) {
## do nothing
} else if (number_sign==-1) {
scale=0
fractional_part=shifted_number-(shifted_number/1)
if (fractional_part>0) shifted_number=shifted_number+1
}
} else if (rounding_method==30) { ## i.e., if rounding_method==up
if (number_sign==1) {
scale=0
fractional_part=shifted_number-(shifted_number/1)
if (fractional_part>0) shifted_number=shifted_number+1
} else if (number_sign==-1) {
## do nothing
}
}

## Remove the fractional part of the shifted number, shift the decimal point back to its original position nDecimalPlaces to the left (to the right if nDecimalPlaces < 0), and restore the number sign

scale=0
shifted_number=shifted_number/1
scale=n_decimal_places
if (n_decimal_places>=0) {
rounded_number=number_sign*shifted_number/(10^n_decimal_places)
} else {
### This formulation gets around bc's inability to handle negative exponents
rounded_number=number_sign*shifted_number*(10^-n_decimal_places)
}

## Print a unique separator token string followed by the the rounded number in decimal form to stdout
## The token string allows the rounded value to be distinguished from the previously printed input expression value and any error messages

print \\\"
Â¦Â§Â§Â¦\\\", rounded_number
}

## Capture any error messages (2>&1), and remove any reverse slashes or linefeeds from the rounded number returned by bc

\" 2>&1 | tr -d '\\\\\\n' | sed -E '

## If the input number or expression is invalid (i.e., the output from the bc calculator has an error message), branch to the end of the script and output the letter I followed by the error message on one line followed by five empty lines

/[eE]rror/s/^([^Â¦]+)(Â¦Â§Â§Â¦.*)?\$/I\\1\\"
& linefeed & "\\" & linefeed & "\\" & linefeed & "\\" & linefeed & "\\" & linefeed & "/
t

## If the input number or expression is valid, remove all text up to and including the separator token, leaving only the rounded number, then print the following components of the rounded number on six separate lines
## Number sign
## First nonzero digit of the whole number part
## Remaining digits of the whole number part
## Leading zeros of the fractional part
## First nonzero digit of the fractional part
## Remaining digits of the fractional part

s/^.*Â¦Â§Â§Â¦//
/^[+-]/!s/^/+/
/[.]/!s/\$/./
s/([+-])0*[.]/\\1./
s/0+\$//
s/([+]|([-]))([1-9]?)([0-9]*)[.](0*)([1-9]?)([0-9]*)/\\2\\"
& linefeed & "\\3\\" & linefeed & "\\4\\" & linefeed & "\\5\\" & linefeed & "\\6\\" & linefeed & "\\7/
'
done <<<"
& numbersAsLinesOfText's quoted form)
-- Construct the text representation of the rounded number or numbers in decimal and exponential forms from the components parts
-- Also, coerce the decimal form into an Applescript integer or real number (or assign the value "too large" if the number exceeds Applescript's maximum allowed value of 1.79769313486231 * 10^308)
-- If a number or expression is invalid, set its rounded results to the null value, and output the error message encountered
set decimalChar to (1 as real as text)'s text 2 -- takes into account locale differences in number formatting
set {roundedNumber, decimalForm, exponentialForm, errorMessage} to {{}, {}, {}, {}}
tell decimalComponents
repeat with i from 0 to (length - 6) by 6
set {numberSign, wholeNumberFirstNonzeroDigit, wholeNumberRemainingDigits, fractionLeadingZeros, fractionFirstNonzeroDigit, fractionRemainingDigits} to items (i + 1) thru (i + 6)
if numberSign starts with "I" then -- if the item is invalid
set {end of roundedNumber, end of decimalForm, end of exponentialForm, end of errorMessage} to {null, null, null, numberSign's text 2 thru -1}
else -- if the item is valid
set decimalVersion to wholeNumberFirstNonzeroDigit & wholeNumberRemainingDigits & decimalChar & fractionLeadingZeros & fractionFirstNonzeroDigit & fractionRemainingDigits
tell decimalVersion to if it starts with decimalChar then set decimalVersion to "0" & it
tell decimalVersion to if it ends with decimalChar then set decimalVersion to text 1 thru -2
tell (numberSign & decimalVersion)
try
set end of roundedNumber to it as number
on error
set end of roundedNumber to "too large" -- if the number exceeds Applescript's maximum allowed value of 1.79769313486231 * 10^308
end try
set end of decimalForm to it
end tell
if wholeNumberFirstNonzeroDigit = "" then
set {theMantissa, theExponent} to {fractionFirstNonzeroDigit & decimalChar & fractionRemainingDigits, (-1 * ((fractionFirstNonzeroDigit's length) + (fractionLeadingZeros's length))) as text}
else
set {theMantissa, theExponent} to {wholeNumberFirstNonzeroDigit & decimalChar & wholeNumberRemainingDigits & fractionLeadingZeros & fractionFirstNonzeroDigit & fractionRemainingDigits, wholeNumberRemainingDigits's length as text}
end if
tell theMantissa to if it starts with decimalChar then set theMantissa to "0" & it
tell theMantissa to if it ends with decimalChar then set theMantissa to it & "0"
tell theExponent to if it does not start with "-" then set theExponent to "+" & it
set end of exponentialForm to numberSign & theMantissa & "E" & theExponent
set end of errorMessage to null
end if
end repeat
end tell
-- If there is only one input item, convert the output lists to their item values instead
if roundedNumber's length = 1 then set {roundedNumber, decimalForm, exponentialForm, errorMessage} to {roundedNumber's first item, decimalForm's first item, exponentialForm's first item, errorMessage's first item}
-- Return the rounded number or numbers in three different forms: Applescript number, text representation in decimal form, and text representation in exponential form, along with any error messages for input items that failed to round properly
return {roundedNumber:roundedNumber, decimalForm:decimalForm, exponentialForm:exponentialForm, errorMessage:errorMessage}
end roundNumber

Edit note: This entry was modified since originally submitted to allow negative nDecimalPlaces values.
Edit note 7-Oct-2015: The substitution command that detects undeclared bc calculator variable names was altered to be more robust in detecting undeclared variable names (/[a-z][a-z0-9_]*/s/^.+\$/undeclared variable/ changed to /[a-zA-Z]/s/^.+\$/undeclared variable/).

Last edited by bmose (2015-10-07 07:19:51 am)

Offline

## Board footer

Powered by FluxBB