Get non-selected date ranges from the selected date ranges in JavaScript - javascript

I have an array of date ranges(selectedRanges) which shows assigned dates for a member between the main date range. I want to know the date ranges where he/she is unassigned. Please refer to the below example.
mainDateRange = ['01-01-2020', '14-06-2020'];
selectedRanges = [
['03-01-2020','04-01-2020'],
['03-01-2020','05-01-2020'], //overlapping dates
['11-01-2020','13-01-2020'],
['01-02-2020','20-02-2020'],
['15-03-2020','18-03-2020'],
['06-01-2020','06-01-2020'], //date ranges will not be ordered
['03-01-2020','04-01-2020']
]; //dates that the member has work assigned
Desired output
excludedRanges = [
['01-01-2020','02-01-2020'],
['07-01-2020','10-01-2020'],
['14-01-2020','31-01-2020'],
['21-02-2020','14-03-2020'],
['19-03-2020','14-06-2020']
]; //shows all the unassigned periods(ranges)
selectedRanges date ranges will have ranges in random order and also may have duplicate and overlapping dates.
I have searched a lot and found nothing. I am only able to get the unselected dates, not as a range. Please help.
Thank you

Interesting problem, I'll propose an approach to achieve this desired behavior by doing the following:
Transform all string dates into date objects.
Sort the selectedRanges array in ascending order using the start and end dates. This sorting step is cricual to finding the date range gaps.
Adding a "moving cursor" date that moves between the mainDateRange to find and add the missing ranges to the output array.
Before we start the date calculations, we'll need a few helper functions. I've added two functions to go back and forth between the date object and the string format you have (dd-mm-yyyy). Please note that you may not need these two helper function if you use something like Moment.js, but I won't impose an extra dependency on your project.
function stringToDate(stringDate) {
const parts = stringDate.split('-').map((p) => parseInt(p));
parts[1] -= 1;
return new Date(...parts.reverse());
}
function dateToString(date) {
return `${('0' + date.getDate()).slice(-2)}-${('0' + (date.getMonth() + 1)).slice(-2)}-${date.getFullYear()}`;
}
I've also added a sorter function that makes sure the ranges are sorted in an ascending fashion (smaller ranges first).
function dateRangeSorter(a, b) {
if (a[0] < b[0]) return -1;
else if (a[0] > b[0]) return 1;
if (a[1] < b[1]) return -1;
else if (a[1] > b[1]) return 1;
return 0;
}
Now we're good to go on the calculation, here is a code snippet that will log the output at the end.
// data
const output = [];
const oneDayInMs = 24 * 60 * 60 * 1000;
const mainDateRange = ['01-01-2020', '14-06-2020'];
const selectedRanges = [
['03-01-2020','04-01-2020'],
['03-01-2020','05-01-2020'],
['11-01-2020','13-01-2020'],
['01-02-2020','20-02-2020'],
['15-03-2020','18-03-2020'],
['06-01-2020','06-01-2020'],
['03-01-2020','04-01-2020']
];
// helpers
function stringToDate(stringDate) {
const parts = stringDate.split('-').map((p) => parseInt(p));
parts[1] -= 1;
return new Date(...parts.reverse());
}
function dateToString(date) {
return `${('0' + date.getDate()).slice(-2)}-${('0' + (date.getMonth() + 1)).slice(-2)}-${date.getFullYear()}`;
}
function dateRangeSorter(a, b) {
if (a[0] < b[0]) return -1;
else if (a[0] > b[0]) return 1;
if (a[1] < b[1]) return -1;
else if (a[1] > b[1]) return 1;
return 0;
}
// transform into date and sort
const mainDateRangeAsDates = mainDateRange.map(stringToDate);
const selectedRangesAsDates = selectedRanges.map((range) => (range.map(stringToDate)))
.sort(dateRangeSorter);
// start at the beginning of the main date range
let movingDate = mainDateRangeAsDates[0];
// loop through the selected ranges
selectedRangesAsDates.forEach(([startDate, endDate]) => {
// if there's a gap, add it to the output
if (movingDate < startDate) {
output.push([
dateToString(movingDate),
dateToString(new Date(startDate.getTime() - oneDayInMs))
]);
}
// move the cursor date to one day after the end of current rage
movingDate = new Date(endDate.getTime() + oneDayInMs);
});
// if there is a gap at the end, add it as well
if (movingDate < mainDateRangeAsDates[1]) {
output.push([
dateToString(movingDate),
dateToString(mainDateRangeAsDates[1])
]);
}
console.log(output);

Used a similar approach to this: How to make sure every number of a bigger range is within some smaller ranges?
Convert all strings to Dates. Sorts by minimum of range.
Moves minimum position forward, until it finds a gap, and pushes to res array.
Pushes range from last minimum to maximum if it exists
mainDateRange = ['01-01-2020', '14-06-2020'];
selectedRanges = [
['03-01-2020', '04-01-2020'],
['03-01-2020', '05-01-2020'], //overlapping dates
['11-01-2020', '13-01-2020'],
['01-02-2020', '20-02-2020'],
['15-03-2020', '18-03-2020'],
['06-01-2020', '06-01-2020'], //date ranges will not be ordered
['03-01-2020', '04-01-2020']
]; //dates that the member has work assigned
function gapFinder(mainDateRange, selectedRanges) {
const dateToInt = a => new Date(a.split('-').reverse().join('-'))
const intToDate = a => new Date(a).toISOString().slice(0, 10).split('-').reverse().join('-')
// convert to numbers
selectedRanges = selectedRanges.map(r => r.map(dateToInt))
// presort ranges
selectedRanges.sort(([a, ], [b, ]) => a - b)
let [min, max] = mainDateRange.map(dateToInt)
const res = []
for (const [x, y] of selectedRanges) {
if (min > max) break
if (min < x)
res.push([min, x.setDate(x.getDate() - 1)])
min = Math.max(min, y.setDate(y.getDate() + 1))
}
if (min <= max) res.push([min, max])
return res.map(r => r.map(intToDate))
}
console.log(JSON.stringify(gapFinder(mainDateRange,selectedRanges)))
selectedRanges.push(['11-06-2020', '13-06-2020'])
console.log(JSON.stringify(gapFinder(mainDateRange,selectedRanges)))

Related

All possible variations of terms to add up to sum in a given amount of piles?

How to get all possible variations of terms to add up to sum, in a given amount of piles, using javascript?
Let's say I have a sum of 10 and I want to split this into 4 piles with positive terms and zeros only.
function getCombinations(sum, piles){
...
}
getCombinations(10,4);
Returns something like this in a two dimensional array:
[
[3,3,3,1],
[3,3,1,3],
[7,1,1,1],
[10,0,0,0],
...
]
It's not mandatory to return [3,3,3,1] and [3,3,1,3] as different solutions, fastest way will do. I will only work with small numbers, max sum will probably be 10.
It's a variation of the Count the coins problem, http://rosettacode.org/wiki/Count_the_coins, but I want the solutions returned, I have a given set of piles and I use all positive terms (and zero) not only specific coin values.
this should do the trick:
const matrix = (num, cols) => {
const matrix = [[num, ...[...Array(cols-1)].map(() => 0)]];
const hashes = new Set;
const coef = Math.pow(10, cols-1);
let digits = 10 * coef - 1;
while (digits-- >= coef) {
const nums = ('' + digits).split('').map(d => +d);
const hash = nums.sort((a, b) => b - a).join('');
if (hashes.has(hash) || nums.reduce((a, b) => a + b, 0) !== num)
continue;
hashes.add(hash);
matrix.push(nums);
}
return matrix;
};
console.log(matrix(10, 4));
Here is a pratical way of doing this:
function getCombinations(sum, piles){
var array =[];
for(var i = 1; i <= piles; i ++) {
var subArray = Array.apply(null, Array(4)).map(
function () {
return Math.floor(Math.random() * sum)});
array.push(subArray);
}
return array;
}
var result = getCombinations(10,4);

Javascript Get Sequential Dates in Array cross week and month

