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” :slight_smile:

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

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):

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.

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.

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

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

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

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.

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

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.

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.

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.

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.

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:

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:

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:

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

(*
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/).