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",
|
||||
"country-flag-emoji-polyfill": "^0.1.8",
|
||||
"date-fns": "^3.6.0",
|
||||
"date-fns-tz": "^3.1.3",
|
||||
"pinia": "^2.1.7",
|
||||
"primeflex": "^3.3.1",
|
||||
"primeicons": "^6.0.1",
|
||||
@ -4581,6 +4582,14 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||
|
@ -24,6 +24,7 @@
|
||||
"bson": "^5.5.1",
|
||||
"country-flag-emoji-polyfill": "^0.1.8",
|
||||
"date-fns": "^3.6.0",
|
||||
"date-fns-tz": "^3.1.3",
|
||||
"pinia": "^2.1.7",
|
||||
"primeflex": "^3.3.1",
|
||||
"primeicons": "^6.0.1",
|
||||
|
@ -36,7 +36,7 @@ export async function fetchRoomOccupancy(
|
||||
return null;
|
||||
}
|
||||
const data = new Uint8Array(roomsResponse);
|
||||
roomOccupancyList = BSON.deserialize(data) as RoomOccupancyList;
|
||||
roomOccupancyList = RoomOccupancyList.fromJSON(BSON.deserialize(data));
|
||||
return roomOccupancyList;
|
||||
});
|
||||
|
||||
|
@ -31,6 +31,7 @@ import { useQuery } from "@tanstack/vue-query";
|
||||
import { watch } from "vue";
|
||||
import { fetchRoomOccupancy } from "@/api/fetchRoomOccupancy";
|
||||
import { isValid } from "date-fns";
|
||||
import { RoomOccupancyList } from "@/model/roomOccupancyList";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
@ -70,6 +71,22 @@ const mobilePage = inject("mobilePage") as Ref<boolean>;
|
||||
|
||||
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({
|
||||
queryKey: ["roomOccupation", selectedRoom, currentDateFrom, currentDateTo],
|
||||
queryFn: () =>
|
||||
@ -77,12 +94,7 @@ const { data: occupations } = useQuery({
|
||||
new Date(currentDateFrom.value).toISOString(),
|
||||
new Date(currentDateTo.value).toISOString()
|
||||
),
|
||||
select: (data) => data
|
||||
.decodeOccupancy(selectedRoom.value, new Date(currentDateFrom.value), new Date(currentDateTo.value))
|
||||
.map((event, index) => ({
|
||||
id: index,
|
||||
event: event,
|
||||
})),
|
||||
select: (data) => transformData(data),
|
||||
enabled: () => selectedRoom.value !== "" && currentDateFrom.value !== "",
|
||||
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/>.
|
||||
|
||||
import { Binary } from "bson";
|
||||
import { AnonymizedEventDTO, 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 { AnonymizedOccupancy } from "./event";
|
||||
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.
|
||||
@ -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.
|
||||
* 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.
|
||||
*/
|
||||
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);
|
||||
|
||||
if (roomOccupancy === undefined) {
|
||||
@ -77,15 +79,10 @@ export class RoomOccupancyList {
|
||||
// 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()));
|
||||
|
||||
// Calculate the slice of bytes, that are needed to decode the requested time range
|
||||
let minutesFromStart = differenceInMinutes(this.start, decodeInterval.start);
|
||||
let minutesToEnd = differenceInMinutes(this.start, decodeInterval.end);
|
||||
|
||||
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);
|
||||
let {decodeSliceStart, decodeSlice} = this.sliceOccupancy(
|
||||
decodeInterval,
|
||||
roomOccupancy.occupancy.buffer
|
||||
);
|
||||
|
||||
// Decode the occupancy data
|
||||
occupancyList.push(...RoomOccupancyList.decodeOccupancyData(new Uint8Array(decodeSlice), decodeSliceStart, this.granularity, room));
|
||||
@ -102,6 +99,36 @@ export class RoomOccupancyList {
|
||||
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.
|
||||
* @returns the interval of the occupancy list.
|
||||
@ -128,7 +155,7 @@ export class RoomOccupancyList {
|
||||
|
||||
// Iterate over all bits in the current byte
|
||||
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) {
|
||||
firstOccupancyBit = byte_i * 8 + bit_i;
|
||||
@ -141,7 +168,8 @@ export class RoomOccupancyList {
|
||||
startTime.toISOString(),
|
||||
endTime.toISOString(),
|
||||
room,
|
||||
true
|
||||
false,
|
||||
false,
|
||||
));
|
||||
|
||||
firstOccupancyBit = null;
|
||||
@ -158,7 +186,8 @@ export class RoomOccupancyList {
|
||||
startTime.toISOString(),
|
||||
endTime.toISOString(),
|
||||
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.
|
||||
*
|
||||
@ -205,7 +249,7 @@ export class RoomOccupancyList {
|
||||
// if the time of date is after the end of the workday
|
||||
if (isAfter(date, RoomOccupancyList.setTimeOfDay(date, END_OF_WORKDAY))) {
|
||||
// shift the time to the start of the next day
|
||||
return startOfDay(addDays(date,1));
|
||||
return RoomOccupancyList.startOfDay(addDays(date,1));
|
||||
} else {
|
||||
return date;
|
||||
}
|
||||
@ -221,20 +265,41 @@ export class RoomOccupancyList {
|
||||
// if the time of date is before the start of the workday
|
||||
if (isBefore(date, RoomOccupancyList.setTimeOfDay(date, START_OF_WORKDAY))) {
|
||||
// shift the time to the end of the previous day
|
||||
return endOfDay(subDays(date,1));
|
||||
return RoomOccupancyList.endOfDay(subDays(date,1));
|
||||
} else {
|
||||
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 time the time as Duration after 00:00.
|
||||
* @returns new date with changed time values.
|
||||
*/
|
||||
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