Convert Number Ranges to Individual Numbers

I wrote a shortcut that converts a string containing individual numbers and ranges of numbers to individual numbers only. My first effort used two loops and took 120 milliseconds to run.

Create Numbers One.shortcut (23.1 KB)

My second effort used one loop but required a string containing consecutive numbers that exceed the highest number in the string being processed. This took 75 milliseconds to run.

Create Numbers.shortcut (23.2 KB)

I know it’s unlikely, but I wondered if anyone knew a shortcut solution that’s simpler or faster.

Just for fun, this is the AppleScriptObjC version using NSMutableIndexSet and NSRange.
The ranges {location, length} are represented by 1st item and (2nd item + 1) - 1st item

use AppleScript version "2.5"
use framework "Foundation"
use scripting additions

-- a trick to handle NSNotFound properly. Credits: https://latenightsw.com/high-sierra-applescriptobjc-bugs/
property NSNotFound : a reference to 9.22337203685477E+18 + 5807

set theText to "1 3-6 8 10-13 15 17-20"
set indexSet to current application's NSMutableIndexSet's new()
set {saveTID, text item delimiters} to {text item delimiters, {space}}
set theParts to text items of theText
set text item delimiters to {"-"}
repeat with aPart in theParts
	set theRange to text items of aPart
	if (count theRange) is 1 then
		(indexSet's addIndex:(item 1 of theRange as integer))
	else
		set theLocation to item 1 of theRange as integer
		set theLength to ((item 2 of theRange as integer) + 1) - theLocation
		(indexSet's addIndexesInRange:({theLocation, theLength}))
	end if
end repeat
set resultArray to {}
repeat until indexSet's firstIndex() = NSNotFound
	set end of resultArray to indexSet's firstIndex()
	indexSet's removeIndex:(indexSet's firstIndex())
end repeat
set text item delimiters to {space}
display dialog (resultArray as text)
set text item delimiters to saveTID

1 Like

Here’s a solution that uses the shell seq command. This took 125 milliseconds to run, although this solution might be a good one if the number of pages in the page ranges is large. Also, it is reasonably short.

Create Numbers Shell.shortcut (22.8 KB)

Hi @StefanK

I think the range length should be 2nd item - 1st item + 1, otherwise the second item’s omitted from the run.

Here’s a vanilla handler I wrote a couple of years ago. It handles negatives as well as positives and accepts either commas or spaces as separators, but as it stands, it can’t cope with multiple separators between number groups:

on rangeExpansion(rangeExpression)
	-- Split the expression at the commas, if any.
	set astid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to {",", space}
	set theRanges to rangeExpression's text items
	
	set integerList to {}
	set AppleScript's text item delimiters to "-"
	repeat with thisRange in theRanges
		-- Split each range or integer text at its dash(es), if any.
		set rangeParts to thisRange's text items
		-- If the first text item's "", the first integer was preceded by a minus sign, so insert the negative of the SECOND text item at the beginning of the parts list.
		if (rangeParts begins with "") then set beginning of rangeParts to -(item 2 of rangeParts)
		-- Similarly, if the penultimate text item's "", insert the negative of the last text item at the end of the parts list.        
		if (((count rangeParts) > 1) and (item -2 of rangeParts is "")) then set end of rangeParts to -(end of rangeParts)
		-- Append all the integers implied by the range to the output list.
		repeat with i from (beginning of rangeParts) to (end of rangeParts)
			set end of integerList to i
		end repeat
	end repeat
	set AppleScript's text item delimiters to astid
	
	return integerList
end rangeExpansion

-- Demo code:
set rangeExpression to "-6,-3--1,3-5,7-11,14,15,17-20"
-- set rangeExpression to "1 3-6 8 10-13 15 17-20"

return rangeExpansion(rangeExpression)
--> {-6, -3, -2, -1, 3, 4, 5, 7, 8, 9, 10, 11, 14, 15, 17, 18, 19, 20}

Right, thanks, good catch.

Too bad can’t use:

  • (void) enumerateIndexesUsingBlock

In my own personal helper framework I have a category on NSIndexset

-(NSArray*)indexSetAsArray;
-(NSMutableArray*)indexSetAsMutableArray;

And on NSArray

-(NSIndexSet*)arrayAsIndexSet;
-(NSMutableIndexSet*)arrayAsMutableIndexSet;

…I would use Swift where you can create an array from an IndexSet just with

let allIndices = Array(indexSet)

:wink:

At the risk of being complicit in the hijacking of peavine’s Shortcuts topic :roll_eyes:, I’d like to point out that the range setting code in Stefan’s first repeat can, with a minor tweak, be made to serve the single-item situation too:

repeat with aPart in theParts
	set theRange to text items of aPart
	set theLocation to item 1 of theRange as integer
	set theLength to ((item -1 of theRange) + 1) - theLocation
	(indexSet's addIndexesInRange:({theLocation, theLength}))
end repeat

And in the second repeat, it’s possible both to reduce the number of reads of indexSet's firstIndex() and to dispense with the NSNotFound trick by simply iterating the same number of times as there are indices in the set:

repeat (indexSet's |count|()) times
	set anIndex to indexSet's firstIndex()
	set end of resultArray to anIndex
	indexSet's removeIndex:(anIndex)
end repeat

Slightly more longwindedly, it’s possible to extract the indices without reducing the set. But I don’t know if this is more efficient or not:

set indexCount to indexSet's |count|()
if (indexCount > 0) then
	set anIndex to (indexSet's firstIndex()) - 1
	repeat indexCount times
		set anIndex to indexSet's indexGreaterThanIndex:(anIndex)
		set end of resultArray to anIndex
	end repeat
end if

Nigel. On many occasions I have posted a shortcut solution in an AppleScript thread, so no complaints from me.

I wrote a slightly different version of my shortcut that uses the shell. With my original string input, the timing result for this shortcut was 110 milliseconds. After modifying the string input to include a very-large page range, the timing result was 115 milliseconds (excluding the Show Result action).

Create Numbers with Shell.shortcut (22.7 KB)