I have an array with the following values (example):
[
1491408000000,
1491494400000,
1491753600000,
1493222400000,
1493308800000,
1493568000000
]
Where the index is a date time. The date time will always be at 12:00:00 on a date.
In this example, the first 3 dates are consecutive cross weekend (weekend is holiday so count as leave), then another group of 3 dates cross weekend and month.
Now, what I am trying to do is find sequential dates (cross week and month) and put them into an array as follows:
[
1491408000000,
1491494400000,
1491753600000
],
[
1493222400000,
1493308800000,
1493568000000
]
I have tried the following code to get the sequential dates but this cannot cross week and month, how to modify the code to get above result? Any help would be much appreciated!
var timeValue = new Date(dateReview).getTime();
valueCon.push(timeValue);
var k = 0;
sortedValue[k] = [];
valueCon.sort( function ( a, b ){
return +a > +b ? 1 : +a == +b ? 0: -1;
})
.forEach( function( v , i ){
var a = v,b = valueCon[i+1]||0;
sortedValue[k].push( +a );
if ( (+b - +a) > 86400000) {
sortedValue[++k] = []
}
return 1;
});
sortedValue.sort( function ( a,b ){
return a.length > b.length ? -1: 1;
});
This requires help from a function to test if two dates are in the same week. The following goes over the set of time values provided in an array and puts the first value into an array within the array. For each subsequent value, it tests if it's in the same week as the first value in each array within the outer array.
If it's in the same week as the first value in any existing array, it's pushed into that array. Otherwise, it's put in a new array and pushed into the outer array.
There may be a neater way to implement the algorithm, but I'll leave that for others.
Due to time zone differences, they are adjusted to the host time zone based on the original time values representing noon in the source time zone.
// Given 2 dates, return true if they are in the same week (Mon to Sun).
// Otherwise, return false
function sameWeek(a, b){
var e = new Date(+a);
// Week starts at 00:00:00.000 on Monday on or before date
var s = new Date(e.setDate(e.getDate() - ((e.getDay()||7) -1)));
s.setHours(0,0,0,0);
// Week ends at 23:59:59.999 the following Sunday
e.setDate(e.getDate() + 6);
e.setHours(23,59,59,999);
// Test b and return value
return b >= s && b <= e;
}
// Given time value for UTC-0400, adjust to same date and time
// in local time zone and return a date
function adjust(n) {
var d = new Date(n);
d.setMinutes(d.getMinutes() - 240 + d.getTimezoneOffset());
return d;
}
var result = [1491408000000,1491494400000,1491753600000,1493222400000,1493308800000,1493568000000
].reduce(function(acc, n) {
var d = adjust(n);
var used;
if (acc.length != 0) {
used = acc.some(function(arr) {
if (sameWeek(adjust(arr[0]), d)) {
arr.push(n);
return true;
}
});
}
if (!used || acc.length == 0) {
acc.push([n]);
}
return acc;
},[]);
// Result array
console.log(result);
// Printed as date strings adjusted to same host local time
result.forEach(arr => {
arr.forEach(n => console.log(adjust(n).toString()))
console.log('\n');
});
Manipulation of timestamps is a pain. JavaScript has a built-in Date type, as you know, and I would suggest you use it. Date#getUTCDay returns the day of the week as an integer (for reference, 4 is Friday, or the day before a weekend), while Date#setUTCDate and Date#getUTCDate together allow you to adjust the date in day increments (and have it overflow/underflow to the next/previous month). Thus, to determine whether a timestamp b follows "sequentially" (excluding weekends) after a, you can use:
function sequential (a, b) {
a = new Date(a)
return a.setUTCDate(a.getUTCDate() + (a.getUTCDay() === 4 ? 3 : 1)) === b
}
Grouping is just an exercise after that; the code above contains all of the real logic behind this solution.
Example Snippet
var dates = [
1491408000000,
1491494400000,
1491753600000,
1493222400000,
1493308800000,
1493568000000
]
function sequential (a, b) {
a = new Date(a)
return a.setUTCDate(a.getUTCDate() + (a.getUTCDay() === 4 ? 3 : 1)) === b
}
function groupSequential(dates) {
if (dates.length < 2) return [dates.slice()]
dates.sort(function(a, b) { return a - b })
var result = [], group
for (var i = 0; i < dates.length; i++) {
sequential(dates[i - 1], dates[i]) || result.push(group = [])
group.push(dates[i])
}
return result
}
console.log(groupSequential(dates))

How to return an array of values, the sums of which equal a specified number

