8 Replies Latest reply on Dec 4, 2012 5:15 PM by frameexpert

Converting consecutive "numbers" to ranges

I would like some feedback on an algorithm that identifies ranges in a list of "numbers". Any feedback will be appreciated.

I have a series of strings indicating section numbers like this:

102.2, 102.3, 102.4, 102.5, 102.6, 102.16, 102.17, 102.18, 102.19

What I want to do is collapse three or more consecutive numbers into ranges:

102.2-102.6, 102.16-102.19

First, I convert the string to an array of sections numbers. Next, I get two lists of numbers; one with the hundreds, the other with the tens. Then, I go through these lists, identifying ranges and return a list of positions in the array. So, in this example, I get:

1

5

6

9

If the length of positions is less than the array, I know there is at least one range. Now I go through the ranges to figure out which members to eliminate from the array. I do this by starting at the end of the positions list and subtract [x] - [x-1]. So

9 - 6 = 3

6 - 5 = 1

5 - 1 = 4

Any result > 1 is a range. I subtract 1 from the result and that tells me how many members to eliminate from the array. So for the first one, I delete two members after the 6, which are 7 and 8. I skip the second one because the result is not greater than 1. The third one gives me 4 - 1, so I delete 3 members after position 1 (2, 3, 4). The array is left with 102.2, 102.6, 102.16, 102.19.

Note that the way I get the hyphens in the correct position is not important here. Also, my descriptions of positions above assume 1 as the first position, not 0 as they are in JavaScript arrays.

Below is my code in JavaScript. Any suggestions for simplifying the algorithm are appreciated. Thanks.

Rick Quatro

```var refs = "102.2, 102.3, 102.4, 102.5, 102.6, 102.16, 102.17, 102.18, 102.19";
// Convert the string into an array.
var refsArray = refs.split(", ");
// Split the numbers into hundreds and tens places.
var numbers = getNumberSets (refsArray);
// Return a list of positions in the array, indicating ranges.
var positions = getRangePositions (numbers);
// Delete the unneeded members from the refsArray.

function removeRedundantMembers (positions, refsArray) {

var count = positions.length - 1, span = 0;
// Loop from the end of the array to the beginning
for (var i = count; i > 0; i -= 1) {
// Get the distance between to consecutive positions.
span = positions[i] - positions[i-1];
if (span > 1) {
// Remove the unneeded members.
refsArray.splice(positions[i-1] + 1, span - 1);
}
}
}

function getRangePositions (numbers) {

// Make an array of positions.
var positions = [];
// Make variables to store the current hundreds and tens values.
var hundred = -99, ten = -99;

for (var i = 0, count = numbers.hundreds.length; i < count; i += 1) {
if (numbers.hundreds[i] !== hundred) {
// hundreds value changed; store this position.
positions.push (i);
// Update current hundreds and tens values.
hundred = numbers.hundreds[i];
ten = numbers.tens[i];
} else {
// hundreds stayed the same; check tens place.
if (numbers.tens[i] !== (ten + 1)) {
// More than one tens places skipped; store this position and the previous.
positions.push (i-1);
positions.push (i);
}
ten = numbers.tens[i];
}
}
// The last position is always needed.
positions.push (count-1);
return positions;
}

function getNumberSets (refsArray) {

// Make an object of arrays for the number parts.
var numbers = {hundreds: [], tens: []};
// Make a regular expression to split each number.
var regex = /^(\d+)\.(\d+)\$/, match;

// Loop through the references and split each one at the dot.
for (var i = 0, count = refsArray.length; i < count; i += 1) {
if (regex.test (refsArray[i])) {
match = regex.exec (refsArray[i]);
// Convert the individual strings to integers and add them to the arrays.
numbers.hundreds.push (parseInt(match[1], 10));
numbers.tens.push (parseInt(match[2], 10));
} else {
// Regex doesn't match, so add zeros to the arrays.
numbers.hundreds.push (0);
numbers.tens.push (0);
}
}
return numbers;
}
```
• 1. Re: Converting consecutive "numbers" to ranges

Rick, an interesting challenge! I'm going to ponder about it.

Whilst doing that, perhaps you could take a peek in Peter Kahrel's script for concatenating index numbers: http://www.kahrel.plus.com/indesign/index_update.html

-- it seems to me this is a similar job.

• 2. Re: Converting consecutive "numbers" to ranges

Thanks for your response. There is one wrinkle that I don't think I mentioned in my original post: the client does not want ranges where there are only two consecutive numbers. For example, if you have 102.1, 102.2, 102.4, they don't want 102.1-102.2, 102.4; they want to leave it as is.

I will have a closer look at Peter's script, as it looks like it has some useful functions. Thanks again.

Rick

• 3. Re: Converting consecutive "numbers" to ranges

I think I got it.

