Flattening two arrays of date ranges in JavaScript? - javascript

I am building an availability calendar. This is a monthly view, and days are essentially a boolean, they can be either available or unavailable. There are multiple "products" that can exist on the calendar. Relatively simple.
I store these "availability ranges" as an array of objects to be as terse as possible. So a possible data set for a single product looks like this:
[
{
"startDate": "2016-11-08",
"endDate": "2016-11-08"
},
{
"startDate": "2016-11-11",
"endDate": "2016-11-14"
},
{
"startDate": "2016-11-20",
"endDate": "2016-11-22"
}
]
The UI looks very similar to this calendar:
The real trouble comes when users update their availability. They have the choice to "update all" or "update one". For example, if Room 1 was already unavailable on January 5th, and the user now wants to make all rooms unavailable from January 1st to January 10th, I need to remove the Room 1 January 5th object from the array, because it overlaps with the new data.
Additionally, I'd like to merge any timespans that are contiguous, eg:
[
{
"startDate": "2016-11-08",
"endDate": "2016-11-08"
},
{
"startDate": "2016-11-09",
"endDate": "2016-11-09"
},
]
Should be merged to:
[
{
"startDate": "2016-11-08",
"endDate": "2016-11-09"
},
]
I realise this is a relatively complex question, but surely there must be a pre-existing solution, or at least something similar?
I have access to momentJs.

Here is a function (ES6) you could use to merge two arrays, each containing periods. I applied it below to some sample data which is a bit more extended than the data you provided, so it covers several cases of overlapping and adjacency:
function mergePeriods(a, b) {
return a.concat(b)
.sort( (a, b) => a.startDate.localeCompare(b.startDate) )
.reduce( ([res, end], p) =>
new Date(p.startDate).getTime()<=new Date(end).getTime()+90000000
? p.endDate > end
? [res, res[res.length-1].endDate = p.endDate]
: [res, end]
: [res.concat(p), p.endDate],
[[], '1970-01-01'])[0];
}
// sample data
var unavailable1 = [
{
"startDate": "2016-11-08",
"endDate": "2016-11-08"
},
{
"startDate": "2016-11-11",
"endDate": "2016-11-14"
},
{
"startDate": "2016-11-20",
"endDate": "2016-11-22"
},
{
"startDate": "2016-11-27",
"endDate": "2016-11-27"
},
{
"startDate": "2016-11-29",
"endDate": "2016-11-29"
}
];
var unavailable2 = [
{
"startDate": "2016-11-09",
"endDate": "2016-11-09"
},
{
"startDate": "2016-11-12",
"endDate": "2016-11-15"
},
{
"startDate": "2016-11-18",
"endDate": "2016-11-21"
},
{
"startDate": "2016-11-26",
"endDate": "2016-11-28"
}
];
// merge the sample data
var res = mergePeriods(unavailable1, unavailable2);
console.log(res);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Explanation
As a first step the two arrays are concatenated and sorted by increasing start date.
Then reduce is called on that array, with the start value (for the accumulator) equal to:
[[], '1970-01-01']
This pair consists of the array that will be accumulated to the final result (it starts empty), and the last encountered endDate date, which is set to a time long ago.
This pair is received in the reduce callback as [res, end], and the current element from the array is named p. Then some comparisons are made to detect how the period p relates to end. In case of overlap are adjacency, the previous element in the current result is updated (extended) to match the p.endDate, which also becomes the new value of end in the next iteration.
In case there is a complete inclusion of p in the previous period, it is just ignored, and [res, end] are retained as they are.
If the period p is disjoint from the previous one, it is concatenated to the result (with concat) and the end is set to this p.endDate.
When the result is composed that way, and reduce returns, we are no longer interested in the latest end date, but only the array, which explains the final [0].
About 90000000
The value 90000000 represents the number of milliseconds in 25 hours. This is to detect if two periods are adjacent. 1 hour extra does not hurt, and will deal well with overnight DST changes. This could alternatively be done with momentjs, but this is not too cumbersome in plain JavaScript either.

I don't know if this is the Fastest approach, but a good place to start would be to make use of the Array.sort to normalize the data by date so that any sequential dates will be next to each other, then use a simple array traversal to coalesce sequential dates.
The example below works with sort an array traversal.
let states = [
{
"startDate": "2016-11-08",
"endDate": "2016-11-08"
},
{
"startDate": "2016-11-11",
"endDate": "2016-11-14"
},
{
"startDate": "2016-11-20",
"endDate": "2016-11-22"
},
{
"startDate": "2016-11-15",
"endDate": "2016-11-19"
},
];
console.log('Started With:', states);
let sorted_states = states.sort(function(a,b){
return (new Date(a.startDate)) - (new Date(b.startDate))
});
// simple array traversal method
for (let i = sorted_states.length - 1; i--;){
let current = sorted_states[i];
let next = sorted_states[i+1];
interval = ((new Date(next.startDate) - new Date(current.endDate)));
if (interval <= (1000 * 60 * 60 * 24)){ // one day
newEntry = {
startDate : current.startDate,
endDate : next.endDate,
};
sorted_states[i] = newEntry;
sorted_states.splice(i+1, 1); // delete coalesced entry
}
}
console.log('Ended With:', sorted_states)

You could use two objects as hash table for start and end dates and use use moments for getting the date of tomorrow and look if the date is a start date.
Then update the object, delete from start and end object, and register again.
This proposal works with unsorted data.
function getTomorrow(day) {
return moment(day).add(1, 'days').format("YYYY-MM-DD");
}
var array = [{ startDate: "2016-11-08", endDate: "2016-11-08" }, { startDate: "2016-11-09", endDate: "2016-11-09" }, { startDate: "2016-12-02", endDate: "2016-12-02" }, { startDate: "2016-12-03", endDate: "2016-12-03" }, { startDate: "2016-12-01", endDate: "2016-12-01" }, { startDate: "2016-12-04", endDate: "2016-12-04" }, ],
start = {},
end = {},
result;
array.forEach(function (object) {
start[object.startDate] = object;
end[object.endDate] = object;
});
Object.keys(end).forEach(function (today) {
var tomorrow = getTomorrow(today);
if (start[tomorrow]) {
end[today].endDate = start[tomorrow].endDate;
end[tomorrow] = end[today];
delete end[today];
delete start[tomorrow];
}
});
result = Object.keys(start).map(function (k) {
return start[k];
});
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.15.2/moment.min.js"></script>

Use isbetween method from moment.
Design : after user selects a starting and end date , use his start date and compare it with each entry in your dates array (arr) if some entry has start date inside user selected range , delete it .After all entries are iterated upon , add user selected range to arr
userSdate="2016-11-08"
userEdate="2016-11-09"
for(i=0;i<arr.length;i++)
{
tmpSdate = arr[i].startDate;
tmpEdate = arr[i].endDate;
console.log( tmpSdate + " : " + tmpEdate );
isIn=moment(tmpSdate ).isBetween(userSdate,userEdate, null, '[]'); //note:[] means inclusive range
if(isIn)
{
delete(arr[i]);//remove arr[i]
}
}
arr.push({ "startDate": userSdate, "endDate" : userEdate});
Since delete doesn't reindex and update length property you might want to reindex it, though that's not mandatory.
Note that i have assumed there is no "range crossing" (when start of a range A lies completely inside range B but not it's(A) end does not) , see below
1 1
A-2------2----3----3--4----------4-----B (no range crossing)
C------D E----F G----------H
A--2------------------2--4--------------4---B (range crossing)
C-------3----------D G--3-----------H
E----------------F

Related

Javascript: Removing Semi-Duplicate Objects within an Array with Conditions

I am trying to remove the "Duplicate" objects within an array while retaining the object that has the lowest value associated with it.
~~Original
var array = [
{
"time": "2021-11-12T20:37:11.112233Z",
"value": 3.2
},
{
"time": "2021-11-12T20:37:56.115222Z",
"value": 3.8
},
{
"time": "2021-11-13T20:37:55.112255Z",
"value": 4.2
},
{
"time": "2021-11-13T20:37:41.112252Z",
"value": 2
},
{
"time": "2021-11-14T20:37:22.112233Z",
"value": 3.2
}
]
~~Expected Output
var array = [
{
"time": "2021-11-12T20:37:11.112233Z",
"value": 3.2
},
{
"time": "2021-11-13T20:37:41.112252Z",
"value": 2
},
{
"time": "2021-11-14T20:37:22.112233Z",
"value": 3.2
}
]
What I have so far:
var result = array.reduce((aa, tt) => {
if (!aa[tt.time]) {
aa[tt.time] = tt;
} else if (Number(aa[tt.time].value) < Number(tt.value)) {
aa[tt.time] = tt;
}
return aa;
}, {});
console.log(result);
I realize the issue with what I am trying to do is that the "time" attribute is not identical to the other time values I am considering as duplicates.
Though for this use case I do not need the time out to ms. YYYY-MM-DDTHH:MM (to the minute) is fine. I am not sure how to implement a reduction method for this case when the time isnt exactly the same. Maybe if only the first 16 characters were checked in the string?
Let me know if any additional information is needed.
So a few issues:
If you want to only check the first 16 characters to detect a duplicate, you should use that substring of tt.time as key for aa instead of the whole string.
Since you want the minimum, your comparison operator is wrong.
The code produces an object, while you want an array, so you still need to extract the values from the object.
Here is your code with those adaptations:
var array = [{"time": "2021-11-12T20:37:11.112233Z","value": 3.2},{"time": "2021-11-12T20:37:56.115222Z","value": 3.8},{"time": "2021-11-13T20:37:55.112255Z","value": 4.2},{"time": "2021-11-13T20:37:41.112252Z","value": 2},{"time": "2021-11-14T20:37:22.112233Z","value": 3.2}];
var result = Object.values(array.reduce((aa, tt) => {
var key = tt.time.slice(0, 16);
if (!aa[key]) {
aa[key] = tt;
} else if (Number(aa[key].value) > Number(tt.value)) {
aa[key] = tt;
}
return aa;
}, {}));
console.log(result);

Can't Sort data from an array. sort is not a function

I'm trying to sort the data from an array, from the minor to the largest one. and I get the error: time.sort is not a function, time.sort is undefined.
Here is my code:
{
this.state.measure.map((data, index) => {
let time = data.duration
const totalTime = time.sort((a, b) => parseFloat(a.time) - parseFloat(b.time))
console.log(totalTime)
})
}
If I console.log(time) I receive 15:00, 12:00, 50:00, 15:10 that is the data that I want to sort, but first I need to convert to numbers using parseFloat() and later use to order, anyway, this is not the problem.
Looks like that the sort() is the problem in my code.
This is my JSON.
{
"measures": [
{
"_id": "5fb56d5e734b7e04b9c97c9d",
"creationDate": "18/11/2020",
"startTime": "10:00",
"duration": "15:00",
"voltage": "220V",
"avgPower": "34W",
"maxPower": "7200W",
"WaterConsumption": "30",
"avgTemperature": "42",
"maxTemperature": "48",
"shower": "5fb56ce7734b7e04b9c97c9b",
"__v": 0,
"id": "5fb56d5e734b7e04b9c97c9d"
},
]
}
I have three objects like this one and I just want to order them from the smaller to the largest one.
I tried to change the code to
{this.state.measure.map((data, index)=> {
let time = parseFloat(data.duration)
console.log(time)
})}
Now I receive 15, 12, 50, 17.1
You should sort the measures array instead.
this.state.measure.sort((a,b)=>{
const [m, s] = a.time.split(":").map(Number), [m2, s2] = b.time.split(":").map(Number);
return m * 60 + s - (m2 * 60 + s2);
});
If I am not wrong then you are wanting something like this-
const data = {
"measures": [
{
"_id": "5fbaaa",
"duration": "15:00",
},
{
"_id": "5fbbdd",
"duration": "12:20",
},
{
"_id": "5fbbbb",
"duration": "12:00",
},
{
"_id": "5fbccc",
"duration": "25:30",
},
]
};
const inMin = (duration) => {
const [hour, min] = duration.split(':');
return hour * 60 + min * 1;
}
data.measures.sort((a, b) => {
return inMin(b.duration) - inMin(a.duration);
});
console.log(data);
Here I've shortened your data structure and assume your duration in HH:mm format and convert the duration in minute and sorting in desc order.
as the time format is in string and have : before using parseFloat
Just do time.split(':').join('.')
this.state.measure.map((data, index) => {
let time = data.duration
const totalTime = time.sort((a, b) => parseFloat(a.time.split(':').join('.') )- parseFloat(b.time.split(':').join('.')))
console.log(totalTime)
})
Okay, so sounds like you want to sort the array. For that, you should use the Array.sort method instead of Array.map.
The following piece of code will sort the array comparing duration values. String.localeCompare is a handy function for that, since it already converts the date for you.
this.state.measures.sort((a,b) => a.duration.localeCompare(b.duration))

Using lodash or a similar library, how do you sort collections by closest result?

I have an array of objects and I'm filtering our these objects based on the user input. Now what I'd like to do is sort the objects by closest matching.
_internalSearch = (input) => {
const { data } = this.props;
const filteredData = _.filter(data, (collection) => {
const subCollection = _.pick(collection, ["name", "alias"];
return _.includes(
subCollection.toString().toLowerCase(),
input.toString().toLowerCase()
);
});
//psudo code:
/**
return _.sortBy( items-by-closest-matching-search )
*/
}
Where data looks something like:
[
{
"id": 2,
"name": "Guide",
"url": "http://gify.net",
"alias": "Maps"
},
{
"id": 0,
"name": "Summa",
"alias": "Fun Town"
},
{
"id": 1,
"name": "Mars",
"url": "https://funstuff.org",
"alias": "Dentist"
}
]
Now if the input is ma all three results show, which is good, but I would like to sort these results by closest-matching by name. So the results would be in the order:
Mars, Summa, Guide
I guess the process would be:
1) Sort alphabetically (optional)
2) Sort on "alias" by distance of substring from start of string
3) Sort on "name" by distance of substring from start of string
I was able to figure it out myself. The solution is pretty simple.
I ended up caring to sort the aliases and found that sorting on name was plenty. Sorting on aliases increased my computational complexity for, in my use case, practically no gain.
sortResults = (data, input) => {
_.sortBy(data, [
({ name }) => {
const index = name.indexOf(input);
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
}
]);
};

Converting strings to intergers in JSON using JavaScript

I have the following json
{
"Title": "Test",
"StartDate": {
"Month": "3",
"Year": "1973"
},
"EndDate": {
"Month": "4",
"Year": "1974"
}
}
I want Month and Year values from StartDate and EndDate to be without quotes, like this:
{
"Title": "Test",
"StartDate": {
"Month": 3,
"Year": 1973
},
"EndDate": {
"Month": 4,
"Year": 1974
}
}
EDIT
I'm not creating the json, with JSON.stringify(). My JSON is created by Form Builder module from Angular 2, despite the fact that I'm setting it's type to number, after I change the value, the value gets quotes.
Before saving your JSON, use the parseInt() functions to convert your values into integers. This will remove the quotes.
JSON.stringify({
Month: parseInt( value , 10);
})
See this answer
EDIT
If you made that JSON Object earlier in your JavaScript code, go for #Adrian Lambertz's answer. If you got that JSON as a String from somewhere else and want to convert it, read my answer :
My original answer
Say you got this JSON as a string in your JavaScript code, you could convert the desired values to integers like this :
var events = JSON.parse(JSONStringYouWantToConvert);
// if the JSON String is an array of events that all have a Title, a StartDate and an EndDate
for (var i = 0; i < events.length; i++) {
// else, forget about the loop and the [i] index, the concept remains the same
events[i].StartDate.Month = parseInt(events[i].StartDate.Month);
events[i].StartDate.Year = parseInt(events[i].StartDate.Year);
events[i].EndDate.Month = parseInt(events[i].EndDate.Month);
events[i].EndDate.Year = parseInt(events[i].EndDate.Year);
}
// make a JSON String that wont have the quotes around the Month and Year numbers
var JSONStringConverted = JSON.stringify(events);
Just convert the strings to numbers.
This code is just an example, you can adapt it to whatever your object structure is.
function normalize(target) {
for (const date of ['StartDate', 'EndDate']) {
for (const item of ['Month', 'Year']) {
target[date][item] = Number(target[date][item]);
}
}
}
I can not see the order from which data the result is expeceted, so here two versions.
Object -> JSON (-> Object)
This works with JSON.stringify
and a replacer function for creating numbers for certain keys, like Month and Year.
var dataObj = { "Title": "Test", "StartDate": { "Month": "3", "Year": "1973" }, "EndDate": { "Month": "4", "Year": "1974" } },
jsonStr = JSON.stringify(dataObj, function (k, v) {
return ['Month', 'Year'].indexOf(k) !== -1 ? +v : v;
}),
parsed = JSON.parse(jsonStr);
console.log(jsonStr);
console.log(parsed);
JSON -> Object
This works with JSON.parse
and a reviver function for creating numbers for certain keys, like Month and Year.
var jsonStr = '{ "Title": "Test", "StartDate": { "Month": "3", "Year": "1973" }, "EndDate": { "Month": "4", "Year": "1974" } }',
parsed = JSON.parse(jsonStr, function (k, v) {
return ['Month', 'Year'].indexOf(k) !== -1 ? +v : v;
});
console.log(parsed);

return conditional week days using reduceRight

i am trying to get specific week(such as saturday) days collection from my mongodb. here is the example DB collection
var array2 = [
{
"_id": "1",
"start": "2016-08-23T16:00:00.000Z",
},
{
"_id": "2",
"start": "2016-08-26T21:00:00.000Z",
},
{
"_id": "3",
"start": "2016-08-23T16:10:00.000Z",
}];
javascript week reference
to get Saturday entry i run a map, and here is the code
array2.map(function(object){
var newObject = {};
var nedatee = new Date(object.start);
var weekday = nedatee.getDay();
if (weekday===6) {
newObject['start'] = true;
}
return newObject;
});
the output is
[ {}, {'start':true}, {}]
i dont want the empty object in my array, and i dont want to manually remove it, is there any smarted way? how can i make good use of reduceRight ?
all i want is
[{'start':true}]
javascript map reference if you need
You could use reduceRight and splice the failures:
var array2 = [
{
"_id": "1",
"start": "2016-08-23T16:00:00.000Z",
},
{
"_id": "2",
"start": "2016-08-26T21:00:00.000Z",
},
{
"_id": "3",
"start": "2016-08-23T16:10:00.000Z",
}];
array2.reduceRight(function(acc,v,i) {
if (new Date(v.start).getUTCDay() != 6){
array2.splice(i,1);
}
},null)
console.log(array2);
Note that the date will be parsed as UTC, so 2016-08-26T21:00:00.000Z will be Saturday in any timezone that is UTC+0300 or more, hence use of getUTCDay (i.e. none of the days is Saturday in the GMT time zone).
Also, you should not really rely on parsing any date string with the Date constructor. You should use a parser and provide the format, see Chrome can't parse date but IE can.
Have you considered using javascript some? Your code would then be
var hasSaturdays = array2.some(function isSaturday(object){
var nedatee = new Date(object.start);
return nedatee.getDay() === 6;
});
Then if hasSaturdays is true then you can return [{'start':true}]
Please note that it is only IE 9+
Using UTC time zone or not this is a simple filtering work and there is Array.prototype.filter() just for this job.
var array2 = [
{
"_id": "1",
"start": "2016-08-23T16:00:00.000Z",
},
{
"_id": "2",
"start": "2016-08-26T21:00:00.000Z",
},
{
"_id": "3",
"start": "2016-08-23T16:10:00.000Z",
}],
saturdays = array2.filter(date => (new Date(date.start)).getDay() === 6)
.map(date => ({start:true}));
console.log(saturdays);

Categories

Resources