Reformat text 'from_like_this' 'toLikeThis'

I have quite a few old AppleScripts where the variables are_like_this, and I’m wanting to change them toLikeThis. I managed to cobble together a script that would change (for example) thisText to thisText, but it only works on text with one underscore and on one variable.

set theString to "the_text"

(* Set Variables *)
set firstWord to text 1 thru ((offset of "_" in theString) - 1) of theString
set lastWord to text ((offset of "_" in theString) + 1) thru -1 of theString

(* Process Variables *)
set capLastWord to capFirstLetter(lastWord)
set newVariable to firstWord & capLastWord

(* Show Notification *)
set theTitle to "Text Reformatted"
set theSound to "Tink"
set theMessage to quoted form of theString & " changed to " & quoted form of newVariable
display notification theMessage with title theTitle sound name theSound

(* Capitalise 1st letter *)
on capFirstLetter(theString)
	set theLength to length of theString
	set theResult to "" as string
	try
		if theLength > 1 then
			set endString to characters 2 thru theLength of theString as text
			set theResult to upperCaseChar(character 1 of theString) & endString as text
		else
			set theResult to upperCaseChar(character 1 of theString) as text
		end if
	end try
	return theResult
end capFirstLetter
on upperCaseChar(theChar)
	set asciiChar to ASCII number of theChar
	if asciiChar ≥ 97 and asciiChar ≤ 122 then set theChar to ASCII character of (asciiChar - 32)
	return theChar
end upperCaseChar

What I’d like to do if possible is to copy one of my old AppleScripts to the clipboard, run the new script and it go through all the variables with multiple underscores and formatThemLikeThis.

Is something like that possible?

This capitalises every letter following an underscore and then loses the underscores. Obviously it’ll act on every underscore in the text, not just those between words in variable names:

set theString to "set the_text to \"Hello\"
set another_multiword_variable to missing value"

set astid to AppleScript's text item delimiters
-- Break the text at the underscores.
set AppleScript's text item delimiters to "_"
set textChunks to theString's text items

-- Capitalise the initial letter of every chunk except the first.
repeat with i from 2 to (count textChunks)
	set thisChunk to item i of textChunks
	set chunkLen to (count thisChunk)
	if (chunkLen > 0) then
		set intitialCapital to upperCaseChar(character 1 of thisChunk)
		if (chunkLen > 1) then
			set thisChunk to intitialCapital & text 2 thru -1 of thisChunk
		else
			set thisChunk to intitialCapital
		end if
		set item i of textChunks to thisChunk
	end if
end repeat

-- Rejoin the doctored chunks without the underscores.
set AppleScript's text item delimiters to ""
set newString to textChunks as text
set AppleScript's text item delimiters to astid

return newString

on upperCaseChar(theChar)
	set asciiChar to id of theChar -- 'ASCII number' and 'ASCII character' are deprecated. Use 'id'.
	if asciiChar ≥ 97 and asciiChar ≤ 122 then set theChar to character id (asciiChar - 32)
	return theChar
end upperCaseChar

By using some of the code you already have, but adding recursion you could get what you want:


set myList to {"this_function", "that_other_method", "all_the_things_to_do"}

on iterateList(theList)
	set newList to {}
	repeat with currentName in theList
		set newList to newList & underscoreToCamelCase(currentName)
	end repeat
	return newList
end iterateList

on underscoreToCamelCase(theString)
	set firstWord to text 1 thru ((offset of "_" in theString) - 1) of theString
	set lastWord to text ((offset of "_" in theString) + 1) thru -1 of theString
	set capLastWord to capFirstLetter(lastWord)
	set newString to firstWord & capLastWord
	if "_" is in newString then
		underscoreToCamelCase(newString)
	else
		return newString
	end if
end underscoreToCamelCase

on capFirstLetter(theString)
	set theLength to length of theString
	set theResult to "" as string
	try
		if theLength > 1 then
			set endString to characters 2 thru theLength of theString as text
			set theResult to upperCaseChar(character 1 of theString) & endString as text
		else
			set theResult to upperCaseChar(character 1 of theString) as text
		end if
	end try
	return theResult
end capFirstLetter

on upperCaseChar(theChar)
	set asciiChar to ASCII number of theChar
	if asciiChar ≥ 97 and asciiChar ≤ 122 then set theChar to ASCII character of (asciiChar - 32)
	return theChar
end upperCaseChar

log iterateList(myList)

Perfect, thanks a lot. Both of you! :slight_smile:

Sorry, but doesn’t Script Editor come with a find and replace (command+F) function?

Or am I missing something?

Tim

It doesn’t do generic editing. :slight_smile:

Oh. OK :slight_smile:

Here’s a very slightly more controlled script using ASObjC. It only acts when an underscore is followed by a lower-case letter and if they follow a letter or digit character. It replaces every instance of each match at once, which saves the script itself having to deal with every underscore in the text individually. But it’ll still act on any matching text in the code being edited, not just variable names.

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

set theString to "set the_text to \"Hello\"
set another_multiword_variable to missing value
set _leadingUnderscoreVariable to 23
set trailingUnderscoreVariable_ to _leadingUnderscoreVariable"

set newString to replaceUnderscoring(theString)

on replaceUnderscoring(theText)
	set |⌘| to current application
	-- Get an NSMutableString version of the text for editing.
	set workString to |⌘|'s class "NSMutableString"'s stringWithString:(theText)
	-- Preset the start of a regex which will be added to later … 
	set regexStart to |⌘|'s class "NSString"'s stringWithString:("(?<=[a-zA-Z0-9])")
	-- … and a regex for finding an underscore and lower-case English letter which come after an English letter or a digit.
	set genericRegex to |⌘|'s class "NSString"'s stringWithString:("(?<=[a-zA-Z0-9])_[a-z]")
	
	repeat
		-- Find the first current match for the generic regex in the text.
		set matchRange to workString's rangeOfString:(genericRegex) options:(|⌘|'s NSRegularExpressionSearch) range:({0, workString's |length|()})
		-- If none, then we've finished.
		if (matchRange's |length| is 0) then exit repeat
		-- Otherwise, get the text of the match.
		set matchedText to workString's substringWithRange:(matchRange)
		-- Zap the underscore and capitalise the letter.
		set capitalisedLetter to (matchedText's stringByReplacingOccurrencesOfString:("_") withString:(""))'s uppercaseString()
		-- Set a regex to find the matched text specifically.
		set specificRegex to regexStart's stringByAppendingString:(matchedText)
		-- Replace every instance of the matched text in the same situation with just the capitalised letter.
		tell workString to replaceOccurrencesOfString:(specificRegex) withString:(capitalisedLetter) options:(|⌘|'s NSRegularExpressionSearch) range:({0, its |length|()})
	end repeat
	
	return workString as text
end replaceUnderscoring

Edit: Just noticed the upper-case "Z"s in the regexes weren’t upper-case! :rolleyes: It didn’t affect the working of the script though, which has to be a bug in the system. In fact, even the nonsense regex “(?<=[a-aA-a0-9])_[a-z]” gets the same results in High Sierra 10.13.3. :confused:
Later: The anomaly appears to be due to a difference in the interpretation of character class ranges rather than a bug. I understood these ranges specifically to cover lower-case letters, upper-case letters, or numerical digits. In the ICU implementation, they cover Unicode code points, so the “A-z” in my original typo “(?<=[a-zA-z0-9])” was actually matching the entire range from upper-case “A” to lower-case “z”, including the six non-letters between “Z” and “a”. Learn something every day.

Even better, thanks a lot!

I’ve corrected the regexes in the script in post #8. (But see the edit note.)

OK, thanks again.

here’s one using ObjC

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

-- classes, constants, and enums used
property NSString : a reference to current application's NSString
property NSMutableString : a reference to current application's NSMutableString

set capString to "MODIFIED_DATE_INFO"
set camlStringTest to my camelCase:capString withSepator:"_"
--> RESULT "modifiedDateInfo"

on camelCase:aString withSepator:aDelim
	set allLowerCase to (NSString's stringWithString:aString)'s lowercaseString
	set aList to (allLowerCase's componentsSeparatedByString:aDelim)
	set camelString to NSMutableString's |string|()
	set firstWord to aList's item 1
	camelString's appendString:firstWord
	set remaingWords to aList's subarrayWithRange:{1, (aList's |count|()) - 1}
	repeat with aWord in remaingWords
		(camelString's appendString:(aWord's capitalizedString))
	end repeat
	return camelString
end camelCase:withSepator:

and one if you would like to add a preFix

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

-- classes, constants, and enums used
property NSString : a reference to current application's NSString
property NSMutableString : a reference to current application's NSMutableString

set capString to "MODIFIED_DATE_INFO"
set camlStringTest to my camelCase:capString addingPrefix:"PL" withSepator:"_"
--> RESULT "plModifiedDateInfo"

on camelCase:aString addingPrefix:aPrefix withSepator:aDelim
	set allLowerCase to (NSString's stringWithString:aString)'s lowercaseString
	set camelString to (NSMutableString's stringWithString:aPrefix)'s lowercaseString
	set allWords to (allLowerCase's componentsSeparatedByString:aDelim)
	repeat with aWord in allWords
		(camelString's appendString:(aWord's capitalizedString))
	end repeat
	return camelString
end camelCase:addingPrefix:withSepator: