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”}
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