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)