mirror of
https://gitlab.dit.htwk-leipzig.de/htwk-software/htwkalender-pwa.git
synced 2025-07-16 17:48:51 +02:00
feat/#3 binary decoder for occupancy events and stubs
This commit is contained in:
9
frontend/package-lock.json
generated
9
frontend/package-lock.json
generated
@ -19,6 +19,7 @@
|
|||||||
"bson": "^5.5.1",
|
"bson": "^5.5.1",
|
||||||
"country-flag-emoji-polyfill": "^0.1.8",
|
"country-flag-emoji-polyfill": "^0.1.8",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
|
"date-fns-tz": "^3.1.3",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"primeflex": "^3.3.1",
|
"primeflex": "^3.3.1",
|
||||||
"primeicons": "^6.0.1",
|
"primeicons": "^6.0.1",
|
||||||
@ -4581,6 +4582,14 @@
|
|||||||
"url": "https://github.com/sponsors/kossnocorp"
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns-tz": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-ZfbMu+nbzW0mEzC8VZrLiSWvUIaI3aRHeq33mTe7Y38UctKukgqPR4nTDwcwS4d64Gf8GghnVsroBuMY3eiTeA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"date-fns": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/de-indent": {
|
"node_modules/de-indent": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
"bson": "^5.5.1",
|
"bson": "^5.5.1",
|
||||||
"country-flag-emoji-polyfill": "^0.1.8",
|
"country-flag-emoji-polyfill": "^0.1.8",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
|
"date-fns-tz": "^3.1.3",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"primeflex": "^3.3.1",
|
"primeflex": "^3.3.1",
|
||||||
"primeicons": "^6.0.1",
|
"primeicons": "^6.0.1",
|
||||||
|
@ -36,7 +36,7 @@ export async function fetchRoomOccupancy(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const data = new Uint8Array(roomsResponse);
|
const data = new Uint8Array(roomsResponse);
|
||||||
roomOccupancyList = BSON.deserialize(data) as RoomOccupancyList;
|
roomOccupancyList = RoomOccupancyList.fromJSON(BSON.deserialize(data));
|
||||||
return roomOccupancyList;
|
return roomOccupancyList;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ import { useQuery } from "@tanstack/vue-query";
|
|||||||
import { watch } from "vue";
|
import { watch } from "vue";
|
||||||
import { fetchRoomOccupancy } from "@/api/fetchRoomOccupancy";
|
import { fetchRoomOccupancy } from "@/api/fetchRoomOccupancy";
|
||||||
import { isValid } from "date-fns";
|
import { isValid } from "date-fns";
|
||||||
|
import { RoomOccupancyList } from "@/model/roomOccupancyList";
|
||||||
|
|
||||||
const { t } = useI18n({ useScope: "global" });
|
const { t } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
@ -70,6 +71,22 @@ const mobilePage = inject("mobilePage") as Ref<boolean>;
|
|||||||
|
|
||||||
const selectedRoom = computed(() => props.room);
|
const selectedRoom = computed(() => props.room);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform decoded JSON object with binary data
|
||||||
|
* to anonymized occupancy events
|
||||||
|
* @param data RoomOccupancyList with binary data
|
||||||
|
* @returns Anonymized occupancy events
|
||||||
|
*/
|
||||||
|
function transformData(data: RoomOccupancyList) {
|
||||||
|
const events = data
|
||||||
|
.decodeOccupancy(selectedRoom.value, new Date(currentDateFrom.value), new Date(currentDateTo.value))
|
||||||
|
.map((event, index) => ({
|
||||||
|
id: index,
|
||||||
|
event: event,
|
||||||
|
}));
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
const { data: occupations } = useQuery({
|
const { data: occupations } = useQuery({
|
||||||
queryKey: ["roomOccupation", selectedRoom, currentDateFrom, currentDateTo],
|
queryKey: ["roomOccupation", selectedRoom, currentDateFrom, currentDateTo],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
@ -77,12 +94,7 @@ const { data: occupations } = useQuery({
|
|||||||
new Date(currentDateFrom.value).toISOString(),
|
new Date(currentDateFrom.value).toISOString(),
|
||||||
new Date(currentDateTo.value).toISOString()
|
new Date(currentDateTo.value).toISOString()
|
||||||
),
|
),
|
||||||
select: (data) => data
|
select: (data) => transformData(data),
|
||||||
.decodeOccupancy(selectedRoom.value, new Date(currentDateFrom.value), new Date(currentDateTo.value))
|
|
||||||
.map((event, index) => ({
|
|
||||||
id: index,
|
|
||||||
event: event,
|
|
||||||
})),
|
|
||||||
enabled: () => selectedRoom.value !== "" && currentDateFrom.value !== "",
|
enabled: () => selectedRoom.value !== "" && currentDateFrom.value !== "",
|
||||||
staleTime: 5000000, // 500 seconds
|
staleTime: 5000000, // 500 seconds
|
||||||
});
|
});
|
||||||
|
651
frontend/src/model/roomOccupancyList.test.ts
Normal file
651
frontend/src/model/roomOccupancyList.test.ts
Normal file
@ -0,0 +1,651 @@
|
|||||||
|
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||||
|
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||||
|
|
||||||
|
//This program is free software: you can redistribute it and/or modify
|
||||||
|
//it under the terms of the GNU Affero General Public License as published by
|
||||||
|
//the Free Software Foundation, either version 3 of the License, or
|
||||||
|
//(at your option) any later version.
|
||||||
|
|
||||||
|
//This program is distributed in the hope that it will be useful,
|
||||||
|
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
//GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
//You should have received a copy of the GNU Affero General Public License
|
||||||
|
//along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { beforeEach, describe, expect, test } from "vitest";
|
||||||
|
import { RoomOccupancyList } from "./roomOccupancyList";
|
||||||
|
import { Binary } from "bson";
|
||||||
|
import { add, addHours, addMinutes, interval, subHours } from "date-fns";
|
||||||
|
import { fromZonedTime, toZonedTime } from "date-fns-tz";
|
||||||
|
|
||||||
|
const testListStart = new Date("2022-01-01T00:00:00Z");
|
||||||
|
var testList : RoomOccupancyList; //= RoomOccupancyList.fromJSON({});
|
||||||
|
var alternating : Uint8Array = new Uint8Array(Array(4).fill(0xF0));
|
||||||
|
var booked : Uint8Array = new Uint8Array(Array(4).fill(0xFF));
|
||||||
|
var empty : Uint8Array = new Uint8Array(Array(4).fill(0x00));
|
||||||
|
var counting : Uint8Array = new Uint8Array([0x00, 0x01, 0x02, 0x03]);
|
||||||
|
|
||||||
|
const localTimezone = "Europe/Berlin";
|
||||||
|
|
||||||
|
describe("RoomOccupancyList", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
alternating = new Uint8Array(Array(4).fill(0xF0));
|
||||||
|
booked = new Uint8Array(Array(4).fill(0xFF));
|
||||||
|
empty = new Uint8Array(Array(4).fill(0x00));
|
||||||
|
counting = new Uint8Array([0x00, 0x01, 0x02, 0x03]);
|
||||||
|
testList = RoomOccupancyList.fromJSON({
|
||||||
|
start: testListStart,
|
||||||
|
granularity: 60,
|
||||||
|
blocks: 32,
|
||||||
|
rooms: [
|
||||||
|
{
|
||||||
|
name: "BOOKED",
|
||||||
|
occupancy: new Binary(booked, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EMPTY",
|
||||||
|
occupancy: new Binary(empty, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ALTERNATING",
|
||||||
|
occupancy: new Binary(alternating, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "COUNTING",
|
||||||
|
occupancy: new Binary(counting, 0),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getRooms", () => {
|
||||||
|
test("get rooms", () => {
|
||||||
|
// act
|
||||||
|
const rooms = testList["getRooms"]();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(rooms).toEqual([
|
||||||
|
"BOOKED",
|
||||||
|
"EMPTY",
|
||||||
|
"ALTERNATING",
|
||||||
|
"COUNTING"
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("get empty rooms", () => {
|
||||||
|
// arrange
|
||||||
|
const emptyRoomOccupancy = RoomOccupancyList.fromJSON({
|
||||||
|
start: testListStart,
|
||||||
|
granularity: 60,
|
||||||
|
blocks: 32,
|
||||||
|
rooms: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// act
|
||||||
|
const rooms = emptyRoomOccupancy["getRooms"]();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(rooms).toEqual([]);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("decodeOccupancy", () => {
|
||||||
|
test("generate stubs for missing room", () => {
|
||||||
|
// arrange
|
||||||
|
|
||||||
|
// act
|
||||||
|
const decoded = testList["decodeOccupancy"](
|
||||||
|
"MISSING",
|
||||||
|
testListStart,
|
||||||
|
addHours(testListStart, 8),
|
||||||
|
);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(decoded).toEqual([
|
||||||
|
{
|
||||||
|
start: "2022-01-01T06:00:00.000Z",
|
||||||
|
end: "2022-01-01T08:00:00.000Z",
|
||||||
|
rooms: "MISSING",
|
||||||
|
free: true,
|
||||||
|
stub: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("generate stubs out of range", () => {
|
||||||
|
// arrange
|
||||||
|
|
||||||
|
// act
|
||||||
|
const decoded = testList["decodeOccupancy"](
|
||||||
|
"BOOKED",
|
||||||
|
subHours(testListStart, 10),
|
||||||
|
testListStart,
|
||||||
|
);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(decoded).toEqual([
|
||||||
|
{
|
||||||
|
start: "2021-12-31T14:00:00.000Z",
|
||||||
|
end: "2021-12-31T19:00:00.000Z",
|
||||||
|
rooms: "BOOKED",
|
||||||
|
free: true,
|
||||||
|
stub: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("decode occupancy in range", () => {
|
||||||
|
// arrange
|
||||||
|
// act
|
||||||
|
const decoded = testList["decodeOccupancy"](
|
||||||
|
"BOOKED",
|
||||||
|
subHours(testListStart, 2),
|
||||||
|
addHours(testListStart, 8),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(decoded).toEqual([
|
||||||
|
{
|
||||||
|
start: "2022-01-01T00:00:00.000Z",
|
||||||
|
end: "2022-01-01T08:00:00.000Z",
|
||||||
|
rooms: "BOOKED",
|
||||||
|
free: false,
|
||||||
|
stub: false
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sliceOccupancy", () => {
|
||||||
|
test.each([
|
||||||
|
booked,
|
||||||
|
empty,
|
||||||
|
alternating
|
||||||
|
])("getCompleteOccupancy of %j", (occupancy) => {
|
||||||
|
// arrange
|
||||||
|
const startTime = new Date(testList.start);
|
||||||
|
const endTime = new Date(addHours(startTime, 32));
|
||||||
|
const sliceInterval = interval(startTime, endTime);
|
||||||
|
|
||||||
|
// act
|
||||||
|
const sliced = testList["sliceOccupancy"](
|
||||||
|
sliceInterval,
|
||||||
|
occupancy
|
||||||
|
)
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(sliced).toEqual({
|
||||||
|
decodeSliceStart: startTime,
|
||||||
|
decodeSlice: new Uint8Array(occupancy),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws start out of bounds", () => {
|
||||||
|
// arrange
|
||||||
|
const startTime = new Date(subHours(testList.start,1));
|
||||||
|
const endTime = new Date(addHours(startTime, 32));
|
||||||
|
const sliceInterval = interval(startTime, endTime);
|
||||||
|
|
||||||
|
// act and assert
|
||||||
|
expect(() => {
|
||||||
|
testList["sliceOccupancy"](
|
||||||
|
sliceInterval,
|
||||||
|
alternating
|
||||||
|
)
|
||||||
|
}).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws end out of bounds", () => {
|
||||||
|
// arrange
|
||||||
|
const startTime = new Date(testList.start);
|
||||||
|
const endTime = new Date(addHours(startTime, 33));
|
||||||
|
const sliceInterval = interval(startTime, endTime);
|
||||||
|
|
||||||
|
// act and assert
|
||||||
|
expect(() => {
|
||||||
|
testList["sliceOccupancy"](
|
||||||
|
sliceInterval,
|
||||||
|
alternating
|
||||||
|
)
|
||||||
|
}).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("range at byte boundaries", () => {
|
||||||
|
// arrange
|
||||||
|
const startTime = new Date(addHours(testList.start, 8));
|
||||||
|
const endTime = new Date(addHours(testList.start, 24));
|
||||||
|
const sliceInterval = interval(startTime, endTime);
|
||||||
|
|
||||||
|
// act
|
||||||
|
const sliced = testList["sliceOccupancy"](
|
||||||
|
sliceInterval,
|
||||||
|
counting
|
||||||
|
)
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(sliced).toEqual({
|
||||||
|
decodeSliceStart: startTime,
|
||||||
|
decodeSlice: new Uint8Array([0x01, 0x02]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("range within byte boundaries", () => {
|
||||||
|
// arrange
|
||||||
|
const startTime = new Date(addMinutes(testListStart, 500));
|
||||||
|
const endTime = new Date(addHours(testListStart, 15));
|
||||||
|
const sliceInterval = interval(startTime, endTime);
|
||||||
|
|
||||||
|
// act
|
||||||
|
const sliced = testList["sliceOccupancy"](
|
||||||
|
sliceInterval,
|
||||||
|
counting
|
||||||
|
)
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(sliced).toEqual({
|
||||||
|
decodeSliceStart: addHours(testListStart,8),
|
||||||
|
decodeSlice: new Uint8Array([0x01]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getOccupancyInterval", () => {
|
||||||
|
test("get empty interval", () => {
|
||||||
|
// arrange
|
||||||
|
const emptyRoomOccupancy = RoomOccupancyList.fromJSON({
|
||||||
|
start: testListStart,
|
||||||
|
granularity: 60,
|
||||||
|
blocks: 0,
|
||||||
|
rooms: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// act
|
||||||
|
const emptyInterval = emptyRoomOccupancy["getOccupancyInterval"]();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(emptyInterval).toEqual(interval(testListStart, testListStart));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("get interval of valid room occupancy list", () => {
|
||||||
|
// act
|
||||||
|
const testInterval = testList["getOccupancyInterval"]();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(testInterval).toEqual(interval(testListStart, addHours(testListStart, 32)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("decodeOccupancyData", () => {
|
||||||
|
test("decode occupancy without length", () => {
|
||||||
|
// arrange
|
||||||
|
// act
|
||||||
|
const decoded = RoomOccupancyList["decodeOccupancyData"](
|
||||||
|
new Uint8Array([]),
|
||||||
|
testListStart,
|
||||||
|
15,
|
||||||
|
"Raum"
|
||||||
|
);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(decoded).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("decode blocked occupancy", () => {
|
||||||
|
// arrange
|
||||||
|
// act
|
||||||
|
const decoded = RoomOccupancyList["decodeOccupancyData"](
|
||||||
|
booked,
|
||||||
|
testListStart,
|
||||||
|
15,
|
||||||
|
"BOOKED"
|
||||||
|
);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(decoded).toEqual([
|
||||||
|
{
|
||||||
|
start: testListStart.toISOString(),
|
||||||
|
end: addHours(testListStart, 8).toISOString(),
|
||||||
|
rooms: "BOOKED",
|
||||||
|
free: false,
|
||||||
|
stub: false
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("decode empty occupancy", () => {
|
||||||
|
// arrange
|
||||||
|
// act
|
||||||
|
const decoded = RoomOccupancyList["decodeOccupancyData"](
|
||||||
|
empty,
|
||||||
|
testListStart,
|
||||||
|
15,
|
||||||
|
"BOOKED"
|
||||||
|
);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(decoded).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("decode alternating occupancy", () => {
|
||||||
|
// arrange
|
||||||
|
// act
|
||||||
|
const decoded = RoomOccupancyList["decodeOccupancyData"](
|
||||||
|
alternating,
|
||||||
|
new Date("2024-01-01T00:00:00Z"),
|
||||||
|
15,
|
||||||
|
"ALTERNATING"
|
||||||
|
);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(decoded).toEqual([
|
||||||
|
{
|
||||||
|
start: "2024-01-01T00:00:00.000Z",
|
||||||
|
end: "2024-01-01T01:00:00.000Z",
|
||||||
|
rooms: "ALTERNATING",
|
||||||
|
free: false,
|
||||||
|
stub: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: "2024-01-01T02:00:00.000Z",
|
||||||
|
end: "2024-01-01T03:00:00.000Z",
|
||||||
|
rooms: "ALTERNATING",
|
||||||
|
free: false,
|
||||||
|
stub: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: "2024-01-01T04:00:00.000Z",
|
||||||
|
end: "2024-01-01T05:00:00.000Z",
|
||||||
|
rooms: "ALTERNATING",
|
||||||
|
free: false,
|
||||||
|
stub: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: "2024-01-01T06:00:00.000Z",
|
||||||
|
end: "2024-01-01T07:00:00.000Z",
|
||||||
|
rooms: "ALTERNATING",
|
||||||
|
free: false,
|
||||||
|
stub: false
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("generateStubEvents", () => {
|
||||||
|
test("no events if negative range", () => {
|
||||||
|
// arrange
|
||||||
|
const startTime = new Date("2022-01-01T00:00:00Z");
|
||||||
|
const endTime = new Date("2021-01-01T00:00:00Z");
|
||||||
|
|
||||||
|
// act
|
||||||
|
const stubEvents = RoomOccupancyList["generateStubEvents"](
|
||||||
|
"ROOM",
|
||||||
|
startTime,
|
||||||
|
endTime
|
||||||
|
);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(stubEvents).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("no events if start and end the same", () => {
|
||||||
|
// arrange
|
||||||
|
const startTime = new Date("2022-01-01T00:00:00Z");
|
||||||
|
const endTime = new Date("2022-01-01T00:00:00Z");
|
||||||
|
|
||||||
|
// act
|
||||||
|
const stubEvents = RoomOccupancyList["generateStubEvents"](
|
||||||
|
"ROOM",
|
||||||
|
startTime,
|
||||||
|
endTime
|
||||||
|
);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(stubEvents).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("generate week", () => {
|
||||||
|
// arrange
|
||||||
|
const startTime = new Date("2022-01-01T12:00:00Z");
|
||||||
|
const endTime = new Date("2022-01-07T12:30:00Z");
|
||||||
|
|
||||||
|
// act
|
||||||
|
const stubEvents = RoomOccupancyList["generateStubEvents"](
|
||||||
|
"ROOM",
|
||||||
|
startTime,
|
||||||
|
endTime
|
||||||
|
);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(stubEvents).toEqual([
|
||||||
|
{
|
||||||
|
start: "2022-01-01T12:00:00.000Z",
|
||||||
|
end: "2022-01-01T19:00:00.000Z",
|
||||||
|
rooms: "ROOM",
|
||||||
|
free: true,
|
||||||
|
stub: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: "2022-01-02T06:00:00.000Z",
|
||||||
|
end: "2022-01-02T19:00:00.000Z",
|
||||||
|
rooms: "ROOM",
|
||||||
|
free: true,
|
||||||
|
stub: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: "2022-01-03T06:00:00.000Z",
|
||||||
|
end: "2022-01-03T19:00:00.000Z",
|
||||||
|
rooms: "ROOM",
|
||||||
|
free: true,
|
||||||
|
stub: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: "2022-01-04T06:00:00.000Z",
|
||||||
|
end: "2022-01-04T19:00:00.000Z",
|
||||||
|
rooms: "ROOM",
|
||||||
|
free: true,
|
||||||
|
stub: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: "2022-01-05T06:00:00.000Z",
|
||||||
|
end: "2022-01-05T19:00:00.000Z",
|
||||||
|
rooms: "ROOM",
|
||||||
|
free: true,
|
||||||
|
stub: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: "2022-01-06T06:00:00.000Z",
|
||||||
|
end: "2022-01-06T19:00:00.000Z",
|
||||||
|
rooms: "ROOM",
|
||||||
|
free: true,
|
||||||
|
stub: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: "2022-01-07T06:00:00.000Z",
|
||||||
|
end: "2022-01-07T12:30:00.000Z",
|
||||||
|
rooms: "ROOM",
|
||||||
|
free: true,
|
||||||
|
stub: true,
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("generate day", () => {
|
||||||
|
// arrange
|
||||||
|
const startTime = new Date("2022-01-01T16:00:00Z");
|
||||||
|
const endTime = new Date("2022-01-01T19:00:00Z");
|
||||||
|
|
||||||
|
// act
|
||||||
|
const stubEvents = RoomOccupancyList["generateStubEvents"](
|
||||||
|
"ROOM",
|
||||||
|
startTime,
|
||||||
|
endTime
|
||||||
|
);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(stubEvents).toEqual([
|
||||||
|
{
|
||||||
|
start: "2022-01-01T16:00:00.000Z",
|
||||||
|
end: "2022-01-01T19:00:00.000Z",
|
||||||
|
rooms: "ROOM",
|
||||||
|
free: true,
|
||||||
|
stub: true,
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("shiftTimeForwardInsideWorkday", () => {
|
||||||
|
test("shift time to next day", () => {
|
||||||
|
// arrange
|
||||||
|
const startTime = new Date("2022-01-01T20:00:00Z");
|
||||||
|
|
||||||
|
// act
|
||||||
|
const shiftedTime = RoomOccupancyList["shiftTimeForwardInsideWorkday"](startTime);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(toZonedTime(shiftedTime, localTimezone)).toEqual(new Date("2022-01-01T23:00:00Z"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("don't shift time on the same day", () => {
|
||||||
|
// arrange
|
||||||
|
const startTime = new Date("2022-01-02T01:00:00Z");
|
||||||
|
|
||||||
|
// act
|
||||||
|
const shiftedTime = RoomOccupancyList["shiftTimeForwardInsideWorkday"](startTime);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(toZonedTime(shiftedTime, localTimezone)).toEqual(new Date("2022-01-02T01:00:00Z"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("don't shift if already inside workday", () => {
|
||||||
|
// arrange
|
||||||
|
const startTime = new Date("2022-01-02T12:30:00Z");
|
||||||
|
|
||||||
|
// act
|
||||||
|
const shiftedTime = RoomOccupancyList["shiftTimeForwardInsideWorkday"](startTime);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(toZonedTime(shiftedTime, localTimezone)).toEqual(new Date("2022-01-02T12:30:00Z"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("shiftTimeBackwardInsideWorkday", () => {
|
||||||
|
test("shift time to last day", () => {
|
||||||
|
// arrange
|
||||||
|
const startTime = new Date("2022-01-02T05:30:00Z");
|
||||||
|
|
||||||
|
// act
|
||||||
|
const shiftedTime = RoomOccupancyList["shiftTimeBackwardInsideWorkday"](startTime);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(toZonedTime(shiftedTime, localTimezone)).toEqual(new Date("2022-01-01T22:59:59.999Z"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("don't shift time on the same day", () => {
|
||||||
|
// arrange
|
||||||
|
const startTime = new Date("2022-01-02T22:00:00Z");
|
||||||
|
|
||||||
|
// act
|
||||||
|
const shiftedTime = RoomOccupancyList["shiftTimeBackwardInsideWorkday"](startTime);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(toZonedTime(shiftedTime, localTimezone)).toEqual(new Date("2022-01-02T22:00:00Z"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("don't shift if already inside workday", () => {
|
||||||
|
// arrange
|
||||||
|
const startTime = new Date("2022-01-02T12:30:00Z");
|
||||||
|
|
||||||
|
// act
|
||||||
|
const shiftedTime = RoomOccupancyList["shiftTimeBackwardInsideWorkday"](startTime);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(toZonedTime(shiftedTime, localTimezone)).toEqual(new Date("2022-01-02T12:30:00Z"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setTimeOfDay", () => {
|
||||||
|
test("set time to 00:00:00", () => {
|
||||||
|
// arrange
|
||||||
|
const startTime = new Date("2022-01-02T12:30:00Z");
|
||||||
|
|
||||||
|
// act
|
||||||
|
const shiftedTime = RoomOccupancyList["setTimeOfDay"](startTime, {});
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(shiftedTime).toEqual(new Date("2022-01-01T23:00:00Z"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set time to 23:59:59", () => {
|
||||||
|
// arrange
|
||||||
|
const startTime = new Date("2022-06-02T12:30:00Z");
|
||||||
|
|
||||||
|
// act
|
||||||
|
const shiftedTime = RoomOccupancyList["setTimeOfDay"](startTime, {hours: 23, minutes: 59, seconds: 59});
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(shiftedTime).toEqual(new Date("2022-06-02T21:59:59Z"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set same time", () => {
|
||||||
|
// arrange
|
||||||
|
const startTime = new Date("2022-01-02T12:30:00Z");
|
||||||
|
|
||||||
|
// act
|
||||||
|
const shiftedTime = RoomOccupancyList["setTimeOfDay"](startTime, {hours: 13, minutes: 30, seconds: 0});
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(shiftedTime).toEqual(new Date("2022-01-02T12:30:00Z"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("startOfDay", () => {
|
||||||
|
test("in the winter", () => {
|
||||||
|
// arrange
|
||||||
|
const startTime = new Date("2022-01-02T12:30:00Z");
|
||||||
|
|
||||||
|
// act
|
||||||
|
const shiftedTime = RoomOccupancyList["startOfDay"](startTime);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(shiftedTime).toEqual(new Date("2022-01-01T23:00:00Z"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("in the summer", () => {
|
||||||
|
// arrange
|
||||||
|
const startTime = new Date("2022-06-02T12:30:00Z");
|
||||||
|
|
||||||
|
// act
|
||||||
|
const shiftedTime = RoomOccupancyList["startOfDay"](startTime);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(shiftedTime).toEqual(new Date("2022-06-01:22:00Z"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("endOfDay", () => {
|
||||||
|
test("in the winter", () => {
|
||||||
|
// arrange
|
||||||
|
const startTime = new Date("2022-01-02T12:30:00Z");
|
||||||
|
|
||||||
|
// act
|
||||||
|
const shiftedTime = RoomOccupancyList["startOfDay"](startTime);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(shiftedTime).toEqual(new Date("2022-01-01T23:00:00Z"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("in the summer", () => {
|
||||||
|
// arrange
|
||||||
|
const startTime = new Date("2022-06-02T12:30:00Z");
|
||||||
|
|
||||||
|
// act
|
||||||
|
const shiftedTime = RoomOccupancyList["startOfDay"](startTime);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(shiftedTime).toEqual(new Date("2022-06-01:22:00Z"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -15,8 +15,16 @@
|
|||||||
//along with this program. If not, see <https://www.gnu.org/licenses/>.
|
//along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import { Binary } from "bson";
|
import { Binary } from "bson";
|
||||||
import { AnonymizedEventDTO, AnonymizedOccupancy } from "./event";
|
import { AnonymizedOccupancy } from "./event";
|
||||||
import { Duration, NormalizedInterval, add, addDays, addMinutes, clamp, differenceInMinutes, eachDayOfInterval, endOfDay, interval, isAfter, isBefore, isEqual, max, min, startOfDay, startOfTomorrow, subDays } from "date-fns";
|
import { Duration, NormalizedInterval, add, addDays, addMinutes, clamp, differenceInMinutes, eachDayOfInterval, endOfDay, interval, isAfter, isBefore, isEqual, max, min, startOfDay, subDays } from "date-fns";
|
||||||
|
import { fromZonedTime, toZonedTime } from "date-fns-tz";
|
||||||
|
|
||||||
|
/// The start time of the day. 07:00
|
||||||
|
const START_OF_WORKDAY : Duration = {hours: 7};
|
||||||
|
/// The end time of the day. 20:00
|
||||||
|
const END_OF_WORKDAY : Duration = {hours: 20};
|
||||||
|
/// The timezone of the data (Leipzig)
|
||||||
|
const TIMEZONE = "Europe/Berlin";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the occupancy of a single room.
|
* Represents the occupancy of a single room.
|
||||||
@ -29,11 +37,6 @@ class RoomOccupancy {
|
|||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The start time of the day. 08:00
|
|
||||||
const START_OF_WORKDAY : Duration = {hours: 8};
|
|
||||||
/// The end time of the day. 20:00
|
|
||||||
const END_OF_WORKDAY : Duration = {hours: 20};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the occupancy of multiple rooms.
|
* Represents the occupancy of multiple rooms.
|
||||||
* start is the start date of the occupancy list.
|
* start is the start date of the occupancy list.
|
||||||
@ -65,7 +68,6 @@ export class RoomOccupancyList {
|
|||||||
* @returns a list of AnonymizedEventDTO objects representing the occupancy of the room.
|
* @returns a list of AnonymizedEventDTO objects representing the occupancy of the room.
|
||||||
*/
|
*/
|
||||||
public decodeOccupancy(room : string, from : Date, to : Date) : AnonymizedOccupancy[] {
|
public decodeOccupancy(room : string, from : Date, to : Date) : AnonymizedOccupancy[] {
|
||||||
console.log("Decoding occupancy for room " + room + " from " + from.toISOString() + " to " + to.toISOString());
|
|
||||||
const roomOccupancy = this.rooms.find((r) => r.name === room);
|
const roomOccupancy = this.rooms.find((r) => r.name === room);
|
||||||
|
|
||||||
if (roomOccupancy === undefined) {
|
if (roomOccupancy === undefined) {
|
||||||
@ -77,15 +79,10 @@ export class RoomOccupancyList {
|
|||||||
// Get start and end of decoded time range (within encoded list and requested range)
|
// Get start and end of decoded time range (within encoded list and requested range)
|
||||||
let decodeInterval = interval(clamp(from, this.getOccupancyInterval()), clamp(to, this.getOccupancyInterval()));
|
let decodeInterval = interval(clamp(from, this.getOccupancyInterval()), clamp(to, this.getOccupancyInterval()));
|
||||||
|
|
||||||
// Calculate the slice of bytes, that are needed to decode the requested time range
|
let {decodeSliceStart, decodeSlice} = this.sliceOccupancy(
|
||||||
let minutesFromStart = differenceInMinutes(this.start, decodeInterval.start);
|
decodeInterval,
|
||||||
let minutesToEnd = differenceInMinutes(this.start, decodeInterval.end);
|
roomOccupancy.occupancy.buffer
|
||||||
|
);
|
||||||
let firstByte = Math.floor(minutesFromStart / this.granularity / 8);
|
|
||||||
let lastByte = Math.floor(minutesToEnd / this.granularity / 8);
|
|
||||||
|
|
||||||
let decodeSliceStart = addMinutes(this.start, firstByte * 8 * this.granularity);
|
|
||||||
let decodeSlice = roomOccupancy.occupancy.buffer.slice(firstByte, lastByte + 1);
|
|
||||||
|
|
||||||
// Decode the occupancy data
|
// Decode the occupancy data
|
||||||
occupancyList.push(...RoomOccupancyList.decodeOccupancyData(new Uint8Array(decodeSlice), decodeSliceStart, this.granularity, room));
|
occupancyList.push(...RoomOccupancyList.decodeOccupancyData(new Uint8Array(decodeSlice), decodeSliceStart, this.granularity, room));
|
||||||
@ -102,6 +99,36 @@ export class RoomOccupancyList {
|
|||||||
return occupancyList;
|
return occupancyList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slice the important parts of the occupancy list for a given time range.
|
||||||
|
* @param from the start of the time range.
|
||||||
|
* @param to the end of the time range.
|
||||||
|
* @returns a new occupancy byte array with the starting time of the first byte
|
||||||
|
* @throws an error, if the selected time range is outside of the occupancy list.
|
||||||
|
*/
|
||||||
|
private sliceOccupancy(decodeInterval : NormalizedInterval, occupancy : Uint8Array) : {decodeSliceStart: Date, decodeSlice: Uint8Array} {
|
||||||
|
// Calculate the slice of bytes, that are needed to decode the requested time range
|
||||||
|
// Note: differenceInMinutes calculates (left - right)
|
||||||
|
let minutesFromStart = differenceInMinutes(decodeInterval.start, this.start);
|
||||||
|
let minutesToEnd = differenceInMinutes(decodeInterval.end, this.start);
|
||||||
|
|
||||||
|
let firstByte = Math.floor(minutesFromStart / this.granularity / 8);
|
||||||
|
let lastByte = Math.ceil(minutesToEnd / this.granularity / 8);
|
||||||
|
|
||||||
|
// check if firstByte and lastByte are within the bounds of the occupancy array and throw an error if not
|
||||||
|
if (
|
||||||
|
firstByte < 0 || firstByte >= occupancy.length ||
|
||||||
|
lastByte < 0 || lastByte > occupancy.length
|
||||||
|
) {
|
||||||
|
throw new Error("Requested time range is outside of the occupancy list.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let decodeSliceStart = addMinutes(this.start, firstByte * 8 * this.granularity);
|
||||||
|
let decodeSlice = occupancy.buffer.slice(firstByte, lastByte);
|
||||||
|
|
||||||
|
return { decodeSliceStart, decodeSlice: new Uint8Array(decodeSlice) };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the decoded time interval within the current occupancy list.
|
* Get the decoded time interval within the current occupancy list.
|
||||||
* @returns the interval of the occupancy list.
|
* @returns the interval of the occupancy list.
|
||||||
@ -128,7 +155,7 @@ export class RoomOccupancyList {
|
|||||||
|
|
||||||
// Iterate over all bits in the current byte
|
// Iterate over all bits in the current byte
|
||||||
for (let bit_i = 0; bit_i < 8; bit_i++) {
|
for (let bit_i = 0; bit_i < 8; bit_i++) {
|
||||||
let isOccupied = (byte & (1 << bit_i)) !== 0;
|
let isOccupied = (byte & (1 << (7-bit_i))) !== 0;
|
||||||
|
|
||||||
if (firstOccupancyBit === null && isOccupied) {
|
if (firstOccupancyBit === null && isOccupied) {
|
||||||
firstOccupancyBit = byte_i * 8 + bit_i;
|
firstOccupancyBit = byte_i * 8 + bit_i;
|
||||||
@ -141,7 +168,8 @@ export class RoomOccupancyList {
|
|||||||
startTime.toISOString(),
|
startTime.toISOString(),
|
||||||
endTime.toISOString(),
|
endTime.toISOString(),
|
||||||
room,
|
room,
|
||||||
true
|
false,
|
||||||
|
false,
|
||||||
));
|
));
|
||||||
|
|
||||||
firstOccupancyBit = null;
|
firstOccupancyBit = null;
|
||||||
@ -158,7 +186,8 @@ export class RoomOccupancyList {
|
|||||||
startTime.toISOString(),
|
startTime.toISOString(),
|
||||||
endTime.toISOString(),
|
endTime.toISOString(),
|
||||||
room,
|
room,
|
||||||
true
|
false,
|
||||||
|
false,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,6 +224,21 @@ export class RoomOccupancyList {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate RoomOccupancyList from plain JSON object.
|
||||||
|
* For performance no deep copy is made, reference attributes will be shared.
|
||||||
|
* @param json the JS object to read from.
|
||||||
|
* @returns a RoomOccupancyList object.
|
||||||
|
*/
|
||||||
|
public static fromJSON(json : any) : RoomOccupancyList {
|
||||||
|
return new RoomOccupancyList(
|
||||||
|
json.start,
|
||||||
|
json.granularity,
|
||||||
|
json.blocks,
|
||||||
|
json.rooms.map((room : any) => new RoomOccupancy(room.name, room.occupancy)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shift the time forward to the start of the next day if it is after the end of the current day.
|
* Shift the time forward to the start of the next day if it is after the end of the current day.
|
||||||
*
|
*
|
||||||
@ -205,7 +249,7 @@ export class RoomOccupancyList {
|
|||||||
// if the time of date is after the end of the workday
|
// if the time of date is after the end of the workday
|
||||||
if (isAfter(date, RoomOccupancyList.setTimeOfDay(date, END_OF_WORKDAY))) {
|
if (isAfter(date, RoomOccupancyList.setTimeOfDay(date, END_OF_WORKDAY))) {
|
||||||
// shift the time to the start of the next day
|
// shift the time to the start of the next day
|
||||||
return startOfDay(addDays(date,1));
|
return RoomOccupancyList.startOfDay(addDays(date,1));
|
||||||
} else {
|
} else {
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
@ -221,20 +265,41 @@ export class RoomOccupancyList {
|
|||||||
// if the time of date is before the start of the workday
|
// if the time of date is before the start of the workday
|
||||||
if (isBefore(date, RoomOccupancyList.setTimeOfDay(date, START_OF_WORKDAY))) {
|
if (isBefore(date, RoomOccupancyList.setTimeOfDay(date, START_OF_WORKDAY))) {
|
||||||
// shift the time to the end of the previous day
|
// shift the time to the end of the previous day
|
||||||
return endOfDay(subDays(date,1));
|
return RoomOccupancyList.endOfDay(subDays(date,1));
|
||||||
} else {
|
} else {
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the date to the specified time after 00:00.
|
* Sets the date to the specified time after 00:00 in the current local timezone.
|
||||||
* @param date the date object to extract the day from.
|
* @param date the date object to extract the day from.
|
||||||
* @param time the time as Duration after 00:00.
|
* @param time the time as Duration after 00:00.
|
||||||
* @returns new date with changed time values.
|
* @returns new date with changed time values.
|
||||||
*/
|
*/
|
||||||
private static setTimeOfDay(date : Date, time : Duration) : Date {
|
private static setTimeOfDay(date : Date, time : Duration) : Date {
|
||||||
return add(startOfDay(date), time);
|
return add(RoomOccupancyList.startOfDay(date), time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start of day in server timezone defined as const TIMEZONE.
|
||||||
|
* @param date
|
||||||
|
* @returns the start of the day.
|
||||||
|
*/
|
||||||
|
private static startOfDay(date : Date) : Date {
|
||||||
|
const dateInLocalTimezone = toZonedTime(date, TIMEZONE);
|
||||||
|
return fromZonedTime(startOfDay(dateInLocalTimezone), TIMEZONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End of day in server timezone defined as const TIMEZONE.
|
||||||
|
* @param date
|
||||||
|
* @returns the end of the day.
|
||||||
|
*/
|
||||||
|
private static endOfDay(date : Date) : Date {
|
||||||
|
const dateInLocalTimezone = toZonedTime(date, TIMEZONE);
|
||||||
|
return fromZonedTime(endOfDay(dateInLocalTimezone), TIMEZONE);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user