Justified text in Applescript

This handler justifies Applescript text. It does so by inserting leading and/or trailing spacer strings around text entries. The spacer strings are formed with an optimized combination of up to five different types of Unicode space characters, each of a different pixel width.

The text to be justified may be input as row sublists, column sublists, or a single delimited text string. Columns may be either left-justified, centered, or right-justified. Spacer columns may be inserted between justified text columns. Two parameters are exposed for optimizing handler performance. The justified text may be output as a single text string or as a list of row text strings. The former is suitable for display in a “display dialog” window or for saving to a text file, the latter for display in a “choose from list” window. Input arguments are error-checked.

Comprehensive user instructions are included with the handler. The ease of use of the handler is demonstrated in a few examples at the end.

(Note: A few cosmetic changes were made to the user instructions and examples since this was first submitted.)

on justifyText(inputFormatOrDelimiter, inputData, columnLayout, customParameters, outputFormat)
	(*
	***  Assume that the justified text consists of R rows and C columns of text entries  ***

	INPUT PARAMETERS:

		● inputFormatOrDelimiter  (the input format of the text to be justified):
			"row"
					- inputData is a list of sublists, each sublist representing one row of text and containing that row's column text entries
			-or-
			"column"
					- inputData is a list of sublists, each sublist representing one column of text and containing that column's row text entries
			-or-
			any text string but "row" or "column"
					- inputData is a text string, each line representing one row of text and containing column text entries delimited by the inputFormatOrDelimiter text string
					- the inputFormatOrDelimiter text string must not conflict with any input text
					- several options are available for choosing a non-conflicting text string delimiter:
						- use an unusual character combination that would never be expected in the input text:
							- "(][)", as one of many examples
						- use a character or characters outside the permissible character set for the input text:
							- any control character (a good option is the tab character = Unicode code point 9)
							- any character outside the extended ASCII character set (Unicode code points > 255)
						- use a rarely used ASCII character or characters; some examples include:
							- copyright sign (© = Unicode code point 169, keyboard shortcut = Option-g)
							- section sign (§ = Unicode code point 167, keyboard shortcut = Option-6)
							- broken bar sign (¦ = Unicode code point 166)
		● inputData  (the text to be justified):
			{row 1 sublist, row 2 sublist, ., row R sublist}
					where:
					row X sublist = {row X column 1 entry, row X column 2 entry, ., row X column C entry}
					- corresponds to inputFormatOrDelimiter = "row" (ROW FORMAT)
			-or-
			{column 1 sublist, column 2 sublist, ., column C sublist}
					where:
					column X sublist = {column X row 1 entry, column X row 2 entry, ., column X row R entry}
					- corresponds to inputFormatOrDelimiter = "column" (COLUMN FORMAT)
			-or-
			"[row 1 column 1 entry][column delimiter][row 1 column 2 entry][column delimiter].[column delimiter][row 1 column C entry]
			 [row 2 column 1 entry][column delimiter][row 2 column 2 entry][column delimiter].[column delimiter][row 2 column C entry]
				.
			 [row R column 1 entry][column delimiter][row R column 2 entry][column delimiter].[column delimiter][row R column C entry]"
					where:
					[column delimiter] = inputFormatOrDelimiter text string
					- corresponds to inputFormatOrDelimiter = any text string but "row" or "column" (DELIMITED FORMAT)
			NOTES:
					- Input text (other than the column delimiter used for delimited format input data) may be comprised only of defined-width characters from the exended ASCII character set
						- control characters (Unicode code points 1-31 and 127-159), the soft hyphen (Unicode code point 173), and characters outside the range of the extended ASCII character set (Unicode code points > 255) are not permitted
						- the zero-pixel width null character (Unicode code point 0) is permitted
					- For row or column format:
						- all sublists must be of the same length
						- all sublist entries must be text strings or coercible to text strings
					- For delimited format:
						- all rows (lines of the input text) must have the same number of columns (delimited text entries within a given line)
		● columnLayout  (the justification type of the text columns, and the location and type of spacer columns, in the justified output):
			{column specifier, column specifier, ..., column specifier}
					where each column specifier may be one of the following:
					JUSTIFICATION COMMAND:
					"[optional prefix]left"		âž” left-justified text column
					"[optional prefix]center"	âž” centered text column
					"[optional prefix]right"		âž” right-justified text column
					FIXED-WIDTH SPACER COLUMN SPECIFIER:
					positive number N			âž” spacer column the width of N regular spaces
					MINIMIZED-WIDTH SPACER COLUMN SPECIFIER:
					negative number N			âž” spacer column of the smallest width possible that assures a separation of at least the width of N regular spaces between the closest same-row text entries in the text columns abutting either side of the minimized-width spacer column
			NOTES:
					- The first justification command in the columnLayout list will be applied to column 1 of inputData, the second justification command to column 2 of inputData, . and the final (Cth) justification command to the final column (column C) of inputData; the number of justification commands in columnLayout must therefore match the number of text columns C in inputData
					- If there are fewer justification commands than the number of columns in inputData, the extra columns in inputData will be ignored
					- If there are more justification commands than the number of columns in inputData, an error will occur
					- Justification commands must end with "left", "center", or "right" but may for purposes of clarity have any text prefix (such as "column 1-", "column 2-", .); the text prefix will be ignored
					- The value of N for spacer columns may contain a fractional component
					- Spacer column specifiers appearing in consecutive columns will be replaced by a single spacer column specifier whose value is the sum of the values (positive and negative) of the consecutive specifiers
		● customParameters  (a two-property record whose property values modify the accuracy and execution speed of column justification):
			{nPixels:[# of pixels], nTypes:[# of types]}
					where:
					[# of pixels] = integer or real number ≥ 0:
						[# of pixels] > 0: this many pixels of each justifying space string will be formed with an optimized combination of special space characters (Unicode code points 8202, 8198, 8201, and/or 8287); the remainder of each space string will be formed with regular spaces (Unicode code point 32)
						[# of pixels] = 0: space strings will be formed with regular spaces only (Unicode code point 32)
					and:
					[# of types] = integer between 0 and 4:
						[# of types] = 4: all four special space character types will be used in space strings (Unicode code points 8202 = hair space, 8198 = six-per-em space, 8201 = thin space, and 8287 = medium mathematical space)
						[# of types] = 3: three special space character types will be used in space strings (Unicode code points 8202, 8198, and 8201)
						[# of types] = 2: two special space character types will be used in space strings (Unicode code points 8202 and 8198)
						[# of types] = 1: one special space character type will be used in space strings (Unicode code point 8202)
						[# of types] = 0: space strings will be formed with regular spaces only (Unicode code point 32)
			-or-
			{}  .or any other non-record value, such as "", missing value, null, 0, false, etc
					- default values will be assigned:  {nPixels:10, nTypes:4}
			NOTES:
					- Higher parameter values generate more accurate column justifications; lower parameter values execute more quickly
						- lower parameter values may be necessary for slower processors or for very large amounts of text to be justified
						- if lower parameter values are required, satisfactory column justification can still often be achieved with parameter settings as low as nPixels = 1 to 2 and/or nTypes = 1 to 2
						- nPixels = 0 and/or nTypes = 0 will restrict the makeSpaceString handler to the use of only regular spaces (character id 32) and will usually result in imperfect column justification
						- excessively high values for nPixels (nPixels > 20 or so) will result in exponentially increasing execution times
					- DEFAULT SETTINGS ARE RECOMMENDED IN MOST CASES
						- to assign default values, set customParameters to any non-record value
						- default values provide the makeSpaceString handler sufficient pixel width (nPixels = 10) and the full range of special space character types (nTypes = 4) to create optimized justifying space strings in a reasonable execution time in most cases
		● outputFormat  (the output format of the justified text)
			"text"	- returns a single text string containing all of the justified text, suitable for display in a "display dialog" window or for saving to a text file
			-or-
			"list"	- returns a list of text strings, each string representing one row of justified text, suitable for display in a "choose from list" window
			-or-
			"both"	- returns the two-item list {justified text in "text" format, justified text in "list" format}

	LIMITATIONS:

		- For justified text saved to a text file, the text file must be opened in a Unicode-aware application (such as TextEdit) and the text set to Lucida Grande regular font (non-bold, non-italic) of any size in order for the column justifications to be preserved
		- Parsing the justified text for its column entries will work as expected only if the parsing program recognizes as whitespace characters the unusual spaces used by this handler in the formation of space strings (six-per-em space, thin space, hair space, and medium mathematical space).  The POSIX "[:space:]" character class fails in this regard, whereas Applescript's "word" text element succeeds:
				words of ("One" & space & "Two" & (character id 8198) & (character id 8287) & "Three") --> {"One", "Two", "Three"}
		- Minor imperfections in column justification will sometimes be encountered and are the unavoidable consequence of forming space strings with Unicode space characters of measurable width, the thinnest available character (Unicode code point 8202 = hair space) being ~40% the width of a regular space character
	*)
	script main
		-- Character widths for Lucida Grande regular font, size 13
		property characterWidths : {0.0, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 4.11328, 4.11328, 4.85596, 8.22021, 8.22021, 8.68994, 9.06445, 2.97705, 4.22754, 4.22754, 6.26514, 10.33398, 4.11328, 7.52197, 4.11328, 6.81738, 8.22021, 8.22021, 8.22021, 8.22021, 8.22021, 8.22021, 8.22021, 8.22021, 8.22021, 8.22021, 4.11328, 4.11328, 10.33398, 10.33398, 10.33398, 5.48438, 11.15918, 8.96924, 7.47754, 8.99463, 9.7373, 7.0459, 6.96973, 9.39453, 9.55322, 3.74512, 4.04346, 8.48682, 6.93164, 11.19727, 9.604, 10.09912, 7.18555, 10.09912, 8.22021, 7.00146, 8.22021, 9.00732, 8.49951, 11.12109, 8.1377, 8.09961, 7.8584, 4.22754, 6.81738, 4.22754, 8.22021, 6.5, 7.98535, 7.1792, 8.18213, 6.65869, 8.18213, 7.24268, 4.77979, 8.10596, 8.06787, 3.75781, 3.95459, 7.59814, 3.75781, 12.13672, 8.06787, 7.98535, 8.18213, 8.18213, 5.31934, 6.62695, 4.8623, 8.06787, 6.72852, 10.0166, 7.97266, 6.79199, 7.45215, 4.22754, 4.85596, 4.22754, 8.22021, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 4.11328, 4.11328, 8.22021, 8.22021, 8.22656, 8.22021, 4.85596, 8.22021, 7.98535, 11.12109, 6.10645, 6.81104, 10.33398, null, 11.12109, 6.5, 5.63672, 10.33398, 5.48438, 5.48438, 7.98535, 8.14404, 8.22021, 4.11328, 7.98535, 5.48438, 6.10645, 6.81104, 10.4165, 10.4165, 10.4165, 5.48438, 8.96924, 8.96924, 8.96924, 8.96924, 8.96924, 8.96924, 11.7876, 8.99463, 7.0459, 7.0459, 7.0459, 7.0459, 3.74512, 3.74512, 3.74512, 3.74512, 9.7373, 9.604, 10.09912, 10.09912, 10.09912, 10.09912, 10.09912, 10.33398, 10.09912, 9.00732, 9.00732, 9.00732, 9.00732, 8.09961, 7.18555, 7.77588, 7.1792, 7.1792, 7.1792, 7.1792, 7.1792, 7.1792, 11.06396, 6.65869, 7.24268, 7.24268, 7.24268, 7.24268, 3.75781, 3.75781, 3.75781, 3.75781, 7.83301, 8.06787, 7.98535, 7.98535, 7.98535, 7.98535, 7.98535, 10.33398, 7.98535, 8.06787, 8.06787, 8.06787, 8.06787, 6.79199, 8.18213, 6.79199} -- pixel widths of the extended ASCII character set (Unicode code points 0 through 255)
		property characterWidth8198 : 2.16455 -- pixel width of the six-per-em space (Unicode code point 8198)
		property characterWidth8201 : 2.60254 -- pixel width of the thin space (five-per-em space, Unicode code point 8201)
		property characterWidth8202 : 1.625 -- pixel width of the hair space (eight-per-em space, Unicode code point 8202)
		property characterWidth8287 : 3.614 -- pixel width of the medium mathematical space (Unicode code point 8287)
		-- Constants
		property character8198 : character id 8198
		property character8201 : character id 8201
		property character8202 : character id 8202
		property character8287 : character id 8287
		property defaultNumberOfSpecialSpaceCharTypes : 4
		property defaultPixelWidthForSpecialSpaceCharStrings : 10
		property errorHeader : "Problem with handler justifyText:" & return & return
		-- Utility properties to increase execution speed of repeat loops
		property justifiedTextAndSpacerColumns : missing value
		property justifiedTextInListFormat : missing value
		property newInputData : missing value
		property newString : missing value
		property numberOfRows : missing value
		property numberOfSpecialSpaceCharTypes : missing value
		property pixelWidthForSpecialSpaceCharStrings : missing value
		property spacerString : missing value
		property stringWidths : missing value
		property textCharIds : missing value
		property textColumn : missing value
		property textColumns : missing value
		property textRows : missing value
		property widthsLeadingSpacesFollowingColumn : missing value
		property widthsTrailingSpacesPrecedingColumn : missing value
		-- Main program
		on run
			validateInputArguments()
			parseCustomParameters()
			parseColumnLayout()
			parseInputData()
			createJustifiedTextAndFixedWidthSpacerColumns()
			createMinimizedWidthSpacerColumns()
			return formatJustifiedText()
		end run
		-- Primary handlers
		on validateInputArguments()
			-- Perform a preliminary validation of input arguments
			tell inputFormatOrDelimiter
				if its class ≠ text then error "" & my errorHeader & "The inputFormatOrDelimiter input argument must be \"row\",\"column\", or a column delimiter text string."
				tell my removeWhitespaces(it) to if it is in {"row", "column"} then set inputFormatOrDelimiter to it
			end tell
			tell inputData
				if (inputFormatOrDelimiter is in {"row", "column"}) then
					if its class ≠ list then error "" & my errorHeader & "The inputFormatOrDelimiter input argument is \"row\" or \"column\", but the inputData input argument is not a list."
					if length = 0 then error "" & my errorHeader & "The inputData input argument is an empty list."
				else
					if its class ≠ text then error "" & my errorHeader & "The inputFormatOrDelimiter input argument specifies delimited text, but the inputData input argument is not a text string."
					if length = 0 then error "" & my errorHeader & "The inputData input argument is an empty text string."
				end if
			end tell
			tell columnLayout
				if its class ≠ list then error "" & my errorHeader & "The columnLayout input argument is not a list"
				if length = 0 then error "" & my errorHeader & "The columnLayout input argument is an empty list."
			end tell
			tell outputFormat
				if its class = text then set outputFormat to my removeWhitespaces(it)
				if outputFormat is not in {"text", "list", "both"} then error "" & my errorHeader & "The outputFormat input argument must be \"text\", \"list\", or \"both\"."
			end tell
		end validateInputArguments
		on parseCustomParameters()
			-- Parse the customParameters input argument, and set the values of the pixelWidthForSpecialSpaceCharStrings and numberOfSpecialSpaceCharTypes properties to the values of customParameters's nPixels and nTypes record properties, respectively; if the record properties are missing, assign default values
			set {my pixelWidthForSpecialSpaceCharStrings, my numberOfSpecialSpaceCharTypes} to {my defaultPixelWidthForSpecialSpaceCharStrings, my defaultNumberOfSpecialSpaceCharTypes}
			if customParameters's class = record then -- ignores the input argument if it is not a record and thus leaves the parameters at their default values
				-- Extract the record property names
				try
					customParameters's item 0
				on error forcedErrorMessage
					set {forcedErrorMessage, AppleScript's text item delimiters} to {forcedErrorMessage's text ((offset of "item 0 of {" in forcedErrorMessage) + ("item 0 of {"'s length)) thru -1, ", "}
					try
						set recordPropertyNames to {}
						repeat with currTextItem in forcedErrorMessage's text items
							set end of recordPropertyNames to currTextItem's text 1 thru ((offset of ":" in currTextItem) - 1)
						end repeat
					end try
					set AppleScript's text item delimiters to ""
				end try
				-- Validate the record property names and values
				repeat with currName in recordPropertyNames
					tell currName's contents
						if it = "nPixels" then
							tell customParameters's nPixels
								if (its class is in {integer, real}) and (it ≥ 0) then
									set my pixelWidthForSpecialSpaceCharStrings to it
								else
									error "" & my errorHeader & "The  \"nPixels\" record property of the customParameters input argument must be a number ≥ 0."
								end if
							end tell
						else if it = "nTypes" then
							tell customParameters's nTypes
								if (its class = integer) and (it ≥ 0) and (it ≤ 4) then
									set my numberOfSpecialSpaceCharTypes to it
								else
									error "" & my errorHeader & "The  \"nTypes\" record property of the customParameters input argument must be an integer between 0 and 4."
								end if
							end tell
						else
							error "" & my errorHeader & "The customParameters input argument has an invalid record property name. The name must be either \"nPixels\" or \"nTypes\"."
						end if
					end tell
				end repeat
			end if
		end parseCustomParameters
		on parseColumnLayout()
			-- Parse the columnLayout input argument, and create placeholders for text columns and spacer columns in the justifiedTextAndSpacerColumns list
			tell (a reference to my justifiedTextAndSpacerColumns)
				set {its contents, isFirstColumn} to {{}, true}
				repeat with currColumnSpecification in columnLayout
					set currColumnSpecification to currColumnSpecification's contents
					if currColumnSpecification's class is in {integer, real} then -- a spacer column
						if not isFirstColumn and (its contents's last item's class is in {integer, real}) then
							set its contents's last item to (its contents's last item) + currColumnSpecification -- condenses consecutive spacer column specifications into a single value by summing the consecutive values
						else
							set end of its contents to currColumnSpecification
						end if
					else if currColumnSpecification's class = text then -- a justified text column
						set currColumnSpecification to my removeWhitespaces(currColumnSpecification's contents)
						if currColumnSpecification ends with "left" then
							set end of its contents to "left"
						else if currColumnSpecification ends with "center" then
							set end of its contents to "center"
						else if currColumnSpecification ends with "right" then
							set end of its contents to "right"
						else
							error "" & my errorHeader & "The columnLayout input argument has a justification command that does not end with \"left\", \"center\", or \"right\"."
						end if
					else
						error "" & my errorHeader & "The columnLayout input argument has an item that is neither a justification command nor spacer column entry."
					end if
					set isFirstColumn to false
				end repeat
			end tell
		end parseColumnLayout
		on parseInputData()
			-- Parse the inputData input argument, and convert to column format if the input data is in row or delimited format
			if inputFormatOrDelimiter = "row" then -- row format
				my convertRowDataToTextColumns()
			else if inputFormatOrDelimiter = "column" then -- column format
				set my textColumns to inputData
			else -- delimited format
				my convertDelimitedDataToTextColumns()
			end if
			-- Verify that all items of the text columns list are sublists (rows) of the same length (same number of rows)
			tell my textColumns
				set {my numberOfRows, columnNumber} to {first item's length, 1}
				repeat with currColumn in rest
					set columnNumber to columnNumber + 1
					tell (currColumn's contents)
						if its class ≠ list then error "" & my errorHeader & "The inputData input argument contains a column entry that is not a list."
						if length ≠ my numberOfRows's contents then error "" & my errorHeader & "Column " & columnNumber & " of the inputData input argument has a different number of rows (" & length & " rows) than reference column 1 (" & my numberOfRows & " rows)."
					end tell
				end repeat
			end tell
		end parseInputData
		on createJustifiedTextAndFixedWidthSpacerColumns()
			-- Replace the justification command and fixed-width spacer column placeholders in the justifiedTextAndSpacerColumns list with their actual content
			set textColumnNumber to 0
			repeat with currColumn in (a reference to my justifiedTextAndSpacerColumns)
				tell currColumn
					if its contents's class is in {integer, real} then -- a spacer column
						if its contents ≥ 0 then -- a fixed-width spacer column
							if (its contents as integer) = (its contents as real) then -- parameter value is a whole number
								set my spacerString to my replicateString(space, its contents)
							else -- parameter value has a fractional component
								set my spacerString to my makeSpaceString((my characterWidths's item 33) * (its contents))'s stringValue -- characterWidths's item 33 = ASCII 32 = regular space character
							end if
							set its contents to {}
							repeat (my numberOfRows) times -- replaces the fixed-width spacer column specifier with a spacer column the width of "fixed-width parameter value" regular spaces
								set end of its contents to my spacerString
							end repeat
						end if
					else -- a justified text column
						set textColumnNumber to textColumnNumber + 1
						set its contents to my justifyColumn(my textColumns's item textColumnNumber, its contents) -- replaces the justification command with a column of justified text
					end if
				end tell
			end repeat
		end createJustifiedTextAndFixedWidthSpacerColumns
		on createMinimizedWidthSpacerColumns()
			-- Replace the minimized-width spacer column placeholders in the justifiedTextAndSpacerColumns list with their actual content
			set {columnNumber, numberOfColumns} to {0, my justifiedTextAndSpacerColumns's length}
			repeat with currColumn in (a reference to my justifiedTextAndSpacerColumns)
				set columnNumber to columnNumber + 1
				tell currColumn
					if its contents's class is in {integer, real} then -- a minimized-width spacer column
						set {its contents, my widthsTrailingSpacesPrecedingColumn, my widthsLeadingSpacesFollowingColumn} to {(its contents) * -1, {}, {}} -- "(its contents) * -1" converts the negaive parameter value to positive; "-(its contents)" is not used because it rounds "its contents" to an integer value
						-- Calculate the width in pixels of trailing spaces for each row of the preceding text column
						if columnNumber = 1 then -- the minimized-width spacer column is the leftmost column
							repeat (my numberOfRows's contents) times
								set end of my widthsTrailingSpacesPrecedingColumn to 0
							end repeat
						else -- the minimized-width spacer column is not the leftmost column
							repeat with currRow in my justifiedTextAndSpacerColumns's item (columnNumber - 1)
								set end of my widthsTrailingSpacesPrecedingColumn to my getStringWidthInPixels(currRow's contents, "trailing spaces")
							end repeat
						end if
						-- Calculate the width in pixels of leading spaces for each row of the following text column
						if columnNumber = numberOfColumns then -- the minimized-width spacer column is the rightmost column
							repeat (my numberOfRows) times
								set end of my widthsLeadingSpacesFollowingColumn to 0
							end repeat
						else -- the minimized-width spacer column is not the rightmost column
							repeat with currRow in my justifiedTextAndSpacerColumns's item (columnNumber + 1)
								set end of my widthsLeadingSpacesFollowingColumn to my getStringWidthInPixels(currRow's contents, "leading spaces")
							end repeat
						end if
						-- Create a spacer string just wide enough to assure that same-row text entries in the columns preceding and following the minimized-width spacer column are at least as far apart as the width of "minimized-width parameter value" regular spaces
						tell ((its contents) * (my characterWidths's item 33)) to set {widthMinimum, widthMaximum} to {it, it} -- characterWidths's item 33 = ASCII 32 = regular space character
						repeat with rowNumber from 1 to (my numberOfRows)
							tell ((my widthsTrailingSpacesPrecedingColumn's item rowNumber) + (my widthsLeadingSpacesFollowingColumn's item rowNumber)) to if it < widthMinimum then set widthMinimum to it
						end repeat
						set my spacerString to my makeSpaceString(widthMaximum - widthMinimum)'s stringValue
						-- Replace the minimized-width spacer column specifier with a spacer column using the spacer string just created
						set its contents to {}
						repeat (my numberOfRows) times
							set end of its contents to my spacerString
						end repeat
					end if
				end tell
			end repeat
		end createMinimizedWidthSpacerColumns
		on formatJustifiedText()
			-- Return the final justfied text in "text" format, "list" format, or both:
			--		"text" format:	a single text string containing all of the justified text, suitable for display in a "display dialog" window or for saving to a text file
			--		"list" format:	a list of text strings, each text string corresponding to a row of justified text, suitable for display in a "choose from list" window
			--		"both":			{justified text in "text" format, justified text in "list" format}
			--
			-- Convert the justified text from column format to row (= "list") format
			tell (a reference to my justifiedTextInListFormat)
				set its contents to {}
				repeat with rowNumber from 1 to (my numberOfRows's contents)
					set end of its contents to ""
					repeat with currColumn in my justifiedTextAndSpacerColumns
						set its contents's last item to its contents's last item & currColumn's item rowNumber
					end repeat
				end repeat
			end tell
			-- Create a version of the justified text in "text" format
			if outputFormat is in {"text", "both"} then
				set AppleScript's text item delimiters to return
				try
					set justifiedTextInTextFormat to "" & my justifiedTextInListFormat
				end try
				set AppleScript's text item delimiters to ""
			end if
			-- Return the formatted text
			if outputFormat = "text" then
				return justifiedTextInTextFormat
			else if outputFormat = "list" then
				return my justifiedTextInListFormat
			else if outputFormat = "both" then
				return {justifiedTextInTextFormat, my justifiedTextInListFormat}
			end if
		end formatJustifiedText
		-- Utility handlers
		on convertDelimitedDataToTextColumns()
			-- Convert from delimited text to row format
			tell (a reference to my newInputData)
				-- Convert each line of delimited text to a row text string
				set its contents to inputData's paragraphs
				-- Convert each row text string to a sublist of column text entries, using the inputFormatOrDelimiter text string to delimit columns
				set AppleScript's text item delimiters to inputFormatOrDelimiter
				try
					repeat with currRow in it
						tell currRow to set its contents to its contents's text items
					end repeat
				end try
				set AppleScript's text item delimiters to ""
				set inputData to its contents
			end tell
			-- Convert from row format to text columns
			my convertRowDataToTextColumns()
		end convertDelimitedDataToTextColumns
		on convertRowDataToTextColumns()
			-- Transform the rows of the inputData input argument into text columns
			tell (a reference to my textColumns)
				set {my textRows, isFirstRow, its contents} to {inputData, true, {}}
				repeat with currRow in my textRows
					if currRow's contents's class ≠ list then error "" & my errorHeader & "The inputData input argument contains a row entry that is not a list."
					set columnNumber to 0
					repeat with currItem in currRow
						if isFirstRow then
							set end of its contents to {currItem's contents}
						else
							set columnNumber to columnNumber + 1
							set end of its contents's item columnNumber to currItem's contents
						end if
					end repeat
					set isFirstRow to false
				end repeat
			end tell
		end convertRowDataToTextColumns
		on getStringWidthInPixels(textString, allTextOrLeadingSpacesOrTrailingSpaces)
			-- Return the sum of the widths in pixels of textString's full string, leading spaces only, or trailing spaces only depending on whether allTextOrLeadingSpacesOrTrailingSpaces is set to "all text", "leading spaces", or "trailing spaces", respectively
			tell allTextOrLeadingSpacesOrTrailingSpaces
				set {isAllText, widthTotal} to {it = "all text", 0}
				if it = "trailing spaces" then
					set my textCharIds to ("" & (textString's text items)'s reverse)'s id as list -- need "as list" for the case where textString is only a single character
				else
					set my textCharIds to textString's id as list -- need "as list" for the case where textString is only a single character
				end if
			end tell
			repeat with currCharId in my textCharIds
				tell currCharId's contents
					if it = 8198 then -- six-per-em space
						set widthTotal to widthTotal + (my characterWidth8198)
					else if it = 8201 then -- thin space (five-per-em space)
						set widthTotal to widthTotal + (my characterWidth8201)
					else if it = 8202 then -- hair space (eight-per-em space)
						set widthTotal to widthTotal + (my characterWidth8202)
					else if it = 8287 then -- medium mathematical space
						set widthTotal to widthTotal + (my characterWidth8287)
					else
						if isAllText then -- measures width of full string
							try
								set widthTotal to widthTotal + (my characterWidths's item (it + 1))
							on error
								if it < 160 then
									error "" & my errorHeader & "The following text string:" & return & return & tab & textString & return & return & "contains a non-permitted control character (Unicode code point = " & it & ")."
								else if it = 173 then
									error "" & my errorHeader & "The following text string:" & return & return & tab & textString & return & return & "contains a non-permitted soft hyphen character (Unicode code point = " & it & ")."
								else -- if it > 255
									error "" & my errorHeader & "The following text string:" & return & return & tab & textString & return & return & "contains a character outside range of the extended ASCII character set (character = " & (character id it) & ", Unicode code point = " & it & ")."
								end if
							end try
						else -- measures width of leading or trailing spaces
							if it is in {32, 160} then -- 32 = regular space, 160 = non-breaking space
								set widthTotal to widthTotal + (my characterWidths's item (it + 1))
							else -- stops measuring width if a non-space character is encountered
								exit repeat
							end if
						end if
					end if
				end tell
			end repeat
			return widthTotal
		end getStringWidthInPixels
		on justifyColumn(columnOfText, leftOrCenterOrRight)
			-- Return a list of text strings (a column of text) either left-, center-, or right-justified by padding with spaces to the width of the longest string
			set my textColumn to columnOfText
			tell (a reference to my textColumn)
				-- Calculate the widths of the text strings, and save the width of the longest string
				set {my stringWidths, widthMax} to {{}, 0}
				repeat with currRow in its contents
					if currRow's contents's class ≠ text then error "" & my errorHeader & "The inputData input argument contains an item in a row or column which is not a text string."
					tell my getStringWidthInPixels(currRow's contents, "all text")
						set end of my stringWidths to it
						if it > widthMax then set widthMax to it
					end tell
				end repeat
				-- Justify the text strings by padding with spaces to the width of the longest string
				set rowNumber to 0
				repeat with currRow in it
					set rowNumber to rowNumber + 1
					tell currRow
						if leftOrCenterOrRight = "left" then
							set its contents to its contents & my makeSpaceString(widthMax - (my stringWidths's item rowNumber))'s stringValue
						else if leftOrCenterOrRight = "center" then
							tell my makeSpaceString((widthMax - (my stringWidths's item rowNumber)) / 2) to set {pad1, widthPad1} to {its stringValue, its stringWidth}
							tell my makeSpaceString(widthMax - (my stringWidths's item rowNumber) - widthPad1) to set {pad2, widthPad2} to {its stringValue, its stringWidth}
							if widthPad1 < widthPad2 then
								set its contents to pad1 & its contents & pad2
							else
								set its contents to pad2 & its contents & pad1
							end if
						else if leftOrCenterOrRight = "right" then
							set its contents to my makeSpaceString(widthMax - (my stringWidths's item rowNumber))'s stringValue & its contents
						end if
					end tell
				end repeat
				return its contents
			end tell
		end justifyColumn
		on makeSpaceString(spacerStringWidth)
			-- Return a record with two properties:
			--		stringValue: a spacer string made up of an optimized combination of regular spaces (Unicode code point 32) and up to four different types of special spaces (in order of usage priority: 8202, 8198, 8201, and 8287) such that the string's total pixel width most closely approximates that specified in spacerStringWidth
			--		stringWidth: the returned string's pixel width
			if spacerStringWidth ≤ 0 then return {stringValue:"", stringWidth:0}
			-- Determine the amount of the spacer string that will be made up of (1) regular spaces (width of n32Base regular spaces) and (2) special space characters (adjustedPixelWidthForSpecialSpaceCharStrings)
			tell (round ((spacerStringWidth - (my pixelWidthForSpecialSpaceCharStrings)) / (my characterWidths's item 33)) rounding down) -- characterWidths's item 33 = ASCII 32 = regular space character
				if it > 0 then
					set {n32Base, adjustedPixelWidthForSpecialSpaceCharStrings} to {it, spacerStringWidth - it * (my characterWidths's item 33)} -- characterWidths's item 33 = ASCII 32 = regular space character
				else
					set {n32Base, adjustedPixelWidthForSpecialSpaceCharStrings} to {0, spacerStringWidth}
				end if
			end tell
			-- Calculate the maximum number of each of the special space characters that may be used in the special-character spacer string
			set {n32Max, n8202Max, n8198Max, n8201Max, n8287Max} to {round (adjustedPixelWidthForSpecialSpaceCharStrings / (my characterWidths's item 33)) rounding up, 0, 0, 0, 0} -- characterWidths's item 33 = ASCII 32 = regular space character
			if adjustedPixelWidthForSpecialSpaceCharStrings's contents > 0 then -- this prevents unnecessary looping if adjustedPixelWidthForSpecialSpaceCharStrings = 0
				tell (my numberOfSpecialSpaceCharTypes) -- this selects the special space characters that may be used in the following order of usage priority: 8202, 8198, 8201, and 8287
					if it > 0 then
						tell me to set n8202Max to round (adjustedPixelWidthForSpecialSpaceCharStrings / (my characterWidth8202)) rounding up
						if it > 1 then
							tell me to set n8198Max to round (adjustedPixelWidthForSpecialSpaceCharStrings / (my characterWidth8198)) rounding up
							if it > 2 then
								tell me to set n8201Max to round (adjustedPixelWidthForSpecialSpaceCharStrings / (my characterWidth8201)) rounding up
								if it > 3 then
									tell me to set n8287Max to round (adjustedPixelWidthForSpecialSpaceCharStrings / (my characterWidth8287)) rounding up
								end if
							end if
						end if
					end if
				end tell
			end if
			-- Calculate the number of each of the special space characters whose combined width most closely approximates the target width (ie, minimizes deltaMin = |combined width - adjustedPixelWidthForSpecialSpaceCharStrings|)
			set deltaMin to 1.0E+9
			repeat with n32 from 0 to n32Max
				repeat with n8287 from 0 to n8287Max
					repeat with n8201 from 0 to n8201Max
						repeat with n8198 from 0 to n8198Max
							repeat with n8202 from 0 to n8202Max
								tell ((n32 * (my characterWidths's item 33) + n8287 * (my characterWidth8287) + n8201 * (my characterWidth8201) + n8198 * (my characterWidth8198) + n8202 * (my characterWidth8202)) - adjustedPixelWidthForSpecialSpaceCharStrings) -- characterWidths's item 33 = ASCII 32 = regular space character
									if it < 0 then
										if -it < deltaMin then set {deltaMin, n32Best, n8287Best, n8201Best, n8198Best, n8202Best} to {-it, n32, n8287, n8201, n8198, n8202}
									else
										if it < deltaMin then set {deltaMin, n32Best, n8287Best, n8201Best, n8198Best, n8202Best} to {it, n32, n8287, n8201, n8198, n8202}
										exit repeat -- avoids some unnecessary loops
									end if
								end tell
							end repeat
							if n8202 = 0 then exit repeat -- avoids some unnecessary loops
						end repeat
						if (n8202 = 0) and (n8198 = 0) then exit repeat -- avoids some unnecessary loops
					end repeat
					if (n8202 = 0) and (n8198 = 0) and (n8201 = 0) then exit repeat -- avoids some unnecessary loops
				end repeat
			end repeat
			-- Create the final spacer string by combining the optimal number of each of the special space characters (nBest...) with regular spaces (n32Base)
			tell (a reference to my spacerString)
				set its contents to ""
				repeat (n32Base + n32Best) times
					set its contents to its contents & space
				end repeat
				repeat n8287Best times
					set its contents to its contents & my character8287
				end repeat
				repeat n8201Best times
					set its contents to its contents & my character8201
				end repeat
				repeat n8198Best times
					set its contents to its contents & my character8198
				end repeat
				repeat n8202Best times
					set its contents to its contents & my character8202
				end repeat
				set finalSpacerStringWidth to (n32Base + n32Best) * (my characterWidths's item 33) + n8287Best * (my characterWidth8287) + n8201Best * (my characterWidth8201) + n8198Best * (my characterWidth8198) + n8202Best * (my characterWidth8202) -- characterWidths's item 33 = ASCII 32 = regular space character
				return {stringValue:its contents, stringWidth:finalSpacerStringWidth}
			end tell
		end makeSpaceString
		on removeWhitespaces(textString)
			-- Remove spaces and tabs from the text string
			tell (a reference to my newString)
				set its contents to ""
				repeat with c in textString
					if c's contents is not in {space, tab} then set its contents to its contents & c
				end repeat
				return its contents
			end tell
		end removeWhitespaces
		on replicateString(textString, repeatFactor)
			-- Return the text string replicated the number of times specified by the repeat factor; return the empty string if the repeat factor is ≤ 0
			if (repeatFactor ≤ 0) or (textString = "") then return ""
			tell (a reference to my newString)
				set its contents to ""
				repeat repeatFactor times
					set its contents to its contents & textString
				end repeat
				return its contents
			end tell
		end replicateString
	end script
	run main
end justifyText

The following examples generate the same justified text output from test input data of 12 rows and 6 columns but use alternative parameter settings.

####################### EXAMPLE 1 #######################

● data is input in "column" format:
	⚬ 6 column sublists, each with 12 row text entries
● column layout is as follows:
	⚬ 1st and 3rd columns right-justified
	⚬ 2nd and 5th columns centered
	⚬ 4th and 6th columns left-justified
	⚬ fixed-width spacer columns 8 spaces wide between the 1st & 2nd, 3rd & 4th, and 5th & 6th text columns
	⚬ minimized-width spacer column up to 5 spaces wide between the 2nd & 3rd text columns
		(becomes a spacer column "0" spaces wide in the justified text because no same-row text between columns 2 and 3 is closer than 5 spaces apart)
	⚬ minimized-width spacer column up to 14 spaces wide between the 4th & 5th text columns
		(becomes a spacer column ~6 spaces wide in the justified text because the closest same-row text between columns 4 and 5, namely row 1's "W" in column 4 and "WWWWWWWWWWW" in column 5, is ~8 regular spaces apart)
● custom parameters are explicitly assigned the default values of nPixels = 10 and nTypes = 4
● justified text is output in "list" format and displayed in a "choose from list" window
set inputFormatOrDelimiter to "column"
set inputData to {¬
	{"W", "llllllllllll", "|||||||||", "===", "!", "^", "'''''", "@", "aaaaaa", "ssssss", "M", "This"}, ¬
	{"WWWWWWWWWWW", "l", "|", "+++", "!", "^^^", "'", "@@@", "a", "s", "MMMMMMMMM", "000"}, ¬
	{"W", "llllllllllll", "|||||||||", "=", "!", "^", "'''''", "@", "aaaaaa", "ssssss", "M", "is"}, ¬
	{"W", "llllllllllll", "|||||||||", "=", "!", "^", "'''''", "@", "aaaaaa", "ssssss", "M", "a"}, ¬
	{"WWWWWWWWWWW", "l", "|", "+++", "!", "^^^", "'", "@@@", "a", "s", "MMMMMMMMM", "111"}, ¬
	{"W", "llllllllllll", "|||||||||", "===", "!", "^", "'''''", "@", "aaaaaa", "ssssss", "M", "test."}}
set columnLayout to {"column 1-right", 8, "column 2-center", -5, "column 3-right", 8, "column 4-left", -14, "column 5-center", 8, "column 6-left"}
set customParameters to {nPixels:10, nTypes:4}
set outputFormat to "list"

set justifiedText to justifyText(inputFormatOrDelimiter, inputData, columnLayout, customParameters, outputFormat)

choose from list justifiedText with prompt (return & " EXAMPLE 1:" & return) with empty selection allowed

####################### EXAMPLE 2 #######################

Identical justified text output as in Example 1 with the following alternative parameter settings:
● data is input in “row” rather than “column” format:
⚬ 12 row sublists, each with 6 column text entries
● column layout is as follows:
⚬ justification commands remain the same, except that the prefixes have been omitted
⚬ fixed-width spacer columns remain the same
⚬ minimized-width spacer column between the 2nd & 3rd text columns is omitted, since the value of -5 in Example 1 became a zero-width column in the justified text
⚬ fixed-width spacer column 6 spaces wide between the 4th & 5th text columns replaces the equivalent minimized-width column of value -14
● custom parameters are assigned default values implicitly rather than explicitly by setting customParameters to a non-record value (empty list in this example)
● justified text is output in “text” rather than “list” format, and it is displayed in a “display dialog” rather than a “choose from list” window; the justified text is also saved to the desktop file “Justified.txt”
⚬ The text file “Justified.txt” must be opened in a Unicode-savvy application (such as TextEdit) and all text set to Lucida Grande regular (non-bold, non-italic) font of any size in order for column justifications to be preserved

set inputFormatOrDelimiter to "row"
set inputData to {¬
	{"W", "WWWWWWWWWWW", "W", "W", "WWWWWWWWWWW", "W"}, ¬
	{"llllllllllll", "l", "llllllllllll", "llllllllllll", "l", "llllllllllll"}, ¬
	{"|||||||||", "|", "|||||||||", "|||||||||", "|", "|||||||||"}, ¬
	{"===", "+++", "=", "=", "+++", "==="}, ¬
	{"!", "!", "!", "!", "!", "!"}, ¬
	{"^", "^^^", "^", "^", "^^^", "^"}, ¬
	{"'''''", "'", "'''''", "'''''", "'", "'''''"}, ¬
	{"@", "@@@", "@", "@", "@@@", "@"}, ¬
	{"aaaaaa", "a", "aaaaaa", "aaaaaa", "a", "aaaaaa"}, ¬
	{"ssssss", "s", "ssssss", "ssssss", "s", "ssssss"}, ¬
	{"M", "MMMMMMMMM", "M", "M", "MMMMMMMMM", "M"}, ¬
	{"This", "000", "is", "a", "111", "test."}}
set columnLayout to {"right", 8, "center", "right", 8, "left", 6, "center", 8, "left"}
set customParameters to {}
set outputFormat to "text"

set justifiedText to justifyText(inputFormatOrDelimiter, inputData, columnLayout, customParameters, outputFormat)

-- long button name to widen the window
set okButtonName to "OK"
repeat 70 times
	set okButtonName to space & okButtonName & space
end repeat
set okButtonName to "." & okButtonName & "."

display dialog (" EXAMPLE 2:" & return & return & justifiedText) buttons okButtonName default button okButtonName

do shell script "echo " & justifiedText's quoted form & " > " & ("" & (path to desktop) & "JustifiedText.txt")'s POSIX path's quoted form

####################### EXAMPLE 3 #######################

Identical to Example 2 except:
● data is input in “delimited” rather than “row” format:
⚬ one text string with 12 lines of text (rows), each line having 6 delimited text entries (column entries for that row)
⚬ column delimiter = “§” (Unicode code point 167, keyboard shortcut = Option-6)

set inputFormatOrDelimiter to "§"
set inputData to ¬
	"W§WWWWWWWWWWW§W§W§WWWWWWWWWWW§W" & return & ¬
	"llllllllllll§l§llllllllllll§llllllllllll§l§llllllllllll" & return & ¬
	"|||||||||§|§|||||||||§|||||||||§|§|||||||||" & return & ¬
	"===§+++§=§=§+++§===" & return & ¬
	"!§!§!§!§!§!" & return & ¬
	"^§^^^§^§^§^^^§^" & return & ¬
	"'''''§'§'''''§'''''§'§'''''" & return & ¬
	"@§@@@§@§@§@@@§@" & return & ¬
	"aaaaaa§a§aaaaaa§aaaaaa§a§aaaaaa" & return & ¬
	"ssssss§s§ssssss§ssssss§s§ssssss" & return & ¬
	"M§MMMMMMMMM§M§M§MMMMMMMMM§M" & return & ¬
	"This§000§is§a§111§test."
set columnLayout to {"right", 8, "center", "right", 8, "left", 6, "center", 8, "left"}
set customParameters to {}
set outputFormat to "text"

set justifiedText to justifyText(inputFormatOrDelimiter, inputData, columnLayout, customParameters, outputFormat)

-- long button name to widen the window
set okButtonName to "OK"
repeat 70 times
	set okButtonName to space & okButtonName & space
end repeat
set okButtonName to "." & okButtonName & "."

display dialog (" EXAMPLE 3:" & return & return & justifiedText) buttons okButtonName default button okButtonName

####################### EXAMPLE 4 #######################

Identical to Example 2 except:
● s’s have been been replaced by z’s in row 10
⚬ note the minor leftward shift of the “z” text entry in row 10 column 5; this is the largest imperfection in column justification seen to date

set inputFormatOrDelimiter to "row"
set inputData to {¬
	{"W", "WWWWWWWWWWW", "W", "W", "WWWWWWWWWWW", "W"}, ¬
	{"llllllllllll", "l", "llllllllllll", "llllllllllll", "l", "llllllllllll"}, ¬
	{"|||||||||", "|", "|||||||||", "|||||||||", "|", "|||||||||"}, ¬
	{"===", "+++", "=", "=", "+++", "==="}, ¬
	{"!", "!", "!", "!", "!", "!"}, ¬
	{"^", "^^^", "^", "^", "^^^", "^"}, ¬
	{"'''''", "'", "'''''", "'''''", "'", "'''''"}, ¬
	{"@", "@@@", "@", "@", "@@@", "@"}, ¬
	{"aaaaaa", "a", "aaaaaa", "aaaaaa", "a", "aaaaaa"}, ¬
	{"zzzzzz", "z", "zzzzzz", "zzzzzz", "z", "zzzzzz"}, ¬
	{"M", "MMMMMMMMM", "M", "M", "MMMMMMMMM", "M"}, ¬
	{"This", "000", "is", "a", "111", "test."}}
set columnLayout to {"right", 8, "center", "right", 8, "left", 6, "center", 8, "left"}
set customParameters to {}
set outputFormat to "text"

set justifiedText to justifyText(inputFormatOrDelimiter, inputData, columnLayout, customParameters, outputFormat)

-- long button name to widen the window
set okButtonName to "OK"
repeat 70 times
	set okButtonName to space & okButtonName & space
end repeat
set okButtonName to "." & okButtonName & "."

display dialog (" EXAMPLE 4:" & return & return & justifiedText) buttons okButtonName default button okButtonName

Browser: Safari 536.28.10
Operating System: Mac OS X (10.8)