You get into trouble because you may have either one digit after the full stop, or two (or possibly even more). The trick, then, is not to treat it as a decimal number with a fraction, but as two separate numbers.

When comparing two numbers, as long as the first number in the first is not the same as the first number in the second set, skip. Then, count same-first-number increasing values for the second number. Only if this set is more than 2 counts long, emit the first and last number with an hyphen in between. For every other case, emit just the original starting number, and a comma plus space.

In code:

```itemlist = app.selection[0].contents;

separated = itemlist.split(', ');
for (i=0; i<separated.length; i++)
separated[i] = [Number(separated[i].match(/^\d+/)),Number(separated[i].match(/\d+\$/)), false];

// first is always okay
result = separated[0][0]+'.'+separated[0][1];

i = 1;
l = separated.length;

while (i < l)
{
while (i < l && separated[i][0] != separated[i-1][0])
{
result += ', '+separated[i][0]+'.'+separated[i][1];
i++;
}
c = 1;
j = i;

while (j < l && separated[j][0] == separated[j-1][0] &&
separated[j][1] == separated[j-1][1]+1)
c++, j++;
if (c > 2)
{
result += '-'+separated[j-1][0]+'.'+separated[j-1][1];
i = j;
} else
{
result += ', '+separated[i][0]+'.'+separated[i][1];
i++;
}
}

```
• 4. Re: Converting consecutive "numbers" to ranges

.. these are my test strings (just two, but I think the code is sound). First line is original, second is concatenated.

Test items: 102.2, 102.3, 102.4, 102.5, 102.6, 102.8, 102.16, 102.17, 102.18, 102.19

-> 102.2-102.6, 102.8, 102.16-102.19

More items: 102.2, 103.3, 103.4, 104.5, 104.6, 104.7, 105.16, 105.17, 105.18, 105.19

-> 102.2, 103.3, 103.4, 104.5-104.7, 105.16-105.19

Message was edited by: [Jongware] You can omit the 'false' in line #5, it wasn't necessary after all. I tried to build the list in memory at first, and this was the Hyphen marker, but it's just easier to build the output while you scan.

• 5. Re: Converting consecutive "numbers" to ranges

Hey, that's excellent. Definitely less code and more direct than my solution. Thank you very much.

Rick

• 6. Re: Converting consecutive "numbers" to ranges

Sorry to be the party pooper (yet again!) but what is the desired output for the following range 100, 100.1, 100.2 100.3 ?

Is it 100.100, 100.1, 100.3 ? which is what Jongware's script gives

Or 100,100.1,100.2 100.3 the result of Rick's script

Or 100 - 100.3

Or 100, 100.1 - 100.3?

Once again sorry

Trevor

• 7. Re: Converting consecutive "numbers" to ranges

Hi Trevor,

No problem. It's good to be prepared for any case, but with this specific project, I know that the data is in the form ###.##. I also know that the strings are already sorted. So, the party is still on with either solution. :-)

Rick

• 8. Re: Converting consecutive "numbers" to ranges

Here is another solution that one of my FrameMaker colleagues worked out. He works around the decimal issue by muliplying by 100.

```var refs = "102.2, 102.3, 102.4, 102.5, 102.6, 102.16, 102.17, 102.18, 102.19, 102.21, 102.22";
var refsArray = refs.split(", ");
var result = "";
var rangeMembers, i, j;

for(i = 0; i < refsArray.length; i++) {

//we'll use this as a counter to keep track of any subsequent
//numbers that are part of a range
rangeMembers = 1;

//set the starting point for the search forward
j = i;

//run this loop to compare the difference between
//later members in the array.
while(++j < refsArray.length) {
//get the previous and current member (j) as an integer.
//Odd things happen when working directly with floats.
//We'll multiply by 100 to preserve up to the hundredths significant
//digit
val1 = Math.round(parseFloat(refsArray[j - 1]) * 100);
val2 = Math.round(parseFloat(refsArray[j]) * 100);

//if the difference is 10, it's a one-tenth increment, therefore
//an additional member of a range
if((val1 + 10) == val2) {
rangeMembers++;
}
//if the difference is 1, it's a one-hundredth increment, therefore
//an additional member of a range
else if((val1 + 1) == val2) {
rangeMembers++;
}
//if we are no longer in a range, abort the search
else {
j = refsArray.length;
}
}

//always add the current member  to the result
result += refsArray[i];

//if we found there were members of a range ahead,
//account for them. Add a hyphen and the last member
//to the result, then reset i so we start there at the next
//for iteration
if(rangeMembers > 2) {
result += "-";
result += refsArray[i + rangeMembers - 1];
i = i + rangeMembers - 1;
}

//if we aren't at the end yet, add the comma and space.
if(i < refsArray.length - 1) {
result += ", ";
}
}