commit bd1626f3a69fe14b12a98f273b40ef034884677d Author: Fenris Wolf Date: Sun Sep 8 21:31:17 2024 +0200 [ini] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1bff2e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/build/ +/temp/ diff --git a/source/main.ts b/source/main.ts new file mode 100644 index 0000000..061cfe8 --- /dev/null +++ b/source/main.ts @@ -0,0 +1,914 @@ +declare var process : any; + + +type int = number; + + +/** + */ +function date_object_get_week_of_year( + date : Date +) : int +{ + let date_ : Date = new Date(date.getTime()); + date_.setHours(0, 0, 0, 0); + // Thursday in current week decides the year. + date_.setDate(date_.getDate() + 3 - (date_.getDay() + 6) % 7); + // January 4 is always in week 1. + let week1 : Date = new Date(date_.getFullYear(), 0, 4); + // Adjust to Thursday in week 1 and count number of weeks from date to week1. + return ( + 1 + + + Math.round( + ( + ((date_.getTime() - week1.getTime()) / 86400000) + - + 3 + + + (week1.getDay() + 6) % 7 + ) + / + 7 + ) + ); +} + +type type_date = { + year : int; + month : int; + day : int; +}; + +type type_time = { + hour : int; + minute : int; + second : int; +}; + +type type_datetime = { + timezone : string; + date : type_date; + time : ( + null + | + type_time + ); +}; + + +type type_pit = int; + + + +/** + */ +function pit_to_date_object( + pit : type_pit +) : Date +{ + return (new Date(pit * 1000)); +} + + +/** + */ +function pit_from_date_object( + date_object : Date +) : type_pit +{ + return Math.round(date_object.getTime() / 1000); +} + + +/** + */ +function pit_now( +) : type_pit +{ + return pit_from_date_object(new Date(Date.now())); +} + + +/** + */ +function pit_to_datetime( + pit : type_pit +) : type_datetime +{ + const date_object : Date = pit_to_date_object(pit); + return { + "timezone": "Europe/Berlin", // TODO + "date": { + "year": date_object.getFullYear(), + "month": (date_object.getMonth() + 1), + "day": (date_object.getDate()), + }, + "time": { + "hour": date_object.getHours(), + "minute": date_object.getMinutes(), + "second": date_object.getSeconds(), + }, + }; +} + + +/** + */ +function pit_from_datetime( + datetime : type_datetime +) : type_pit +{ + // TODO: timezone + const date_object : Date = new Date( + datetime.date.year, + (datetime.date.month - 1), // TODO + datetime.date.day, + ((datetime.time === null) ? 0 : datetime.time.hour), + ((datetime.time === null) ? 0 : datetime.time.minute), + ((datetime.time === null) ? 0 : datetime.time.second) + ); + return pit_from_date_object(date_object); +} + + +/** + */ +function pit_is_before( + pit : type_pit, + reference : type_pit +) : boolean +{ + return (pit < reference); +} + + +/** + */ +function pit_is_after( + pit : type_pit, + reference : type_pit +) : boolean +{ + return (pit > reference); +} + + +/** + */ +function pit_is_between( + pit : type_pit, + reference_left : type_pit, + reference_right : type_pit +) : boolean +{ + return ( + pit_is_after(pit, reference_left) + && + pit_is_before(pit, reference_right) + ); +} + + +/** + */ +function pit_shift_hour( + pit : type_pit, + increment : int +) : type_pit +{ + return (pit + (60 * 60 * increment)); +} + + +/** + */ +function pit_shift_day( + pit : type_pit, + increment : int +) : type_pit +{ + return (pit + (60 * 60 * 24 * increment)); +} + + +/** + */ +function pit_shift_week( + pit : type_pit, + increment : int +) : type_pit +{ + return (pit + (60 * 60 * 24 * 7 * increment)); +} + + +/** + */ +function pit_shift_year( + pit : type_pit, + increment : int +) : type_pit +{ + return (pit + (60 * 60 * 24 * 365 * increment)); +} + + +/** + */ +function pit_trunc_minute( + pit : type_pit +) : type_pit +{ + const datetime_input : type_datetime = pit_to_datetime(pit); + const datetime_output : type_datetime = { + "timezone": "Europe/Berlin", // TODO + "date": { + "year": datetime_input.date.year, + "month": datetime_input.date.month, + "day": datetime_input.date.day, + }, + "time": { + "hour": ((datetime_input.time === null) ? 0 : datetime_input.time.hour), + "minute": ((datetime_input.time === null) ? 0 : datetime_input.time.minute), + "second": 0, + }, + }; + return pit_from_datetime(datetime_output); +} + + +/** + */ +function pit_trunc_hour( + pit : type_pit +) : type_pit +{ + const datetime_input : type_datetime = pit_to_datetime(pit); + const datetime_output : type_datetime = { + "timezone": "Europe/Berlin", // TODO + "date": { + "year": datetime_input.date.year, + "month": datetime_input.date.month, + "day": datetime_input.date.day, + }, + "time": { + "hour": ((datetime_input.time === null) ? 0 : datetime_input.time.hour), + "minute": 0, + "second": 0, + }, + }; + return pit_from_datetime(datetime_output); +} + + +/** + */ +function pit_trunc_day( + pit : type_pit +) : type_pit +{ + const datetime_input : type_datetime = pit_to_datetime(pit); + const datetime_output : type_datetime = { + "timezone": "Europe/Berlin", // TODO + "date": { + "year": datetime_input.date.year, + "month": datetime_input.date.month, + "day": datetime_input.date.day, + }, + "time": { + "hour": 0, + "minute": 0, + "second": 0, + }, + }; + return pit_from_datetime(datetime_output); +} + + +/** + */ +function pit_trunc_week( + pit : type_pit +) : type_pit +{ + const date_object : Date = pit_to_date_object(pit); + return pit_shift_day( + pit, + -(date_object.getDay()) + ); +} + + +/** + */ +function pit_trunc_month( + pit : type_pit +) : type_pit +{ + const datetime_input : type_datetime = pit_to_datetime(pit); + const datetime_output : type_datetime = { + "timezone": "Europe/Berlin", // TODO + "date": { + "year": datetime_input.date.year, + "month": datetime_input.date.month, + "day": 1, + }, + "time": { + "hour": 0, + "minute": 0, + "second": 0, + }, + }; + return pit_from_datetime(datetime_output); +} + + +/** + */ +function pit_trunc_year( + pit : type_pit +) : type_pit +{ + const datetime_input : type_datetime = pit_to_datetime(pit); + const datetime_output : type_datetime = { + "timezone": "Europe/Berlin", // TODO + "date": { + "year": datetime_input.date.year, + "month": 1, + "day": 1, + }, + "time": { + "hour": 0, + "minute": 0, + "second": 0, + }, + }; + return pit_from_datetime(datetime_output); +} + + +/** + */ +function pit_from_year_and_week( + year : int, + week : int +) : type_pit +{ + return pit_trunc_week( + pit_shift_week( + pit_from_datetime( + { + "timezone": "Europe/Berlin", // TODO + "date": { + "year": year, + "month": 1, + "day": 1, + }, + "time": { + "hour": 12, + "minute": 0, + "second": 0 + } + } + ), + week + ) + ) +} + + + +// --- + +type type_role = ( + "editor" + | + "viewer" +); + +type type_user_id = int; + +type type_user_object = { + name : string; +}; + +type type_event = { + name : string; + begin : type_datetime; + end : ( + null + | + type_datetime + ); + description : ( + null + | + string + ); +}; + +type type_calendar_id = int; + +type type_calendar_object = ( + { + kind : "concrete"; + data : { + name : string; + users : Array< + { + id : type_user_id; + role : type_role; + } + >; + events : Array; + }; + } + | + { + kind : "collection"; + data : { + name : string; + sources : Array< + type_calendar_id + >; + } + } +); + +type type_datamodel = { + users : Array< + { + id : type_user_id; + object : type_user_object; + } + >; + calendars : Array< + { + id : type_calendar_id; + object : type_calendar_object; + } + >; +}; + + +/** + */ +function calendar_list( + data : type_datamodel +) : Array< + { + key : type_calendar_id; + preview : { + name : string; + } + } +> +{ + return ( + data.calendars + .map( + (calendar_entry) => ({ + "key": calendar_entry.id, + "preview": { + "name": calendar_entry.object.data.name, + } + }) + ) + ); +} + + +/** + */ +function calendar_read( + data : type_datamodel, + calendar_id : type_calendar_id +) : type_calendar_object +{ + const hits = ( + data.calendars + .filter( + (calendar_entry) => (calendar_entry.id === calendar_id) + ) + ); + if (hits.length <= 0) { + throw (new Error("not found")); + } + else { + return hits[0].object; + } +} + + +/** + */ +function calendar_gather_events( + data : type_datamodel, + calendar_id : type_calendar_id, + from_pit : type_pit, + to_pit : type_pit +) : Array< + { + calendar_id : type_calendar_id; + event : type_event; + } +> +{ + const calendar_object : type_calendar_object = calendar_read( + data, + calendar_id + ); + switch (calendar_object.kind) { + case "concrete": { + return ( + calendar_object.data.events + .filter( + (event) => pit_is_between( + pit_from_datetime(event.begin), + from_pit, + to_pit + ) + ) + .map( + (event) => ({"calendar_id": calendar_id, "event": event}) + ) + ); + break; + } + case "collection": { + return ( + calendar_object.data.sources + .map( + (source_calendar_id) => calendar_gather_events( + data, + source_calendar_id, + from_pit, + to_pit + ) + ) + .reduce( + (x, y) => x.concat(y), + [] + ) + ); + break; + } + } +} + + +/** + */ +function calendar_view_table( + data : type_datamodel, + calendar_id : type_calendar_id, + from : { + year : int; + week : int; + }, + to : { + year : int; + week : int; + } +) : Array< + Array< + { + date : type_date; + entries : Array< + { + calendar_id : type_calendar_id; + event : type_event; + } + >; + } + > +> +{ + /* + const calendar_object : type_calendar_object = calendar_read( + data, + calendar_id + ); + */ + const from_pit : type_pit = pit_from_year_and_week(from.year, from.week); + const to_pit : type_pit = pit_from_year_and_week(to.year, to.week); + + // prepare + const entries : Array< + { + calendar_id : type_calendar_id; + event : type_event; + } + > = calendar_gather_events( + data, + calendar_id, + from_pit, + to_pit + ); + let result : Array< + Array< + { + date : type_date; + entries : Array< + { + calendar_id : type_calendar_id; + event : type_event; + } + >; + } + > + > = []; + let row : Array< + { + date : type_date; + entries : Array< + { + calendar_id : type_calendar_id; + event : type_event; + } + >; + } + > = []; + let day : int = 0; + while (true) { + let pit_current : type_pit = pit_shift_day(from_pit, day); + if (pit_is_before(pit_current, to_pit)) { + day += 1; + row.push( + { + "date": pit_to_datetime(pit_shift_hour(pit_current, 12)).date, + "entries": [], + } + ); + if (day % 7 === 0) { + result.push(row); + row = []; + } + else { + // do nothing + } + } + else { + break; + } + } + + // fill + ( + entries + .forEach( + (entry) => { + const distance_seconds : int = (pit_from_datetime(entry.event.begin) - from_pit); + const distance_days : int = (distance_seconds / (60 * 60 * 24)); + + const week : int = Math.floor(Math.floor(distance_days) / 7); + const day : int = (Math.floor(distance_days) % 7); + + // process.stdout.write(JSON.stringify({entry, week, day}, undefined, "\t") + "\n"); + + result[week][day].entries.push(entry); + } + ) + ); + + return result; +} + + +/** + */ +function calendar_view_table_html( + data : type_datamodel, + calendar_id : type_calendar_id, + from : { + year : int; + week : int; + }, + to : { + year : int; + week : int; + } +) : string +{ + const rows = calendar_view_table( + data, + calendar_id, + from, + to + ); + return ( + "\n" + + + ( + "\t\n" + + + "\t\n" + ) + + + ( + "\t\n" + + + ( + rows + .map( + (row) => ( + "\t\t\n" + + + ( + row + .map( + (cell) => ( + "\t\t\t\n" + ) + ) + .join("") + ) + + + "\t\t\n" + ) + ) + .join("") + ) + + + "\t\n" + ) + + + "
\n" + + + // (cell.entries.map(entry => entry.event.name).join(" | ") + "\n") + JSON.stringify(cell) + "\n" + + + "\t\t\t
\n" + ); +} + + +/** + */ +function main( +) : void +{ + const data : type_datamodel = { + "users": [ + { + "id": 1, + "object": { + "name": "Anton" + } + }, + { + "id": 2, + "object": { + "name": "Berta" + } + }, + { + "id": 3, + "object": { + "name": "Caesar" + } + }, + ], + "calendars": [ + { + "id": 1, + "object": { + "kind": "concrete", + "data": { + "name": "Garten", + "users": [ + { + "id": 1, + "role": "editor" + }, + ], + "events": [ + { + "name": "Unkraut jähten", + "begin": { + "timezone": "Europe/Berlin", + "date": {"year": 2024, "month": 9, "day": 19}, + "time": {"hour": 10, "minute": 0, "second": 0} + }, + "end": null, + "description": null + }, + { + "name": "Kartoffeln ernten", + "begin": { + "timezone": "Europe/Berlin", + "date": {"year": 2024, "month": 9, "day": 24}, + "time": {"hour": 17, "minute": 0, "second": 0} + }, + "end": { + "timezone": "Europe/Berlin", + "date": {"year": 2024, "month": 9, "day": 24}, + "time": {"hour": 20, "minute": 0, "second": 0} + }, + "description": null + }, + ] + } + } + }, + { + "id": 2, + "object": { + "kind": "concrete", + "data": { + "name": "Kochen", + "users": [ + { + "id": 1, + "role": "editor" + }, + { + "id": 2, + "role": "viewer" + }, + ], + "events": [ + { + "name": "38.KW", + "begin": { + "timezone": "Europe/Berlin", + "date": {"year": 2024, "month": 9, "day": 20}, + "time": {"hour": 17, "minute": 0, "second": 0} + }, + "end": { + "timezone": "Europe/Berlin", + "date": {"year": 2024, "month": 9, "day": 20}, + "time": {"hour": 19, "minute": 0, "second": 0} + }, + "description": "Krautnudeln" + } + ] + } + } + }, + { + "id": 3, + "object": { + "kind": "concrete", + "data": { + "name": "Band", + "users": [ + { + "id": 2, + "role": "editor" + }, + ], + "events": [ + { + "name": "Auftritt im Park", + "begin": { + "timezone": "Europe/Berlin", + "date": {"year": 2024, "month": 9, "day": 21}, + "time": {"hour": 20, "minute": 0, "second": 0} + }, + "end": { + "timezone": "Europe/Berlin", + "date": {"year": 2024, "month": 9, "day": 21}, + "time": {"hour": 21, "minute": 0, "second": 0} + }, + "description": null + } + ] + } + } + }, + { + "id": 4, + "object": { + "kind": "collection", + "data": { + "name": "Überblick", + "sources": [ + 1, + 2, + 3 + ] + } + } + }, + ] + }; + const output : string = calendar_view_table_html( + data, + 4, + { + "year": 2024, + "week": 37 + }, + { + "year": 2024, + "week": 40 + } + ); + process.stdout.write(output); +} + + +main(); + diff --git a/tools/build b/tools/build new file mode 100755 index 0000000..796073a --- /dev/null +++ b/tools/build @@ -0,0 +1,4 @@ +#!/usr/bin/env sh + +make -f tools/makefile + diff --git a/tools/makefile b/tools/makefile new file mode 100644 index 0000000..6bb89a9 --- /dev/null +++ b/tools/makefile @@ -0,0 +1,31 @@ +## dirs + +dir_source := source +dir_temp := temp +dir_build := build + + +## cmds + +cmd_log := echo "--" +cmd_tsc := tsc --strict +cmd_cat := cat +cmd_mkdir := mkdir -p + + +## rules + +.PHONY: _default +_default: ${dir_build}/kalender + +${dir_temp}/kalender-unlinked.js: ${dir_source}/main.ts + @ ${cmd_log} "compiling …" + @ ${cmd_mkdir} $(dir $@) + @ ${cmd_tsc} $^ --outFile $@ + +${dir_build}/kalender: ${dir_temp}/kalender-unlinked.js + @ ${cmd_log} "linking …" + @ ${cmd_mkdir} $(dir $@) + @ ${cmd_cat} $^ > $@ + @ chmod +x $@ +