I'm trying to create an array of numbers of a set length, defining the minimum and a maximum number in the set, and letting a function determine the rest of the numbers between. The kicker is that the sum of this array of numbers must be equal to a predetermined value. The trick is figuring out how that function works.
I found this on stack overflow, which got me the following function:
export const distributeValues = (amount, weights=[]) => {
const distributedAmounts = []
let totalWeights = weights.reduce( (a,b) => a + b)
weights.forEach( weight => {
const weightValue = parseFloat(weight)
const percentage = weightValue / totalWeights
const distributedAmount = Math.round(percentage * amount)
distributedAmounts.push(distributedAmount)
totalWeights -= weightValue
amount -= distributedAmount
})
return distributedAmounts
}
This seems like a good start, but I actually need to work backwards; I'm trying to figure out a function that will give me the weights that would be passed into the above function.
Right now, I have this, a function broken into two parts (apologies for the redundancy):
export const getDistributions = (amount, distributions, modifier) => {
const values = []
let amountLeft = amount;
for (let i = 0; i < distributions; i++ ) {
const value = Math.max(Math.round((amountLeft / (modifier || 4))),1)
amountLeft -= value
values.push(value)
}
// -------------------------------------------- //
// --- correct for cases where total values --- //
// --- end up greater/less than amount --- //
// -------------------------------------------- //
let iterator = 0
let totalAssignedValue = values.reduce((a,b) => a+b);
const lastIndex = (values.length - 1);
const getIndex = (iterator, values) => {
return iterator > lastIndex ? iterator % lastIndex : iterator
}
while (totalAssignedValue > amount) {
iterator = getIndex(iterator)
if (iterator !== lastIndex && iterator !== 0 && values[iterator] > 1) {
values[iterator]--
}
iterator ++
totalAssignedValue = values.reduce((a,b) => a+b);
}
while (totalAssignedValue < amount) {
iterator = getIndex(iterator)
if (iterator !== lastIndex && iterator !== 0) {
values[iterator]++
}
iterator ++
totalAssignedValue = values.reduce((a,b) => a+b);
}
// -------------------------------------------- //
// -------------- end correction -------------- //
// -------------------------------------------- //
return values;
}
The first part tries and distributes the values, but invariably I end up with values that are greater or lesser than the input amount, so there's a second part of the equation that fixes that. Seems a little unclean though, and it's a little arbitrary how the remainders get distributed, so a pure mathematical solution would be great.
I'm starting to wonder if I'm going to need calculus for this, because I basically have the integral (the sum of the array's values), the range of the integral (min and max values), and now have to figure out the formula for the curve. This may be overkill at this point, though.
Thanks for the input!
How about this? First create the set in such way that the first member is the minimum, the second member is minimum + 1, the third minimum + 2, etc. Then sum up the numbers in the set and subtract the sum from the predetermined value. Then distribute the result of the subtraction among all the numbers in the set as outlined betlow.
Set makeSet(int preDet, int min, int max, int setLength)
{
if((max + max - setLength + 1) * setLength / 2 < preDet) return null;
if((min + min + setLength - 1) * setLength / 2 > preDet) return null;
Set set = Set(setLength);
int val = min;
for (int i = 0; i < setLength; i++)
{
set[i] = val++;
}
int sum = (min + val - 1) * setLength / 2;
int dev = preDet - sum;
if(dev)
{
int adj = dev / setLength;
if(dev % setLength) adj++;
for(int i = setLength -1; dev; i--)
{
if(adj > dev) adj = dev;
set[i] += adj;
dev -= adj;
}
}
return set;
}

How to calculate the time percentage of any time in an array of date objects in Javascript

I have a sorted array of Javascript date objects, the array always has a length of 14 entries. In pseudo:
dates
["label1"] = date object 1,
["label2"] = date object 2,
...
["label14"] = date object 14
Although they are in sorted order by date, the dates are not distributed evenly. For example, between entry 1 and 2 may be 1 hour, whilst between entry 5 and 6 are a few minutes, or a few hours.
My challenge is to find an algorithm that for any given input date, will find the following from this array:
The position in the array of the previous date
The position in the array of the next date
Our input date will fall in between the above dates, I'd like to know the percentage in time between the 2 points. For example, exactly in between the dates would return 50%.
I don't like asking "please code this for me" questions but I'm really no good in writing efficient alghorithms and have been unsuccesful in finding an existing alghorithm.
Since the array is sorted, the previous date is at the previous position in the array while the next date is at the next position in the array.
To determine the percentage, you can simply use the following formula:
var percentage = (date - prev) / (next - prev);
That will result in a number between 0 and 1, so if you need between 0 and 100%, simply multiply by 100.
Example:
var dates = [];
for (var i=0; i<14; i++) { dates.push(new Date(1392119656126 + i*i*10000000)) }
for (var i = 1; i<dates.length-1; i++) {
var percentage = ((dates[i]-dates[i-1])/(dates[i+1]-dates[i-1])) * 100;
console.log('P', dates[i-1], 'C', dates[i], 'N', dates[i+1], 'PERC', percentage);
}
ou can use a binary search for that value. Adapted from this answer:
function index(arr, date) { // binary search
var l = 0,
r = arr.length - 1;
while (l <= r) {
var m = l + ((r - l) >> 1);
if (arr[m] < date)
l = m + 1;
else if (arr[m] > date)
r = m - 1;
else // +arr[m] == +date
return m;
}
if (l==0) return -1;
if (l==arr.length) return l;
return l-1+(date-arr[l-1])/(arr[l]-arr[l-1]);
}
Example:
> var dates=[new Date(0), new Date(100), new Date(500)]
> index(dates, new Date(-1))
-1
> index(dates, new Date(0))
0
> index(dates, new Date(50))
0.5
> index(dates, new Date(70))
0.7
> index(dates, new Date(100))
1
> index(dates, new Date(150))
1.125
> index(dates, new Date(500))
2
> index(dates, new Date(600))
3
So when you get a result from such an index call, you would
first check whether it's outside the boundaries res < 0 || res >= length
get the previous position by Math.floor(res)
get the next position by Math.ceil(res)
get the percentage between them by (res%1)*100

How to reduce consecutive integers in an array to hyphenated range expressions?

In JavaScript, how can I convert a sequence of numbers in an array to a range of numbers? In other words, I want to express consecutive occurring integers (no gaps) as hyphenated ranges.
[2,3,4,5,10,18,19,20] would become [2-5,10,18-20]
[1,6,7,9,10,12] would become [1,6-7,9-10,12]
[3,5,99] would remain [3,5,99]
[5,6,7,8,9,10,11] would become [5-11]
Here is an algorithm that I made some time ago, originally written for C#, now I ported it to JavaScript:
function getRanges(array) {
var ranges = [], rstart, rend;
for (var i = 0; i < array.length; i++) {
rstart = array[i];
rend = rstart;
while (array[i + 1] - array[i] == 1) {
rend = array[i + 1]; // increment the index if the numbers sequential
i++;
}
ranges.push(rstart == rend ? rstart+'' : rstart + '-' + rend);
}
return ranges;
}
getRanges([2,3,4,5,10,18,19,20]);
// returns ["2-5", "10", "18-20"]
getRanges([1,2,3,5,7,9,10,11,12,14 ]);
// returns ["1-3", "5", "7", "9-12", "14"]
getRanges([1,2,3,4,5,6,7,8,9,10])
// returns ["1-10"]
Just having fun with solution from CMS :
function getRanges (array) {
for (var ranges = [], rend, i = 0; i < array.length;) {
ranges.push ((rend = array[i]) + ((function (rstart) {
while (++rend === array[++i]);
return --rend === rstart;
})(rend) ? '' : '-' + rend));
}
return ranges;
}
Very nice question: here's my attempt:
function ranges(numbers){
var sorted = numbers.sort(function(a,b){return a-b;});
var first = sorted.shift();
return sorted.reduce(function(ranges, num){
if(num - ranges[0][1] <= 1){
ranges[0][1] = num;
} else {
ranges.unshift([num,num]);
}
return ranges;
},[[first,first]]).map(function(ranges){
return ranges[0] === ranges[1] ?
ranges[0].toString() : ranges.join('-');
}).reverse();
}
Demo on JSFiddler
I needed TypeScript code today to solve this very problem -- many years after the OP -- and decided to try a version written in a style more functional than the other answers here. Of course, only the parameter and return type annotations distinguish this code from standard ES6 JavaScript.
function toRanges(values: number[],
separator = '\u2013'): string[] {
return values
.slice()
.sort((p, q) => p - q)
.reduce((acc, cur, idx, src) => {
if ((idx > 0) && ((cur - src[idx - 1]) === 1))
acc[acc.length - 1][1] = cur;
else acc.push([cur]);
return acc;
}, [])
.map(range => range.join(separator));
}
Note that slice is necessary because sort sorts in place and we can't change the original array.
Here's my take on this...
function getRanges(input) {
//setup the return value
var ret = [], ary, first, last;
//copy and sort
var ary = input.concat([]);
ary.sort(function(a,b){
return Number(a) - Number(b);
});
//iterate through the array
for (var i=0; i<ary.length; i++) {
//set the first and last value, to the current iteration
first = last = ary[i];
//while within the range, increment
while (ary[i+1] == last+1) {
last++;
i++;
}
//push the current set into the return value
ret.push(first == last ? first : first + "-" + last);
}
//return the response array.
return ret;
}
Using ES6, a solution is:
function display ( vector ) { // assume vector sorted in increasing order
// display e.g.vector [ 2,4,5,6,9,11,12,13,15 ] as "2;4-6;9;11-13;15"
const l = vector.length - 1; // last valid index of vector array
// map [ 2,4,5,6,9,11,12,13,15 ] into array of strings (quote ommitted)
// --> [ "2;", "4-", "-", "6;", "9;", "11-", "-", "13;", "15;" ]
vector = vector.map ( ( n, i, v ) => // n is current number at index i of vector v
i < l && v [ i + 1 ] - n === 1 ? // next number is adjacent ?
`${ i > 0 && n - v [ i - 1 ] === 1 ? "" : n }-` :
`${ n };`
);
return vector.join ( "" ). // concatenate all strings in vector array
replace ( /-+/g, "-" ). // replace multiple dashes by single dash
slice ( 0, -1 ); // remove trailing ;
}
If you want to add extra spaces for readability, just add extra calls to string.prototype.replace().
If the input vector is not sorted, you can add the following line right after the opening brace of the display() function:
vector.sort ( ( a, b ) => a - b ); // sort vector in place, in increasing order.
Note that this could be improved to avoid testing twice for integer adjacentness (adjacenthood? I'm not a native English speaker;-).
And of course, if you don't want a single string as output, split it with ";".
Rough outline of the process is as follows:
Create an empty array called ranges
For each value in sorted input array
If ranges is empty then insert the item {min: value, max: value}
Else if max of last item in ranges and the current value are consecutive then set max of last item in ranges = value
Else insert the item {min: value, max: value}
Format the ranges array as desired e.g. by combining min and max if same
The following code uses Array.reduce and simplifies the logic by combining step 2.1 and 2.3.
function arrayToRange(array) {
return array
.slice()
.sort(function(a, b) {
return a - b;
})
.reduce(function(ranges, value) {
var lastIndex = ranges.length - 1;
if (lastIndex === -1 || ranges[lastIndex].max !== value - 1) {
ranges.push({ min: value, max: value });
} else {
ranges[lastIndex].max = value;
}
return ranges;
}, [])
.map(function(range) {
return range.min !== range.max ? range.min + "-" + range.max : range.min.toString();
});
}
console.log(arrayToRange([2, 3, 4, 5, 10, 18, 19, 20]));
If you simply want a string that represents a range, then you'd find the mid-point of your sequence, and that becomes your middle value (10 in your example). You'd then grab the first item in the sequence, and the item that immediately preceded your mid-point, and build your first-sequence representation. You'd follow the same procedure to get your last item, and the item that immediately follows your mid-point, and build your last-sequence representation.
// Provide initial sequence
var sequence = [1,2,3,4,5,6,7,8,9,10];
// Find midpoint
var midpoint = Math.ceil(sequence.length/2);
// Build first sequence from midpoint
var firstSequence = sequence[0] + "-" + sequence[midpoint-2];
// Build second sequence from midpoint
var lastSequence = sequence[midpoint] + "-" + sequence[sequence.length-1];
// Place all new in array
var newArray = [firstSequence,midpoint,lastSequence];
alert(newArray.join(",")); // 1-4,5,6-10
Demo Online: http://jsbin.com/uvahi/edit
; For all cells of the array
;if current cell = prev cell + 1 -> range continues
;if current cell != prev cell + 1 -> range ended
int[] x = [2,3,4,5,10,18,19,20]
string output = '['+x[0]
bool range = false; --current range
for (int i = 1; i > x[].length; i++) {
if (x[i+1] = [x]+1) {
range = true;
} else { //not sequential
if range = true
output = output || '-'
else
output = output || ','
output.append(x[i]','||x[i+1])
range = false;
}
}
Something like that.
An adaptation of CMS's javascript solution for Cold Fusion
It does sort the list first so that 1,3,2,4,5,8,9,10 (or similar) properly converts to 1-5,8-10.
<cfscript>
function getRanges(nArr) {
arguments.nArr = listToArray(listSort(arguments.nArr,"numeric"));
var ranges = [];
var rstart = "";
var rend = "";
for (local.i = 1; i <= ArrayLen(arguments.nArr); i++) {
rstart = arguments.nArr[i];
rend = rstart;
while (i < ArrayLen(arguments.nArr) and (val(arguments.nArr[i + 1]) - val(arguments.nArr[i])) == 1) {
rend = val(arguments.nArr[i + 1]); // increment the index if the numbers sequential
i++;
}
ArrayAppend(ranges,rstart == rend ? rstart : rstart & '-' & rend);
}
return arraytolist(ranges);
}
</cfscript>
Tiny ES6 module for you guys. It accepts a function to determine when we must break the sequence (breakDetectorFunc param - default is the simple thing for integer sequence input).
NOTICE: since input is abstract - there's no auto-sorting before processing, so if your sequence isn't sorted - do it prior to calling this module
function defaultIntDetector(a, b){
return Math.abs(b - a) > 1;
}
/**
* #param {Array} valuesArray
* #param {Boolean} [allArraysResult=false] if true - [1,2,3,7] will return [[1,3], [7,7]]. Otherwise [[1.3], 7]
* #param {SequenceToIntervalsBreakDetector} [breakDetectorFunc] must return true if value1 and value2 can't be in one sequence (if we need a gap here)
* #return {Array}
*/
const sequenceToIntervals = function (valuesArray, allArraysResult, breakDetectorFunc) {
if (!breakDetectorFunc){
breakDetectorFunc = defaultIntDetector;
}
if (typeof(allArraysResult) === 'undefined'){
allArraysResult = false;
}
const intervals = [];
let from = 0, to;
if (valuesArray instanceof Array) {
const cnt = valuesArray.length;
for (let i = 0; i < cnt; i++) {
to = i;
if (i < cnt - 1) { // i is not last (to compare to next)
if (breakDetectorFunc(valuesArray[i], valuesArray[i + 1])) {
// break
appendLastResult();
}
}
}
appendLastResult();
} else {
throw new Error("input is not an Array");
}
function appendLastResult(){
if (isFinite(from) && isFinite(to)) {
const vFrom = valuesArray[from];
const vTo = valuesArray[to];
if (from === to) {
intervals.push(
allArraysResult
? [vFrom, vTo] // same values array item
: vFrom // just a value, no array
);
} else if (Math.abs(from - to) === 1) { // sibling items
if (allArraysResult) {
intervals.push([vFrom, vFrom]);
intervals.push([vTo, vTo]);
} else {
intervals.push(vFrom, vTo);
}
} else {
intervals.push([vFrom, vTo]); // true interval
}
from = to + 1;
}
}
return intervals;
};
module.exports = sequenceToIntervals;
/** #callback SequenceToIntervalsBreakDetector
#param value1
#param value2
#return bool
*/
first argument is the input sequence sorted array, second is a boolean flag controlling the output mode: if true - single item (outside the intervals) will be returned as arrays anyway: [1,7],[9,9],[10,10],[12,20], otherwise single items returned as they appear in the input array
for your sample input
[2,3,4,5,10,18,19,20]
it will return:
sequenceToIntervals([2,3,4,5,10,18,19,20], true) // [[2,5], [10,10], [18,20]]
sequenceToIntervals([2,3,4,5,10,18,19,20], false) // [[2,5], 10, [18,20]]
sequenceToIntervals([2,3,4,5,10,18,19,20]) // [[2,5], 10, [18,20]]
Here's a version in Coffeescript
getRanges = (array) ->
ranges = []
rstart
rend
i = 0
while i < array.length
rstart = array[i]
rend = rstart
while array[i + 1] - array[i] is 1
rend = array[i + 1] # increment the index if the numbers sequential
i = i + 1
if rstart == rend
ranges.push rstart + ''
else
ranges.push rstart + '-' + rend
i = i + 1
return ranges
I've written my own method that's dependent on Lo-Dash, but doesn't just give you back an array of ranges, rather, it just returns an array of range groups.
[1,2,3,4,6,8,10] becomes:
[[1,2,3,4],[6,8,10]]
http://jsfiddle.net/mberkom/ufVey/

Categories