diff --git a/.gitignore b/.gitignore index 2fe9f10a..d5a9ca10 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ target/ tests/rust_tests.rs _logs/ js-api/ +tests/jsons/stable/tijl-goals \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 3dbe7247..dd931de8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -392,12 +392,6 @@ dependencies = [ "serde", ] -[[package]] -name = "soft" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62483b0a930a082c57fe1f4aa56432c9653a3387a90a4e4cb6cffed31a1a3541" - [[package]] name = "syn" version = "2.0.39" @@ -611,6 +605,5 @@ dependencies = [ "serde", "serde-wasm-bindgen", "serde_json", - "soft", "wasm-bindgen", ] diff --git a/Cargo.toml b/Cargo.toml index 805d47a3..a58a8bab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ with-logging = [] new-tests = [] -default = [ "new-tests", "with-logging" ] +default = ["new-tests", "with-logging"] [lib] #see https://github.com/rust-lang/cargo/issues/6659#issuecomment-463335095 @@ -54,9 +54,6 @@ console_error_panic_hook = "0.1.7" log = "0.4.19" env_logger = "0.10.0" -# soft assertions without panic -soft = "0.1.1" - # random number generator for tests rand = "0.8.5" # getrandom with js features should be included to make sure the wasm-unknown build target can use the crate in js context (even though we only use this crate in test code) diff --git a/build_templates/run_test.rs b/build_templates/run_test.rs index 3d5a2cd8..60bd4837 100644 --- a/build_templates/run_test.rs +++ b/build_templates/run_test.rs @@ -1,8 +1,8 @@ #![cfg_attr(rustfmt, rustfmt_skip)] //skip cargo fmt on autogenerated file extern crate scheduler; -extern crate soft; -mod common; +use scheduler::technical::input_output::Input; +use scheduler::models::calendar; /// AUTO-GENERATED FILE. Do not change. /// Will be overwritten on build. Edit the file in build_templates or change test generation in build.rs diff --git a/build_templates/tests_mod.rs b/build_templates/tests_mod.rs index c0caffc5..2461e46c 100644 --- a/build_templates/tests_mod.rs +++ b/build_templates/tests_mod.rs @@ -2,15 +2,19 @@ mod TEST_MODULE_NAME { // stable tests -//TEST_FUNCTIONS_STABLE + //TEST_FUNCTIONS_STABLE // experimental tests -//TEST_FUNCTIONS_EXPERIMENTAL + //TEST_FUNCTIONS_EXPERIMENTAL - use scheduler::legacy::{input::Input, output::FinalTasks}; + use crate::calendar::Calendar; + use crate::Input; + use scheduler::models::activity::Activity; + use scheduler::services::{activity_generator, activity_placer}; + + use scheduler::technical::input_output; use std::path::Path; - use crate::common; - + fn test(folder: &str) { let (actual_output, desired_output) = generate_outputs(folder); assert_eq!(actual_output, desired_output); @@ -26,13 +30,46 @@ mod TEST_MODULE_NAME { let output_path = Path::new(&output_path_str[..]); let actual_output_path = Path::new(&actual_output_path_str[..]); - let input: Input = common::get_input_from_json(input_path).unwrap(); - let desired_output: String = common::get_output_string_from_json(output_path).unwrap(); + let input: Input = input_output::get_input_from_json(input_path).unwrap(); + let desired_output: String = + input_output::get_output_string_from_json(output_path).unwrap(); + + // ONLY do this if expected is malformatted ... check that contents don't change! + // input_output::write_to_file(output_path, &desired_output).unwrap(); + + let mut calendar = Calendar::new(input.start_date, input.end_date); + + calendar.add_budgets_from(&input.goals); + + //generate and place simple goal activities + let simple_goal_activities = + activity_generator::generate_simple_goal_activities(&calendar, &input.goals); + dbg!(&simple_goal_activities); + activity_placer::place(&mut calendar, simple_goal_activities); + + //generate and place budget goal activities + let budget_goal_activities: Vec = + activity_generator::generate_budget_goal_activities(&calendar, &input.goals); + dbg!(&calendar); + activity_placer::place(&mut calendar, budget_goal_activities); + + calendar.log_impossible_min_day_budgets(); + + let get_to_week_min_budget_activities = + activity_generator::generate_get_to_week_min_budget_activities(&calendar, &input.goals); + activity_placer::place(&mut calendar, get_to_week_min_budget_activities); + + calendar.log_impossible_min_week_budgets(); + + let top_up_week_budget_activities = + activity_generator::generate_top_up_week_budget_activities(&calendar, &input.goals); + activity_placer::place(&mut calendar, top_up_week_budget_activities); + + let output = calendar.print(); - let output: FinalTasks = scheduler::run_scheduler(input); let actual_output = serde_json::to_string_pretty(&output).unwrap(); - common::write_to_file(actual_output_path, &actual_output).unwrap(); + input_output::write_to_file(actual_output_path, &actual_output).unwrap(); (actual_output, desired_output) } diff --git a/pkg/README.md b/pkg/README.md index 4ebd4d3f..28456759 100644 --- a/pkg/README.md +++ b/pkg/README.md @@ -3,7 +3,28 @@ If you like Rust and scheduling algorithms you've come to the right place :) We can talk big-O, add features or optimize hot loops. -> Please contact me tijl.leenders@gmail.com or open an issue. +> Please contact me tijl@zinzen.me or open an issue. + + +## ZinZen® +ZinZen® is a platform for stress-free life planning. It works by defining life goals with constraints and dependencies. +An automatic scheduler then schedules tasks in a calendar to reach these goals, auto-magically updating the schedule when +goals or constraints change. + +This repository contains the ZinZen®-scheduler. The scheduler consists of a WASM-module written in Rust +that can be called from the React-based UI application. + +The ZinZen® UI application can be found here: [ZinZen® Github](https://github.com/tijlleenders/ZinZen) + + + +## Getting started +All documentation can be found in the folder [documentation](documentation/Readme.md). +In this folder you will find a [technical know-how base](documentation/technical/Readme.md), +as well as all the necessary [functional documentation](documentation/functional/Readme.md) to understand the +scheduling algorithms and all the concepts introduced in the ZinZen®-scheduler. + +For a quick set-up guide see the [Development Setup page](documentation/technical/Development-Setup.md). ## Legal stuff diff --git a/pkg/package.json b/pkg/package.json index f272f01b..bf8c1052 100644 --- a/pkg/package.json +++ b/pkg/package.json @@ -1,7 +1,7 @@ { "name": "zinzen", "description": "Algorithm for auto-scheduling time-constrained tasks on a timeline", - "version": "0.1.0", + "version": "0.2.0", "license": "AGPL-3.0", "repository": { "type": "git", @@ -14,9 +14,11 @@ "LICENSE.md" ], "module": "scheduler.js", - "homepage": "https://github.com/tijlleenders/ZinZen-scheduler/wiki", + "homepage": "https://github.com/tijlleenders/ZinZen-scheduler", "types": "scheduler.d.ts", - "sideEffects": false, + "sideEffects": [ + "./snippets/*" + ], "keywords": [ "zinzen", "scheduler", diff --git a/pkg/scheduler.d.ts b/pkg/scheduler.d.ts index 158ab71b..b3c18125 100644 --- a/pkg/scheduler.d.ts +++ b/pkg/scheduler.d.ts @@ -1,6 +1,7 @@ /* tslint:disable */ /* eslint-disable */ /** +* The main wasm function to call * @param {any} input * @returns {any} */ @@ -19,19 +20,23 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl export interface InitOutput { readonly memory: WebAssembly.Memory; readonly schedule: (a: number, b: number) => void; - readonly __wbindgen_malloc: (a: number) => number; - readonly __wbindgen_realloc: (a: number, b: number, c: number) => number; + readonly __wbindgen_malloc: (a: number, b: number) => number; + readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; readonly __wbindgen_add_to_stack_pointer: (a: number) => number; + readonly __wbindgen_free: (a: number, b: number, c: number) => void; + readonly __wbindgen_exn_store: (a: number) => void; } +export type SyncInitInput = BufferSource | WebAssembly.Module; /** -* Synchronously compiles the given `bytes` and instantiates the WebAssembly module. +* Instantiates the given `module`, which can either be bytes or +* a precompiled `WebAssembly.Module`. * -* @param {BufferSource} bytes +* @param {SyncInitInput} module * * @returns {InitOutput} */ -export function initSync(bytes: BufferSource): InitOutput; +export function initSync(module: SyncInitInput): InitOutput; /** * If `module_or_path` is {RequestInfo} or {URL}, makes a request and @@ -41,4 +46,4 @@ export function initSync(bytes: BufferSource): InitOutput; * * @returns {Promise} */ -export default function init (module_or_path?: InitInput | Promise): Promise; +export default function __wbg_init (module_or_path?: InitInput | Promise): Promise; diff --git a/pkg/scheduler.js b/pkg/scheduler.js index 76702f08..10258812 100644 --- a/pkg/scheduler.js +++ b/pkg/scheduler.js @@ -1,24 +1,24 @@ - let wasm; -const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); -cachedTextDecoder.decode(); +if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; -let cachedUint8Memory0 = new Uint8Array(); +let cachedUint8Memory0 = null; function getUint8Memory0() { - if (cachedUint8Memory0.byteLength === 0) { + if (cachedUint8Memory0 === null || cachedUint8Memory0.byteLength === 0) { cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer); } return cachedUint8Memory0; } function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); } -const heap = new Array(32).fill(undefined); +const heap = new Array(128).fill(undefined); heap.push(undefined, null, true, false); @@ -37,9 +37,15 @@ function addHeapObject(obj) { function getObject(idx) { return heap[idx]; } +function _assertBoolean(n) { + if (typeof(n) !== 'boolean') { + throw new Error('expected a boolean argument'); + } +} + let WASM_VECTOR_LEN = 0; -const cachedTextEncoder = new TextEncoder('utf-8'); +const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } ); const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' ? function (arg, view) { @@ -60,14 +66,14 @@ function passStringToWasm0(arg, malloc, realloc) { if (realloc === undefined) { const buf = cachedTextEncoder.encode(arg); - const ptr = malloc(buf.length); + const ptr = malloc(buf.length, 1) >>> 0; getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); WASM_VECTOR_LEN = buf.length; return ptr; } let len = arg.length; - let ptr = malloc(len); + let ptr = malloc(len, 1) >>> 0; const mem = getUint8Memory0(); @@ -83,7 +89,7 @@ function passStringToWasm0(arg, malloc, realloc) { if (offset !== 0) { arg = arg.slice(offset); } - ptr = realloc(ptr, len, len = offset + arg.length * 3); + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; const view = getUint8Memory0().subarray(ptr + offset, ptr + len); const ret = encodeString(arg, view); if (ret.read !== arg.length) throw new Error('failed to pass whole string'); @@ -94,25 +100,112 @@ function passStringToWasm0(arg, malloc, realloc) { return ptr; } -let cachedInt32Memory0 = new Int32Array(); +function isLikeNone(x) { + return x === undefined || x === null; +} + +let cachedInt32Memory0 = null; function getInt32Memory0() { - if (cachedInt32Memory0.byteLength === 0) { + if (cachedInt32Memory0 === null || cachedInt32Memory0.byteLength === 0) { cachedInt32Memory0 = new Int32Array(wasm.memory.buffer); } return cachedInt32Memory0; } -let stack_pointer = 32; +function _assertNum(n) { + if (typeof(n) !== 'number') throw new Error('expected a number argument'); +} -function addBorrowedObject(obj) { - if (stack_pointer == 1) throw new Error('out of js stack'); - heap[--stack_pointer] = obj; - return stack_pointer; +let cachedFloat64Memory0 = null; + +function getFloat64Memory0() { + if (cachedFloat64Memory0 === null || cachedFloat64Memory0.byteLength === 0) { + cachedFloat64Memory0 = new Float64Array(wasm.memory.buffer); + } + return cachedFloat64Memory0; +} + +function debugString(val) { + // primitive types + const type = typeof val; + if (type == 'number' || type == 'boolean' || val == null) { + return `${val}`; + } + if (type == 'string') { + return `"${val}"`; + } + if (type == 'symbol') { + const description = val.description; + if (description == null) { + return 'Symbol'; + } else { + return `Symbol(${description})`; + } + } + if (type == 'function') { + const name = val.name; + if (typeof name == 'string' && name.length > 0) { + return `Function(${name})`; + } else { + return 'Function'; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = '['; + if (length > 0) { + debug += debugString(val[0]); + } + for(let i = 1; i < length; i++) { + debug += ', ' + debugString(val[i]); + } + debug += ']'; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == 'Object') { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return 'Object(' + JSON.stringify(val) + ')'; + } catch (_) { + return 'Object'; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} + +function _assertBigInt(n) { + if (typeof(n) !== 'bigint') throw new Error('expected a bigint argument'); +} + +let cachedBigInt64Memory0 = null; + +function getBigInt64Memory0() { + if (cachedBigInt64Memory0 === null || cachedBigInt64Memory0.byteLength === 0) { + cachedBigInt64Memory0 = new BigInt64Array(wasm.memory.buffer); + } + return cachedBigInt64Memory0; } function dropObject(idx) { - if (idx < 36) return; + if (idx < 132) return; heap[idx] = heap_next; heap_next = idx; } @@ -122,7 +215,16 @@ function takeObject(idx) { dropObject(idx); return ret; } + +let stack_pointer = 128; + +function addBorrowedObject(obj) { + if (stack_pointer == 1) throw new Error('out of js stack'); + heap[--stack_pointer] = obj; + return stack_pointer; +} /** +* The main wasm function to call * @param {any} input * @returns {any} */ @@ -143,7 +245,31 @@ export function schedule(input) { } } -async function load(module, imports) { +function logError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + let error = (function () { + try { + return e instanceof Error ? `${e.message}\n\nStack:\n${e.stack}` : e.toString(); + } catch(_) { + return ""; + } + }()); + console.error("wasm-bindgen: imported JS function that was not marked as `catch` threw an error:", error); + throw e; + } +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + wasm.__wbindgen_exn_store(addHeapObject(e)); + } +} + +async function __wbg_load(module, imports) { if (typeof Response === 'function' && module instanceof Response) { if (typeof WebAssembly.instantiateStreaming === 'function') { try { @@ -174,73 +300,301 @@ async function load(module, imports) { } } -function getImports() { +function __wbg_get_imports() { const imports = {}; imports.wbg = {}; imports.wbg.__wbindgen_error_new = function(arg0, arg1) { const ret = new Error(getStringFromWasm0(arg0, arg1)); return addHeapObject(ret); }; - imports.wbg.__wbindgen_json_parse = function(arg0, arg1) { - const ret = JSON.parse(getStringFromWasm0(arg0, arg1)); + imports.wbg.__wbindgen_is_undefined = function(arg0) { + const ret = getObject(arg0) === undefined; + _assertBoolean(ret); + return ret; + }; + imports.wbg.__wbindgen_in = function(arg0, arg1) { + const ret = getObject(arg0) in getObject(arg1); + _assertBoolean(ret); + return ret; + }; + imports.wbg.__wbindgen_string_get = function(arg0, arg1) { + const obj = getObject(arg1); + const ret = typeof(obj) === 'string' ? obj : undefined; + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; + }; + imports.wbg.__wbindgen_is_bigint = function(arg0) { + const ret = typeof(getObject(arg0)) === 'bigint'; + _assertBoolean(ret); + return ret; + }; + imports.wbg.__wbindgen_is_object = function(arg0) { + const val = getObject(arg0); + const ret = typeof(val) === 'object' && val !== null; + _assertBoolean(ret); + return ret; + }; + imports.wbg.__wbindgen_object_clone_ref = function(arg0) { + const ret = getObject(arg0); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_jsval_eq = function(arg0, arg1) { + const ret = getObject(arg0) === getObject(arg1); + _assertBoolean(ret); + return ret; + }; + imports.wbg.__wbindgen_bigint_from_u64 = function(arg0) { + const ret = BigInt.asUintN(64, arg0); return addHeapObject(ret); }; - imports.wbg.__wbindgen_json_serialize = function(arg0, arg1) { + imports.wbg.__wbg_error_f851667af71bcfc6 = function() { return logError(function (arg0, arg1) { + let deferred0_0; + let deferred0_1; + try { + deferred0_0 = arg0; + deferred0_1 = arg1; + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); + } + }, arguments) }; + imports.wbg.__wbg_new_abda76e883ba8a5f = function() { return logError(function () { + const ret = new Error(); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_stack_658279fe44541cf6 = function() { return logError(function (arg0, arg1) { + const ret = getObject(arg1).stack; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; + }, arguments) }; + imports.wbg.__wbindgen_number_get = function(arg0, arg1) { const obj = getObject(arg1); - const ret = JSON.stringify(obj === undefined ? null : obj); - const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len0; - getInt32Memory0()[arg0 / 4 + 0] = ptr0; + const ret = typeof(obj) === 'number' ? obj : undefined; + if (!isLikeNone(ret)) { + _assertNum(ret); + } + getFloat64Memory0()[arg0 / 8 + 1] = isLikeNone(ret) ? 0 : ret; + getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret); + }; + imports.wbg.__wbindgen_boolean_get = function(arg0) { + const v = getObject(arg0); + const ret = typeof(v) === 'boolean' ? (v ? 1 : 0) : 2; + _assertNum(ret); + return ret; + }; + imports.wbg.__wbindgen_number_new = function(arg0) { + const ret = arg0; + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_string_new = function(arg0, arg1) { + const ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_jsval_loose_eq = function(arg0, arg1) { + const ret = getObject(arg0) == getObject(arg1); + _assertBoolean(ret); + return ret; + }; + imports.wbg.__wbg_String_88810dfeb4021902 = function() { return logError(function (arg0, arg1) { + const ret = String(getObject(arg1)); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; + }, arguments) }; + imports.wbg.__wbg_getwithrefkey_5e6d9547403deab8 = function() { return logError(function (arg0, arg1) { + const ret = getObject(arg0)[getObject(arg1)]; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_set_841ac57cff3d672b = function() { return logError(function (arg0, arg1, arg2) { + getObject(arg0)[takeObject(arg1)] = takeObject(arg2); + }, arguments) }; + imports.wbg.__wbg_new_08236689f0afb357 = function() { return logError(function () { + const ret = new Array(); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_get_4a9aa5157afeb382 = function() { return logError(function (arg0, arg1) { + const ret = getObject(arg0)[arg1 >>> 0]; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_set_0ac78a2bc07da03c = function() { return logError(function (arg0, arg1, arg2) { + getObject(arg0)[arg1 >>> 0] = takeObject(arg2); + }, arguments) }; + imports.wbg.__wbg_isArray_38525be7442aa21e = function() { return logError(function (arg0) { + const ret = Array.isArray(getObject(arg0)); + _assertBoolean(ret); + return ret; + }, arguments) }; + imports.wbg.__wbg_length_cace2e0b3ddc0502 = function() { return logError(function (arg0) { + const ret = getObject(arg0).length; + _assertNum(ret); + return ret; + }, arguments) }; + imports.wbg.__wbg_instanceof_ArrayBuffer_c7cc317e5c29cc0d = function() { return logError(function (arg0) { + let result; + try { + result = getObject(arg0) instanceof ArrayBuffer; + } catch (_) { + result = false; + } + const ret = result; + _assertBoolean(ret); + return ret; + }, arguments) }; + imports.wbg.__wbg_call_669127b9d730c650 = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).call(getObject(arg1)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_next_1989a20442400aaa = function() { return handleError(function (arg0) { + const ret = getObject(arg0).next(); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_next_15da6a3df9290720 = function() { return logError(function (arg0) { + const ret = getObject(arg0).next; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_done_bc26bf4ada718266 = function() { return logError(function (arg0) { + const ret = getObject(arg0).done; + _assertBoolean(ret); + return ret; + }, arguments) }; + imports.wbg.__wbg_value_0570714ff7d75f35 = function() { return logError(function (arg0) { + const ret = getObject(arg0).value; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_isSafeInteger_c38b0a16d0c7cef7 = function() { return logError(function (arg0) { + const ret = Number.isSafeInteger(getObject(arg0)); + _assertBoolean(ret); + return ret; + }, arguments) }; + imports.wbg.__wbg_new_c728d68b8b34487e = function() { return logError(function () { + const ret = new Object(); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_iterator_7ee1a391d310f8e4 = function() { return logError(function () { + const ret = Symbol.iterator; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_instanceof_Uint8Array_19e6f142a5e7e1e1 = function() { return logError(function (arg0) { + let result; + try { + result = getObject(arg0) instanceof Uint8Array; + } catch (_) { + result = false; + } + const ret = result; + _assertBoolean(ret); + return ret; + }, arguments) }; + imports.wbg.__wbg_new_d8a000788389a31e = function() { return logError(function (arg0) { + const ret = new Uint8Array(getObject(arg0)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_length_a5587d6cd79ab197 = function() { return logError(function (arg0) { + const ret = getObject(arg0).length; + _assertNum(ret); + return ret; + }, arguments) }; + imports.wbg.__wbg_set_dcfd613a3420f908 = function() { return logError(function (arg0, arg1, arg2) { + getObject(arg0).set(getObject(arg1), arg2 >>> 0); + }, arguments) }; + imports.wbg.__wbindgen_is_function = function(arg0) { + const ret = typeof(getObject(arg0)) === 'function'; + _assertBoolean(ret); + return ret; + }; + imports.wbg.__wbg_get_2aff440840bb6202 = function() { return handleError(function (arg0, arg1) { + const ret = Reflect.get(getObject(arg0), getObject(arg1)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_buffer_344d9b41efe96da7 = function() { return logError(function (arg0) { + const ret = getObject(arg0).buffer; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { + const ret = debugString(getObject(arg1)); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; + }; + imports.wbg.__wbindgen_bigint_get_as_i64 = function(arg0, arg1) { + const v = getObject(arg1); + const ret = typeof(v) === 'bigint' ? v : undefined; + if (!isLikeNone(ret)) { + _assertBigInt(ret); + } + getBigInt64Memory0()[arg0 / 8 + 1] = isLikeNone(ret) ? BigInt(0) : ret; + getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret); + }; + imports.wbg.__wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); }; imports.wbg.__wbindgen_throw = function(arg0, arg1) { throw new Error(getStringFromWasm0(arg0, arg1)); }; + imports.wbg.__wbindgen_memory = function() { + const ret = wasm.memory; + return addHeapObject(ret); + }; return imports; } -function initMemory(imports, maybe_memory) { +function __wbg_init_memory(imports, maybe_memory) { } -function finalizeInit(instance, module) { +function __wbg_finalize_init(instance, module) { wasm = instance.exports; - init.__wbindgen_wasm_module = module; - cachedInt32Memory0 = new Int32Array(); - cachedUint8Memory0 = new Uint8Array(); + __wbg_init.__wbindgen_wasm_module = module; + cachedBigInt64Memory0 = null; + cachedFloat64Memory0 = null; + cachedInt32Memory0 = null; + cachedUint8Memory0 = null; return wasm; } -function initSync(bytes) { - const imports = getImports(); +function initSync(module) { + if (wasm !== undefined) return wasm; + + const imports = __wbg_get_imports(); - initMemory(imports); + __wbg_init_memory(imports); + + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } - const module = new WebAssembly.Module(bytes); const instance = new WebAssembly.Instance(module, imports); - return finalizeInit(instance, module); + return __wbg_finalize_init(instance, module); } -async function init(input) { +async function __wbg_init(input) { + if (wasm !== undefined) return wasm; + if (typeof input === 'undefined') { input = new URL('scheduler_bg.wasm', import.meta.url); } - const imports = getImports(); + const imports = __wbg_get_imports(); if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) { input = fetch(input); } - initMemory(imports); + __wbg_init_memory(imports); - const { instance, module } = await load(await input, imports); + const { instance, module } = await __wbg_load(await input, imports); - return finalizeInit(instance, module); + return __wbg_finalize_init(instance, module); } export { initSync } -export default init; +export default __wbg_init; diff --git a/pkg/scheduler.wasm b/pkg/scheduler.wasm deleted file mode 100644 index fce2f9a0..00000000 Binary files a/pkg/scheduler.wasm and /dev/null differ diff --git a/pkg/scheduler_bg.wasm b/pkg/scheduler_bg.wasm index 4f149917..6bc66620 100644 Binary files a/pkg/scheduler_bg.wasm and b/pkg/scheduler_bg.wasm differ diff --git a/pkg/scheduler_bg.wasm.d.ts b/pkg/scheduler_bg.wasm.d.ts index 7f767af8..93f3f419 100644 --- a/pkg/scheduler_bg.wasm.d.ts +++ b/pkg/scheduler_bg.wasm.d.ts @@ -2,6 +2,8 @@ /* eslint-disable */ export const memory: WebAssembly.Memory; export function schedule(a: number, b: number): void; -export function __wbindgen_malloc(a: number): number; -export function __wbindgen_realloc(a: number, b: number, c: number): number; +export function __wbindgen_malloc(a: number, b: number): number; +export function __wbindgen_realloc(a: number, b: number, c: number, d: number): number; export function __wbindgen_add_to_stack_pointer(a: number): number; +export function __wbindgen_free(a: number, b: number, c: number): void; +export function __wbindgen_exn_store(a: number): void; diff --git a/src/bin/flamegraph-bin.rs b/src/bin/flamegraph-bin.rs index 95f627a2..be472363 100644 --- a/src/bin/flamegraph-bin.rs +++ b/src/bin/flamegraph-bin.rs @@ -1,23 +1,17 @@ extern crate scheduler; -use scheduler::legacy::input::Input; -use std::error::Error; -use std::fs::File; -use std::io::BufReader; -use std::path::Path; /// To generate a flamegraph of the scheduler on your machine, follow the platform-specific instructions [here](https://github.com/flamegraph-rs/flamegraph). /// If you're running inside WSL2 you'll probably need to follow https://gist.github.com/abel0b/b1881e41b9e1c4b16d84e5e083c38a13 /// Then `cargo flamegraph --bin flamegraph` fn main() { - let path = Path::new("./tests/jsons/stable/demo-2-with-filler-with-budget/input.json"); - - let input: Input = get_input_from_json(path).unwrap(); - let _output = scheduler::run_scheduler(input); + // let path = Path::new("./tests/jsons/stable/algorithm-challenge/input.json"); + // let input = get_input_from_json(path).unwrap(); + // let _output = scheduler::run_scheduler(input); } -pub fn get_input_from_json>(path: P) -> Result> { - let file = File::open(path)?; - let reader = BufReader::new(file); - let input = serde_json::from_reader(reader)?; - Ok(input) -} +// pub fn get_input_from_json>(path: P) -> Result<&JsValue, Box> { +// let file = File::open(path)?; +// let reader = BufReader::new(file); +// let input = serde_json::from_reader(reader)?; +// Ok(input) +// } diff --git a/src/bin/main.rs b/src/bin/main.rs new file mode 100644 index 00000000..1ff873a1 --- /dev/null +++ b/src/bin/main.rs @@ -0,0 +1,24 @@ +use chrono::NaiveDateTime; +use serde::Deserialize; +use serde_json::{self, Value}; +use std::{fs, path::Path}; +extern crate scheduler; +use scheduler::{models::goal::Goal, run_scheduler}; +fn main() { + println!("Running!"); + let path = Path::new("./tests/jsons/stable/algorithm-challenge/input.json"); + let file = fs::File::open(path).expect("file should open read only"); + let json: Value = serde_json::from_reader(file).expect("file should be proper JSON"); + dbg!(&json); + let input: Input = serde_json::from_value(json).unwrap(); + dbg!(&input); + run_scheduler(input.start_date, input.end_date, input.goals); +} + +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +struct Input { + start_date: NaiveDateTime, + end_date: NaiveDateTime, + goals: Vec, +} diff --git a/src/legacy/budget.rs b/src/legacy/budget.rs deleted file mode 100644 index 05531acf..00000000 --- a/src/legacy/budget.rs +++ /dev/null @@ -1,52 +0,0 @@ -use std::collections::HashMap; - -use chrono::NaiveDateTime; -use serde::Deserialize; - -use super::date::deserialize_normalized_date; -use super::slot::Slot; - -/// Keeps track of the min and max time allowed and scheduled per time period for a collection of Steps/Tasks. -#[derive(Debug, Deserialize, Clone, Copy, PartialEq)] -pub struct Budget { - pub budget_type: BudgetType, - pub min: Option, - pub max: Option, -} - -/// weekly or daily -#[derive(Debug, Deserialize, Clone, Copy, PartialEq)] -pub enum BudgetType { - Weekly, - Daily, -} - -#[derive(Debug, Deserialize)] //Todo deserialize not needed as this is not in input, only TaskBudget is -pub struct StepBudgets { - #[serde(deserialize_with = "deserialize_normalized_date")] - pub calendar_start: NaiveDateTime, - #[serde(deserialize_with = "deserialize_normalized_date")] - pub calendar_end: NaiveDateTime, - /// A map from goal IDs to a vector of budget IDs associated with that goal - pub budget_ids_map: HashMap>, - /// A map from goal IDs to the `StepBudget` objects associated with that goal. - pub budget_map: HashMap>, -} - -#[derive(Debug, Deserialize)] -pub struct StepBudget { - #[allow(dead_code)] - pub(crate) step_budget_type: BudgetType, - pub slot_budgets: Vec, - pub min: Option, //only needed once, can't remove as used for subsequent SlotBudget initialization? - #[allow(dead_code)] - pub(crate) max: Option, //only needed once, can't remove as used for subsequent SlotBudget initialization? -} - -#[derive(Debug, Deserialize)] -pub struct SlotBudget { - pub slot: Slot, - pub min: Option, - pub max: Option, - pub used: usize, -} diff --git a/src/legacy/date.rs b/src/legacy/date.rs deleted file mode 100644 index d8a264cb..00000000 --- a/src/legacy/date.rs +++ /dev/null @@ -1,17 +0,0 @@ -use chrono::{NaiveDateTime, NaiveTime, Timelike}; -use serde::{Deserialize, Deserializer}; -pub fn normalize_date(date: &NaiveDateTime) -> NaiveDateTime { - create_date_by_hour(date, date.hour() as usize) -} -pub fn create_date_by_hour(date: &NaiveDateTime, hour: usize) -> NaiveDateTime { - NaiveDateTime::new( - date.date(), - NaiveTime::from_hms_opt(hour as u32, 0, 0).unwrap(), - ) -} -pub fn deserialize_normalized_date<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - Ok(normalize_date(&Deserialize::deserialize(deserializer)?)) -} diff --git a/src/legacy/goal.rs b/src/legacy/goal.rs deleted file mode 100644 index 4f176ae9..00000000 --- a/src/legacy/goal.rs +++ /dev/null @@ -1,107 +0,0 @@ -use super::repetition::Repetition; -use chrono::NaiveDateTime; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, option::Option}; - -use super::{budget::Budget, slot::Slot}; - -pub type GoalsMap = HashMap; - -/// An aim or desired result someone wants to reach. -#[derive(Deserialize, Debug, Default, Clone, PartialEq)] -pub struct Goal { - // mandatory fields - /// The id passed by the frontend, usually a uuid. - pub id: String, - /// The title given to the Goal, ie "Run", "Read a book" or "Become a nuclear scientist". - pub title: String, - - // calculation of the slots - /// Schedule on calender after this datetime only. - #[serde(default)] - pub start: Option, - /// Goal has to be achieved until this datetime. - #[serde(default)] - pub deadline: Option, - /// The minimum duration per Step towards the Goal. - #[serde(default)] - pub min_duration: Option, - /// The maximum duration, if the other Goals allow for it. - #[serde(default)] - pub max_duration: Option, - - // repeatability - /// Repetition like 'daily' or 'weekly'. - pub repeat: Option, - - // constraints - /// Filters that reduce the potential Timeline of the Steps for this Goal. - /// Examples: After 8, Weekends, not this afternoon - #[serde(default)] - pub filters: Option, - /// Budgets that apply to this Goal, and all of its subGoals - if any. - #[serde(default)] - pub budgets: Option>, - - // additional stuff; yet not necessary for the algorithm - /// Ids of the subGoals this Goal has - if any. - /// Example: Goal 'Work' has subGoal 'ProjectA', which has subGoals 'Prepare for meeting', 'Meeting', etc... - #[serde(default)] - pub children: Option>, - /// If there is a specific order, this Goal can only be scheduled after certain other Goals complete. - #[serde(default)] - pub after_goals: Option>, - - // ??? - /// Internal - should be private - #[serde(default)] - pub tags: Vec, -} - -/// Mon Tue Wed Thu Fri Sat Sun -#[derive(Deserialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq)] -pub enum Day { - #[serde(rename = "Mon")] - Monday, - #[serde(rename = "Tue")] - Tuesday, - #[serde(rename = "Wed")] - Wednesday, - #[serde(rename = "Thu")] - Thursday, - #[serde(rename = "Fri")] - Friday, - #[serde(rename = "Sat")] - Saturday, - #[serde(rename = "Sun")] - Sunday, -} - -/// Filters used to reduce the Timeline on which a Goal can be scheduled. -#[derive(Debug, Deserialize, Clone, PartialEq)] -pub struct TimeFilter { - /// Whatever day this Goal gets scheduled on - only schedule it after this time. - pub after_time: Option, - /// Whatever day this Goal gets scheduled on - only schedule it before this time. - pub before_time: Option, - /// Only schedule this Goal on these days of the week. - pub on_days: Option>, - /// For whatever reason - don't schedule the Goal during these time slots. - pub not_on: Option>, -} - -// TODO 2023-05-05 | Struct Tag should not be public and think hard about if we can remove them as it complicates the logic -// As agreed in a meeting to refactor this and seperate Tag for Goals and Taasks -// === -/// Helper tags for the algorithm -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] -pub enum Tag { - DoNotSplit, - Weekly, - Optional, - FlexDur, - Remove, - IgnoreStepGeneration, - Filler, - Budget, -} diff --git a/src/legacy/input.rs b/src/legacy/input.rs deleted file mode 100644 index 098af750..00000000 --- a/src/legacy/input.rs +++ /dev/null @@ -1,40 +0,0 @@ -use super::budget::StepBudgets; -use super::date::deserialize_normalized_date; -use super::goal::GoalsMap; -use super::step::Step; -use chrono::prelude::*; -use serde::Deserialize; - -/// The front end gets a Calendar by passing a JSON data into the scheduler, via an Input object. -/// It has the requested calendar start and end, and a collection of goals that the scheduler needs to schedule. -/// On the scheduler side, the JSON is received as a ['JSValue'](https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/struct.JsValue.html). -/// This allows it to be deserialized into this struct. -#[derive(Deserialize, Debug)] -pub struct Input { - #[serde(rename = "startDate")] - #[serde(deserialize_with = "deserialize_normalized_date")] - pub calendar_start: NaiveDateTime, - #[serde(rename = "endDate")] - #[serde(deserialize_with = "deserialize_normalized_date")] - pub calendar_end: NaiveDateTime, - pub goals: GoalsMap, -} - -#[derive(Debug, Deserialize)] -pub struct StepsToPlace { - #[serde(deserialize_with = "deserialize_normalized_date")] - pub calendar_start: NaiveDateTime, - #[serde(deserialize_with = "deserialize_normalized_date")] - pub calendar_end: NaiveDateTime, - pub steps: Vec, - pub step_budgets: StepBudgets, -} - -#[derive(Deserialize, Debug)] -pub struct PlacedSteps { - #[serde(deserialize_with = "deserialize_normalized_date")] - pub calendar_start: NaiveDateTime, - #[serde(deserialize_with = "deserialize_normalized_date")] - pub calendar_end: NaiveDateTime, - pub steps: Vec, -} diff --git a/src/legacy/mod.rs b/src/legacy/mod.rs deleted file mode 100644 index b292f9c6..00000000 --- a/src/legacy/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod budget; -mod date; -pub mod goal; -pub mod input; -pub mod output; -mod repetition; -mod slot; -mod step; -mod timeline; diff --git a/src/legacy/output.rs b/src/legacy/output.rs deleted file mode 100644 index aadef917..00000000 --- a/src/legacy/output.rs +++ /dev/null @@ -1,46 +0,0 @@ -//new module for outputting the result of step_placer in -//whichever format required by front-end -use super::date::deserialize_normalized_date; -use super::goal::Tag; -use chrono::{NaiveDate, NaiveDateTime}; -use serde::{Deserialize, Serialize}; -use std::cmp::Ordering; -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Task { - pub taskid: usize, - pub goalid: String, - pub title: String, - pub duration: usize, - #[serde(deserialize_with = "deserialize_normalized_date")] - pub start: NaiveDateTime, - #[serde(deserialize_with = "deserialize_normalized_date")] - pub deadline: NaiveDateTime, - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(skip)] - pub tags: Vec, - #[serde(skip)] - pub impossible: bool, -} - -impl Ord for Task { - fn cmp(&self, other: &Self) -> Ordering { - self.start.cmp(&other.start) - } -} - -impl PartialOrd for Task { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] -pub struct DayTasks { - pub day: NaiveDate, - pub tasks: Vec, -} -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Default)] -pub struct FinalTasks { - pub scheduled: Vec, - pub impossible: Vec, -} diff --git a/src/legacy/repetition.rs b/src/legacy/repetition.rs deleted file mode 100644 index cf447324..00000000 --- a/src/legacy/repetition.rs +++ /dev/null @@ -1,159 +0,0 @@ -use serde::de::{self, Visitor}; -use serde::Deserialize; -use serde::*; -use std::fmt; - -/// How often a goal repeats. -/// Textual descriptions of a repetition from the front-end -/// (e.g. "4/week" or "mondays") are converted into this enum -/// via a custom serde deserializer. -/// This enum is used by the Goal struct for it's "repeat" field, to -/// determine how many steps to generate from a goal. -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum Repetition { - Mondays, - Tuesdays, - Wednesdays, - Thursdays, - Fridays, - Saturdays, - Sundays, - - Weekdays, - Weekends, - - Hourly, - Daily(usize), - Weekly(usize), - - #[allow(non_camel_case_types)] - EveryXHours(usize), - #[allow(non_camel_case_types)] - EveryXDays(usize), - - #[allow(non_camel_case_types)] - FlexDaily(usize, usize), - #[allow(non_camel_case_types)] - FlexWeekly(usize, usize), -} - -//How to implement serde deserialize: https://serde.rs/impl-deserialize.html -struct RepetitionVisitor; - -impl<'de> Visitor<'de> for RepetitionVisitor { - type Value = Repetition; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!( - formatter, - "a string that follows the zinzen json schema either daily, hourly, weekly, mondays etc." - ) - } - - fn visit_str(self, s: &str) -> Result - where - E: de::Error, - { - match s { - "mondays" => Ok(Repetition::Mondays), - "tuesdays" => Ok(Repetition::Tuesdays), - "wednesdays" => Ok(Repetition::Wednesdays), - "thursdays" => Ok(Repetition::Thursdays), - "fridays" => Ok(Repetition::Fridays), - "saturdays" => Ok(Repetition::Saturdays), - "sundays" => Ok(Repetition::Sundays), - - "daily" => Ok(Repetition::Daily(1)), - "hourly" => Ok(Repetition::Hourly), - "weekly" => Ok(Repetition::Weekly(1)), - - "weekdays" => Ok(Repetition::Weekdays), - "weekends" => Ok(Repetition::Weekends), - - _ => { - if s.contains('-') && s.contains('/') { - //e.g. '3-5/week' - let split = s.split('/').collect::>(); - let numbers = split[0]; //e.g. 3-5 - let rep = split[1]; //e.g. week - let split = numbers.split('-').collect::>(); - let min = split[0] - .parse::() - .expect("expected format to be x-y/period"); //e.g. 3 - let max = split[1] - .parse::() - .expect("expected format to be x-y/period"); //e.g. 5 - match rep { - "week" => Ok(Repetition::FlexWeekly(min, max)), - "day" => Ok(Repetition::FlexDaily(min, max)), - _ => panic!("unrecognized repetition: {}", rep), - } - } else if s.contains('/') { - //e.g. '4/week' - let split = s.split('/').collect::>(); - let num = split[0] - .parse::() - .expect("expected format to be x/period"); - match split[1] { - "week" => Ok(Repetition::Weekly(num)), - "day" => Ok(Repetition::Daily(num)), - _ => panic!("unrecognized repetition: {}", s), - } - } else if s.contains(' ') { - //e.g. 'every 5 days' - let split = s.split(' ').collect::>(); - let num = split[1] - .parse::() - .expect("front end should use format 'every x days' or 'every x hours' "); - let rep = split[2]; - if rep == "days" { - Ok(Repetition::EveryXDays(num)) - } else if rep == "hours" { - Ok(Repetition::EveryXHours(num)) - } else { - panic!("front end should use format 'every x days' or 'every x hours' "); - } - } else { - Err(E::custom("Error deserializing goal")) - } - } - } - } -} - -impl<'de> Deserialize<'de> for Repetition { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_string(RepetitionVisitor) - } -} - -//The main reason Display is being implemented for repetition -// is so that the string representation of Repetition::MONDAYS-SUNDAYS matches the -//string representation of chrono::Weekday(). This makes it easy in the TimeSlotsIterator to do -//If self.start.weekday().to_string() == self.repetition.unwrap().to_string(). -//see: https://docs.rs/chrono/latest/src/chrono/weekday.rs.html#141 -impl fmt::Display for Repetition { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(match *self { - Repetition::Daily(_) => "DAILY", - Repetition::Hourly => "HOURLY", - Repetition::Weekly(_) => "Weekly", - Repetition::Weekdays => "WEEKDAYS", - Repetition::Weekends => "WEEKENDS", - Repetition::EveryXDays(_) => "EveryXdays", - Repetition::EveryXHours(_) => "EveryXhours", - Repetition::Mondays => "Mon", - Repetition::Tuesdays => "Tue", - Repetition::Wednesdays => "Wed", - Repetition::Thursdays => "Thu", - Repetition::Fridays => "Fri", - Repetition::Saturdays => "Sat", - Repetition::Sundays => "Sun", - Repetition::FlexDaily(_, _) => "FlexDaily", - Repetition::FlexWeekly(_, _) => "FlexWeekly", - }) - } -} diff --git a/src/legacy/slot/iterator.rs b/src/legacy/slot/iterator.rs deleted file mode 100644 index 6a3c728e..00000000 --- a/src/legacy/slot/iterator.rs +++ /dev/null @@ -1,51 +0,0 @@ -use super::Slot; -use chrono::{Duration, NaiveDateTime}; - -/// Iterator for a `Slot` and provide functionalities to walk through -/// a `Slot` based on custom interval duration -#[derive(Debug, Clone)] -pub struct SlotIterator { - slot: Slot, - pointer: NaiveDateTime, - /// Duration interval for pointer to corss over slot - interval: Duration, -} -impl SlotIterator { - /// Initialize new SlotIterator with default interval duration to 1 day - #[allow(dead_code)] - pub fn initialize(slot: Slot) -> SlotIterator { - SlotIterator { - slot, - pointer: slot.start, - interval: Duration::days(1), - } - } - - /// Create new SlotIterator with custom interval duration - pub fn new(slot: Slot, interval_duration: Duration) -> SlotIterator { - SlotIterator { - slot, - pointer: slot.start, - interval: interval_duration, - } - } -} - -impl Iterator for SlotIterator { - type Item = Slot; - - fn next(&mut self) -> Option { - if self.pointer >= self.slot.end { - return None; - } - let next_pointer = self.pointer + self.interval; - - let slot = Slot { - start: self.pointer, - end: next_pointer, - }; - self.pointer = next_pointer; - - Some(slot) - } -} diff --git a/src/legacy/slot/mod.rs b/src/legacy/slot/mod.rs deleted file mode 100644 index 9bafe139..00000000 --- a/src/legacy/slot/mod.rs +++ /dev/null @@ -1,62 +0,0 @@ -pub mod iterator; - -use super::date::deserialize_normalized_date; -use chrono::{Datelike, NaiveDateTime, Timelike}; -use serde::Deserialize; -use std::fmt::{self, Debug, Display}; - -// TODO 2023-04-26 | Slot rules as below: -// - A rule that slot.end must not be before slot.start -// - A suggestion to add rule to create 2 slots if slot.start is after slot.end - -#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Deserialize)] -pub struct Slot { - #[serde(deserialize_with = "deserialize_normalized_date")] - pub start: NaiveDateTime, - #[serde(deserialize_with = "deserialize_normalized_date")] - pub end: NaiveDateTime, -} - -#[derive(PartialEq, Eq, Clone, Debug)] -pub struct SlotConflict { - pub slot: Slot, - pub num_conflicts: usize, -} - -impl Debug for Slot { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let start = self.start; - let end = self.end; - write!( - f, - "Slot {{ \n\tstart:\t {:02}-{:02}-{:02} {:02},\n\t end:\t {:02}-{:02}-{:02} {:02}, \n}}", - start.year(), - start.month(), - start.day(), - start.hour(), - end.year(), - end.month(), - end.day(), - end.hour(), - ) - } -} - -impl Display for Slot { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let start = self.start; - let end = self.end; - write!( - f, - "Slot {{ start: {:02}-{:02}-{:02} {:02} - end: {:02}-{:02}-{:02} {:02} }}", - start.year(), - start.month(), - start.day(), - start.hour(), - end.year(), - end.month(), - end.day(), - end.hour(), - ) - } -} diff --git a/src/legacy/step.rs b/src/legacy/step.rs deleted file mode 100644 index 47f825da..00000000 --- a/src/legacy/step.rs +++ /dev/null @@ -1,67 +0,0 @@ -use chrono::NaiveDateTime; -use serde::{Deserialize, Serialize}; - -use super::{ - goal::{Goal, Tag}, - slot::Slot, - timeline::Timeline, -}; - -/// Steps are generated to achieve a Goal in one or more Steps. -/// A leaf Goal can generate one or more Steps. -#[derive(Deserialize, Debug, Eq, Clone, PartialEq)] -pub struct Step { - /// Only used by the scheduler. - /// Unstable between scheduler runs if input changes. - pub id: usize, - /// Reference to the Goal a Step was generated from. - pub goal_id: String, - /// Title of the Goal the Step was generated from. - /// Duplicated for ease of debugging and simplicity of code. - pub title: String, - /// Duration the Step wants to claim on the Calendar. - /// This duration is equal or part of the Goal duration. - pub duration: usize, - /// Used for finding next Step to be scheduled in combination with Step flexibility and Tags. - pub status: StepStatus, - /// Used for finding next Step to be scheduled in combination with Step Status and Tags. - pub flexibility: usize, - /// Final start time for Step on Calendar - should be removed in favor of Timeline + SlotStatus combination. - pub start: Option, - /// Final end time for Step on Calendar - should be removed in favor of Timeline + SlotStatus combination. - pub deadline: Option, - /// The places on Calendar that could potentially be used given the Goal constraints - and what other scheduled Steps already have consumed. - pub slots: Vec, - /// Used for finding next Step to be scheduled in combination with Step flexibility and Status. - #[serde(default)] - pub tags: Vec, - /// Used for adding Blocked Step Tag, used in finding next Step to be scheduled. - #[serde(default)] - pub after_goals: Option>, -} - -/// Used to decide in which order to schedule steps, together with their flexibility -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] -pub enum StepStatus { - /// Task is scheduled and can't be modified any more. - Scheduled, - /// Task is impossible - its MaybeSlots Timeline is removed. - Impossible, - /// Task is waiting for something to be properly initialized. - Uninitialized, - /// Task is waiting for another Goal to be scheduled first. - Blocked, - /// Task is available for scheduling, but its relative flexibility and Tags will determine if it gets picked first - ReadyToSchedule, -} - -#[derive(Debug, Clone)] -pub struct NewStep { - pub step_id: usize, - pub title: String, - pub duration: usize, - pub goal: Goal, - pub timeline: Timeline, - pub status: StepStatus, - pub timeframe: Option, -} diff --git a/src/legacy/timeline/iterator.rs b/src/legacy/timeline/iterator.rs deleted file mode 100644 index dac33931..00000000 --- a/src/legacy/timeline/iterator.rs +++ /dev/null @@ -1,125 +0,0 @@ -use super::super::slot::{iterator::SlotIterator, Slot}; -use super::Timeline; -use chrono::{Duration, NaiveDateTime, Timelike}; - -// TODO 2023-05-20 | create edge cases to test behavior when first slot start time in the timeline is not 00 (midnight) -// - Test idea to froce make the start of the timeline from 00 (midnight) of the first whatever even if it is other time in the same day - -/* -TimelineIterator goals: -- ability to cros over timeline slots like SlotIterator through interval duration - - cross over timeline by 1 day duration -- Get count of days, hours, minutes, etc for a timeline - -Ideas: -- Ability to move to a specific time in the timeline. -- Ability to perform some action in a timeline without -forcing to split slots into hours, or similar. -- -*/ - -/// Iterator for a `Timeline` and provide functionalities to walk through -/// slots in a `Timeline` based on custom interval duration -#[derive(Debug, Clone)] -pub struct TimelineIterator { - timeline: Timeline, - /// Duration interval for pointer to corss over timeline timelines - interval: Duration, -} - -impl TimelineIterator { - /// Initialize new TimelineIterator with default interval duration to 1 day - #[allow(dead_code)] - pub fn initialize(timeline: Timeline) -> TimelineIterator { - // if let Some(_) = timeline.slots.first() { - if timeline.slots.first().is_some() { - TimelineIterator { - timeline, - interval: Duration::days(1), - } - } else { - panic!("Timeline slots are empty") - } - } - - /// Create new TimelineIterator with custom interval duration - #[allow(dead_code)] - pub fn new(timeline: Timeline, interval_duration: Duration) -> TimelineIterator { - if timeline.slots.first().is_some() { - TimelineIterator { - timeline, - interval: interval_duration, - } - } else { - panic!("Timeline slots are empty") - } - } - - /// Create new TimelineIterator which iterate for a daily calendar - /// day regardless time of slots in the timeline - #[allow(dead_code)] - pub fn new_calendar_day(timeline: Timeline) -> TimelineIterator { - // TODO 2023-07-11: based on debugging in https://github.com/tijlleenders/ZinZen-scheduler/pull/363 - // for case bug_215, agreed to create a custom TimelineIterator to iterate on daily basis from - // midnight to midnight. - if let Some(first_slot) = timeline.slots.first() { - let start_date = first_slot - .start - .with_hour(0) - .unwrap() - .with_minute(0) - .unwrap() - .with_second(0) - .unwrap(); - let end_date: NaiveDateTime; - if timeline.slots.len() == 1 { - end_date = first_slot.end; - } else if let Some(last_slot) = timeline.slots.last() { - end_date = last_slot.end; - } else { - panic!("Can't get last timeline slot") - } - - let custom_timeline = Timeline::initialize(start_date, end_date).unwrap(); - TimelineIterator::initialize(custom_timeline) - } else { - panic!("Timeline slots are empty") - } - } -} - -/// Walk through list of slots in timeline based on custom interval duration -impl Iterator for TimelineIterator { - type Item = Vec; - - fn next(&mut self) -> Option { - if self.timeline.slots.is_empty() { - return None; - } - - if let Some(first_slot) = self.timeline.slots.first() { - match self.timeline.slots.take(&first_slot.clone()) { - Some(slot) => { - let slot_duration = slot.end.signed_duration_since(slot.start); - - // A condition to avoid iteration over slots when inerval > slot duration - let slot_iterator: SlotIterator = if self.interval > slot_duration { - SlotIterator::new(slot, slot_duration) - } else { - SlotIterator::new(slot, self.interval) - }; - - let mut walking_slots: Vec = vec![]; - for slot in slot_iterator { - walking_slots.push(slot); - } - - Some(walking_slots) - } - None => None, - } - } else { - None - } - } -} diff --git a/src/legacy/timeline/mod.rs b/src/legacy/timeline/mod.rs deleted file mode 100644 index 2296a523..00000000 --- a/src/legacy/timeline/mod.rs +++ /dev/null @@ -1,43 +0,0 @@ -pub mod iterator; - -use super::slot::Slot; -use chrono::NaiveDateTime; -use serde::Deserialize; -use std::collections::BTreeSet; - -pub type TimelineSlotsType = BTreeSet; - -//TODO 2023-04-21 -// - Implement Display for Timeline -// - If possible to develop divide timeline into hours, days, weeks, months, years - -/// Timeline controlling passing list of slots in the system -/// Provide 2 public functionalities: -/// 1. remove timeline which is a list of slots -/// 2. get next slot of timeline -#[derive(Debug, Deserialize, PartialEq, Clone, Default)] -pub struct Timeline { - pub slots: TimelineSlotsType, -} - -impl Timeline { - /// Create new empty timeline - #[allow(dead_code)] - pub fn new() -> Timeline { - let collection: TimelineSlotsType = BTreeSet::new(); - Timeline { slots: collection } - } - - /// Initialize a new timeline - #[allow(dead_code)] - pub fn initialize(start: NaiveDateTime, end: NaiveDateTime) -> Option { - let init_slot: Slot = Slot { start, end }; - let mut collection: TimelineSlotsType = BTreeSet::new(); - - if collection.insert(init_slot) { - Some(Timeline { slots: collection }) - } else { - None - } - } -} diff --git a/src/lib.rs b/src/lib.rs index 751f998c..ded8e85c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,31 +1,19 @@ //! # ZinZen scheduler //! //! The ZinZen scheduler is a "calendar as a function". -//! Input: A calendar start datetime and end datetime, plus some Goals with flexible time constraints. +//! Input: A calendar start datetime and end datetime, plus a Directed Acyclical Graph of Goals/Budgets with time constraints. //! Output: A calendar that successfully allocates all Goals - or the maximum amount of Goals in that time period. //! -//! ``` -//! use scheduler::legacy::input::Input; -//! -//! let json_input: serde_json::Value = serde_json::json!({ -//! "startDate": "2022-01-01T00:00:00", -//! "endDate": "2022-01-09T00:00:00", -//! "goals": { -//! "uuid1": { -//! "id": "uuid1", -//! "title": "sleep", -//! "min_duration": 8, -//! "repeat": "daily", -//! "filters": { -//! "after_time": 22, -//! "before_time": 8 -//! } -//! } -//! } -//! }); -//! let input: Input = serde_json::from_value(json_input).unwrap(); -//! let output = scheduler::run_scheduler(input); -//! ``` +// TODO: fix DocTest +// ``` +// use scheduler::scheduler; +// +// let json_input: serde_json::Value = serde_json::json!({ +// "TODO_working_example" +// }); +// let input: Input = serde_json::from_value(json_input).unwrap(); +// let output = scheduler::run_scheduler(input); +// ``` //! //! ## Getting Started //! This project is hosted on [Github](https://github.com/tijlleenders/ZinZen-scheduler). The Docs.rs / Crates.io version is probably (far) behind. @@ -64,18 +52,18 @@ //! ZinZen® trademark is a tool to protect the ZinZen® identity and the //! quality perception of the ZinZen® projects. -use std::rc::Rc; +use chrono::NaiveDateTime; +use models::{activity::Activity, calendar::Calendar, goal::Goal, task::FinalTasks}; +use serde_wasm_bindgen::{from_value, to_value}; +use services::activity_generator; +use services::activity_placer; +use technical::input_output::Input; use wasm_bindgen::prelude::*; - -pub mod legacy; -/// The data structures pub mod models; -use crate::legacy::input::Input; -use crate::legacy::output::FinalTasks; +pub mod services; +/// The data structures +pub mod technical; -/// The services handling the data structures -use crate::models::calendar::{Calendar, Goals}; -use crate::models::date::{DateTime, DateTimeRange}; #[wasm_bindgen(typescript_custom_section)] const TS_APPEND_CONTENT: &'static str = r#" interface Input { @@ -90,123 +78,47 @@ interface Input { #[wasm_bindgen] pub fn schedule(input: &JsValue) -> Result { console_error_panic_hook::set_once(); - let input: Input = serde_wasm_bindgen::from_value(input.clone())?; - let final_tasks = run_scheduler(input); - Ok(serde_wasm_bindgen::to_value(&final_tasks)?) + // JsError implements From, so we can just use `?` on any Error + let input: Input = from_value(input.clone()).unwrap(); + let final_tasks = run_scheduler(input.start_date, input.end_date, input.goals); + Ok(to_value(&final_tasks)?) } -pub fn run_scheduler(input: Input) -> FinalTasks { - let date_start = DateTime::from_naive_date_time(&input.calendar_start); - let date_end = DateTime::from_naive_date_time(&input.calendar_end); - let goals = get_goals(&input); +pub fn run_scheduler( + start_date: NaiveDateTime, + end_date: NaiveDateTime, + goals: Vec, +) -> FinalTasks { + let mut calendar = Calendar::new(start_date, end_date); + dbg!(&calendar); - let calendar = Calendar::new(&input, &goals); + calendar.add_budgets_from(&goals); - while !calendar.has_finished_scheduling() { - log::info!("\n{calendar:?}"); + //generate and place simple goal activities + let simple_goal_activities = + activity_generator::generate_simple_goal_activities(&calendar, &goals); + dbg!(&simple_goal_activities); + activity_placer::place(&mut calendar, simple_goal_activities); - #[derive(PartialEq)] - enum Handling { - DoNothing, - Flexibility1, - MostFlexibility, - Impossible, - } + //generate and place budget goal activities + let budget_goal_activities: Vec = + activity_generator::generate_budget_goal_activities(&calendar, &goals); + dbg!(&calendar); + activity_placer::place(&mut calendar, budget_goal_activities); - // determine flexibility - // (Handling marker, flexibility measure, position in the calender unproccessed vector) - let mut handling = (Handling::DoNothing, 0, None); - let mut unprocessed = calendar - .unprocessed() - .iter() - .map(|pos| calendar.flexibility(*pos).unwrap()) - .collect::>(); - unprocessed.sort_by(|(_, _, a), (_, _, b)| a.goal.id().cmp(&b.goal.id())); - for (pos, flex, _f) in unprocessed { - match flex { - 0 => { - handling = (Handling::Impossible, flex, Some(pos)); - log::info!("Impossible {flex} {pos}"); - break; - } - 1 => { - handling = (Handling::Flexibility1, flex, Some(pos)); - log::info!("Flexibility1 {flex} {pos}"); - break; - } - _ if handling.2.is_none() => { - handling = { - log::info!("MostFlexibiltiy {flex} {pos}"); - (Handling::MostFlexibility, flex, Some(pos)) - } - } - _ => { - handling = if handling.1 < flex { - log::info!("MostFlexibiltiy {flex} {pos}"); - (Handling::MostFlexibility, flex, Some(pos)) - } else { - handling - } - } - } - } - log::info!( - "selected position in unprocesse vec of calendar {:?}", - handling.2, - ); + calendar.log_impossible_min_day_budgets(); - // calculate placement - if let (handling, _flex, Some(selected)) = handling { - match handling { - Handling::DoNothing => break, - Handling::Impossible => { - if let Some((_flexibility, _tail)) = calendar.take(selected) { - calendar.push_impossible( - selected, - DateTimeRange::new(date_start.clone(), date_end.clone()), - ); - } - } - Handling::Flexibility1 => { - if let Some((flexibility, _tail)) = calendar.take(selected) { - let slot = flexibility.day.first_fit(flexibility.goal.min_span()); - calendar.push_scheduled(selected, slot); - } - } - Handling::MostFlexibility => { - if let Some((flexibility, tail)) = calendar.take(selected) { - if tail.is_empty() { - let slot = flexibility.day.first_fit(flexibility.goal.min_span()); - calendar.push_scheduled(selected, slot); - } else { - let slots = flexibility.day.slots(flexibility.goal.min_span()); - let (_, to_occupy) = tail - .iter() - .map(|pos| { - calendar.flexibility_at(*pos).unwrap().day.overlap(&slots) - }) - .map(|v| v.into_iter().min_by(|(a, _), (b, _)| a.cmp(b)).unwrap()) - .min_by(|(a, _), (b, _)| a.cmp(b)) - .unwrap(); - calendar.push_scheduled(selected, to_occupy); - } - } - } - } - } else { - break; - } - } - log::info!("\n{calendar:?}"); + let get_to_week_min_budget_activities = + activity_generator::generate_get_to_week_min_budget_activities(&calendar, &goals); + activity_placer::place(&mut calendar, get_to_week_min_budget_activities); + //TODO: Test that day stays below min when week min being reached so other goals can get to the week min too - calendar.result() -} + calendar.log_impossible_min_week_budgets(); + + let top_up_week_budget_activities = + activity_generator::generate_top_up_week_budget_activities(&calendar, &goals); + activity_placer::place(&mut calendar, top_up_week_budget_activities); + //TODO: Test that day stays below min or max when week max being reachd -/// helper function for legacy code -fn get_goals(input: &Input) -> Goals { - input - .goals - .values() - .map(|g| Rc::new(g.into())) - .collect::>() + calendar.print() } diff --git a/src/models/activity.rs b/src/models/activity.rs new file mode 100644 index 00000000..a6af1186 --- /dev/null +++ b/src/models/activity.rs @@ -0,0 +1,517 @@ +use chrono::{Datelike, Days, Duration, NaiveDateTime}; +use serde::Deserialize; + +use super::budget::Budget; +use super::goal::Goal; +use super::{calendar::Calendar, goal::Filters}; +use crate::models::budget::TimeBudget; +use crate::models::calendar::Hour; +use std::vec; +use std::{ + fmt, + ops::{Add, Sub}, + rc::{Rc, Weak}, +}; + +#[derive(Clone)] +pub struct Activity { + pub goal_id: String, + pub activity_type: ActivityType, + pub title: String, + pub min_block_size: usize, + pub max_block_size: usize, + pub calendar_overlay: Vec>>, + pub time_budgets: Vec, + pub total_duration: usize, + pub duration_left: usize, + pub status: Status, +} +impl Activity { + pub fn get_compatible_hours_overlay( + calendar: &Calendar, + filter_option: Option, + adjusted_goal_start: NaiveDateTime, + adjusted_goal_deadline: NaiveDateTime, + ) -> Vec>> { + let mut compatible_hours_overlay: Vec>> = + Vec::with_capacity(calendar.hours.capacity()); + for hour_index in 0..calendar.hours.capacity() { + let mut compatible = true; + + if filter_option.is_some() { + if filter_option.clone().unwrap().after_time + < filter_option.clone().unwrap().before_time + { + //normal case + let hour_of_day = hour_index % 24; + if hour_of_day < filter_option.clone().unwrap().after_time { + compatible = false; + } + if hour_of_day >= filter_option.clone().unwrap().before_time { + compatible = false; + } + } else { + // special case where we know that compatible times cross the midnight boundary + let hour_of_day = hour_index % 24; + if hour_of_day >= filter_option.clone().unwrap().before_time + && hour_of_day < filter_option.clone().unwrap().after_time + { + compatible = false; + } + } + if filter_option + .as_ref() + .unwrap() + .on_days + .contains(&calendar.get_week_day_of(hour_index)) + { + // OK + } else { + compatible = false; + } + } + + if hour_index < calendar.get_index_of(adjusted_goal_start) { + compatible = false; + } + if hour_index >= calendar.get_index_of(adjusted_goal_deadline) { + compatible = false; + } + + //check if hour is already occupied by some other activity (for later rounds of scheduling partly occupied calendar) + match &*calendar.hours[hour_index] { + Hour::Free => {} + Hour::Occupied { + activity_index: _, + activity_title: _, + activity_goalid: _activity_goalid, + } => { + compatible = false; + } + } + + if compatible { + compatible_hours_overlay.push(Some(Rc::downgrade(&calendar.hours[hour_index]))); + } else { + compatible_hours_overlay.push(None); + } + } + compatible_hours_overlay + } + + pub fn flex(&self) -> usize { + let mut flex = 0; + let mut buffer = 0; + for hour_index in 0..self.calendar_overlay.len() { + match &self.calendar_overlay[hour_index] { + None => { + buffer = 0; + } + Some(hour_pointer) => { + //if free and buffer size > duration : add to flex and buffer + buffer += 1; + if hour_pointer.upgrade().is_none() { + buffer = 0; + } else if hour_pointer.upgrade().unwrap() == Hour::Free.into() + && self.min_block_size <= buffer + { + flex += 1; + } + } + } + } + flex + } + + pub fn get_best_scheduling_index_and_length(&self) -> Option<(usize, usize)> { + let mut best_scheduling_index_and_conflicts: Option<(usize, usize, usize)> = None; + for hour_index in 0..self.calendar_overlay.len() { + let mut conflicts = 0; + match &self.calendar_overlay[hour_index] { + None => { + continue; + } + Some(_) => { + //TODO: shouldn't this logic be in creating the activity and then set to min_block_size so we can just use that here? + let offset_size: usize = match self.activity_type { + ActivityType::SimpleGoal => self.total_duration, + ActivityType::Budget => self.min_block_size, + ActivityType::GetToMinWeekBudget => 1, + ActivityType::TopUpWeekBudget => 1, + }; + for offset in 0..offset_size { + match &self.calendar_overlay[hour_index + offset] { + None => { + // panic!("Does this ever happen?"); + // Yes in algorithm_challenge test case + // TODO: do we need to mark all from hour_index till offset as None?" + continue; + } + Some(weak) => { + if weak.upgrade().is_none() { + break; // this will reset conflicts too + } + conflicts += weak.weak_count(); + //if last position check if best so far - or so little we can break + if offset == offset_size - 1 { + match best_scheduling_index_and_conflicts { + None => { + best_scheduling_index_and_conflicts = + Some((hour_index, conflicts, offset_size)); + } + Some((_, best_conflicts, _)) => { + if conflicts < best_conflicts || conflicts == 0 { + best_scheduling_index_and_conflicts = + Some((hour_index, conflicts, offset_size)); + } + } + } + continue; + } + } + } + } + } + } + } + best_scheduling_index_and_conflicts.map(|(best_index, _, size)| (best_index, size)) + } + + pub(crate) fn release_claims(&mut self) { + let mut empty_overlay: Vec>> = + Vec::with_capacity(self.calendar_overlay.capacity()); + for _ in 0..self.calendar_overlay.capacity() { + empty_overlay.push(None); + } + self.calendar_overlay = empty_overlay; + } + + pub(crate) fn get_activities_from_budget_goal( + goal: &Goal, + calendar: &Calendar, + ) -> Vec { + if goal.children.is_some() || goal.filters.as_ref().is_none() { + return vec![]; + } + if goal.budget_config.as_ref().unwrap().min_per_day == 0 { + return vec![]; + } + let (adjusted_goal_start, adjusted_goal_deadline) = goal.get_adj_start_deadline(calendar); + let mut activities: Vec = Vec::with_capacity(1); + let filter_option = goal.filters.clone().unwrap(); + + //TODO: This is cutting something like Sleep into pieces + //Replace by an if on title == 'sleep' / "Sleep" / "Sleep πŸ˜΄πŸŒ™"? + //Yes ... but what about translations? => better to match on goalid + let mut adjusted_min_block_size = 1; + if goal.title.contains("leep") { + adjusted_min_block_size = goal.budget_config.as_ref().unwrap().min_per_day; + } + + for day in 0..(adjusted_goal_deadline - adjusted_goal_start).num_days() as u64 { + if filter_option + .on_days + .contains(&adjusted_goal_start.add(Days::new(day)).weekday()) + { + // OK + } else { + // This day is not allowed + continue; + } + let activity_start = adjusted_goal_start.add(Days::new(day)); + let activity_deadline = adjusted_goal_start.add(Days::new(day + 1)); + + let compatible_hours_overlay = Activity::get_compatible_hours_overlay( + calendar, + Some(filter_option.clone()), + activity_start, + activity_deadline, + ); + + let activity = Activity { + goal_id: goal.id.clone(), + activity_type: ActivityType::Budget, + title: goal.title.clone(), + min_block_size: adjusted_min_block_size, + max_block_size: goal.budget_config.as_ref().unwrap().max_per_day, + calendar_overlay: compatible_hours_overlay, + time_budgets: vec![], + total_duration: adjusted_min_block_size, + duration_left: goal.budget_config.as_ref().unwrap().min_per_day, + status: Status::Unprocessed, + }; + dbg!(&activity); + activities.push(activity); + } + activities + } + + pub(crate) fn get_activities_from_simple_goal( + goal: &Goal, + calendar: &Calendar, + ) -> Vec { + if goal.children.is_some() || goal.filters.as_ref().is_some() { + return vec![]; + } + let (adjusted_goal_start, adjusted_goal_deadline) = goal.get_adj_start_deadline(calendar); + let mut activities: Vec = Vec::with_capacity(1); + + let activity_total_duration = goal.min_duration.unwrap(); + let mut min_block_size = activity_total_duration; + if activity_total_duration > 8 { + min_block_size = 1; + //todo!() //split into multiple activities so flexibilities are correct?? + // or yield flex 1 or maximum of the set from activity.flex()? + }; + + let compatible_hours_overlay = Activity::get_compatible_hours_overlay( + calendar, + goal.filters.clone(), + adjusted_goal_start, + adjusted_goal_deadline, + ); + + let activity = Activity { + goal_id: goal.id.clone(), + activity_type: ActivityType::SimpleGoal, + title: goal.title.clone(), + min_block_size, + max_block_size: min_block_size, + calendar_overlay: compatible_hours_overlay, + time_budgets: vec![], + total_duration: activity_total_duration, + duration_left: min_block_size, //TODO: Correct this - is it even necessary to have duration_left? + status: Status::Unprocessed, + }; + dbg!(&activity); + activities.push(activity); + + activities + } + + pub fn update_overlay_with(&mut self, budgets: &Vec) { + if self.status == Status::Scheduled + || self.status == Status::Impossible + || self.status == Status::Processed + { + //return - no need to update overlay + return; + } + + //check if block is lost/stolen or not - as current weak pointer state could be disposed/stale/dead + for hour_index in 0..self.calendar_overlay.len() { + if self.calendar_overlay[hour_index].is_some() + && self.calendar_overlay[hour_index] + .as_ref() + .unwrap() + .upgrade() + .is_none() + { + //block was stolen/lost to some other activity + self.calendar_overlay[hour_index] = None; + } + } + + //Check if blocks are too small + let mut block_size_found: usize = 0; + for hour_index in 0..self.calendar_overlay.len() { + match &self.calendar_overlay[hour_index] { + None => { + if block_size_found < self.min_block_size { + // found block in calendar that is too small to fit min_block size + let mut start_index = hour_index; + if hour_index > block_size_found { + start_index -= block_size_found; + } + for index_to_set_to_none in start_index..hour_index { + self.calendar_overlay[index_to_set_to_none] = None; + } + } + block_size_found = 0; + continue; + } + Some(_) => { + block_size_found += 1; + } + } + } + // This is for if we reach the end of the overlay and a block is still building + if block_size_found < self.min_block_size { + // found block in calendar that is too small to fit min_block size + for index_to_set_to_none in + self.calendar_overlay.len() - block_size_found..self.calendar_overlay.len() + { + self.calendar_overlay[index_to_set_to_none] = None; + } + } + + //Check if hour is in at least one block that is allowed by all budgets + let mut is_part_of_at_least_one_valid_block_placing_option: Vec = + vec![false; self.calendar_overlay.len()]; + let mut is_activity_part_of_budget = false; + for budget in budgets { + //check if activity goal id is in the budget - else don't bother + if budget.participating_goals.contains(&self.goal_id) { + // great, process it + is_activity_part_of_budget = true; + } else { + // budget not relevant to this activity + continue; + } + + //set hour_option to true for any hour inside a block that satisfies all budgets + 'outer: for index in 0..is_part_of_at_least_one_valid_block_placing_option.len() { + //check if block under validation is large enough + for offset in 0..self.min_block_size { + if self.calendar_overlay[index + offset].is_none() { + continue 'outer; + } + } + if budget.is_within_budget(index, self.min_block_size, self.activity_type.clone()) { + for offset in 0..self.min_block_size { + is_part_of_at_least_one_valid_block_placing_option[index + offset] = true; + } + } + } + } + if is_activity_part_of_budget { + for (index, hour_option) in is_part_of_at_least_one_valid_block_placing_option + .iter_mut() + .enumerate() + { + if self.calendar_overlay[index].is_some() && !*hour_option { + self.calendar_overlay[index] = None; + } + } + } + + if self.flex() == 0 { + self.status = Status::Impossible; + } + } + + pub fn get_activities_to_get_min_week_budget( + goal_to_use: &Goal, + calendar: &Calendar, + time_budget: &TimeBudget, + ) -> Vec { + let mut activities: Vec = vec![]; + + let compatible_hours_overlay = Activity::get_compatible_hours_overlay( + calendar, + goal_to_use.filters.clone(), + calendar + .start_date_time + .sub(Duration::hours(24)) //TODO: fix magic number + .add(Duration::hours(time_budget.calendar_start_index as i64)), + calendar + .start_date_time + .sub(Duration::hours(24)) //TODO: fix magic number + .add(Duration::hours(time_budget.calendar_end_index as i64)), + ); + + let max_hours = time_budget.max_scheduled - time_budget.scheduled; + + activities.push(Activity { + goal_id: goal_to_use.id.clone(), + activity_type: ActivityType::GetToMinWeekBudget, + title: goal_to_use.title.clone(), + min_block_size: 1, + max_block_size: max_hours, + calendar_overlay: compatible_hours_overlay, + time_budgets: vec![], + total_duration: max_hours, + duration_left: max_hours, + status: Status::Unprocessed, + }); + + activities + } + + pub fn get_activities_to_top_up_week_budget( + goal_to_use: &Goal, + calendar: &Calendar, + time_budget: &TimeBudget, + ) -> Vec { + let mut activities: Vec = vec![]; + + let compatible_hours_overlay = Activity::get_compatible_hours_overlay( + calendar, + goal_to_use.filters.clone(), + calendar + .start_date_time + .sub(Duration::hours(24)) //TODO: fix magic number + .add(Duration::hours(time_budget.calendar_start_index as i64)), + calendar + .start_date_time + .sub(Duration::hours(24)) //TODO: fix magic number + .add(Duration::hours(time_budget.calendar_end_index as i64)), + ); + + let max_hours = time_budget.max_scheduled - time_budget.scheduled; + + activities.push(Activity { + goal_id: goal_to_use.id.clone(), + activity_type: ActivityType::TopUpWeekBudget, + title: goal_to_use.title.clone(), + min_block_size: 1, + max_block_size: max_hours, + calendar_overlay: compatible_hours_overlay, + time_budgets: vec![], + total_duration: max_hours, + duration_left: max_hours, + status: Status::Unprocessed, + }); + + activities + } +} + +#[derive(Debug, PartialEq, Clone, Deserialize)] +pub enum Status { + Unprocessed, + Processed, + Scheduled, + Impossible, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum ActivityType { + SimpleGoal, + Budget, + GetToMinWeekBudget, + TopUpWeekBudget, +} + +impl fmt::Debug for Activity { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f).unwrap(); + writeln!(f, "title: {:?}", self.title).unwrap(); + writeln!(f, "status:{:?}", self.status).unwrap(); + writeln!(f, "total duration: {:?}", self.total_duration).unwrap(); + writeln!(f, "duration left: {:?}", self.duration_left).unwrap(); + writeln!(f, "flex:{:?}", self.flex()).unwrap(); + for hour_index in 0..self.calendar_overlay.capacity() { + let day_index = hour_index / 24; + let hour_of_day = hour_index % 24; + match &self.calendar_overlay[hour_index] { + None => { + write!(f, "-").unwrap(); + } + Some(weak) => { + writeln!( + f, + "day {:?} - hour {:?} at index {:?}: {:?} claims but {:?}", + day_index, + hour_of_day, + hour_index, + weak.weak_count(), + weak.upgrade().unwrap() + ) + .unwrap(); + } + } + } + Ok(()) + } +} diff --git a/src/models/budget.rs b/src/models/budget.rs new file mode 100644 index 00000000..d2c403b0 --- /dev/null +++ b/src/models/budget.rs @@ -0,0 +1,162 @@ +use std::{ + fmt::{Debug, Formatter}, + ops::{Add, Sub}, +}; + +use chrono::{Datelike, Duration}; +use serde::Deserialize; + +use super::{activity::ActivityType, calendar::Calendar, goal::Goal}; + +#[derive(Debug, Clone, Deserialize)] +pub struct Budget { + pub originating_goal_id: String, + pub participating_goals: Vec, + pub time_budgets: Vec, +} +impl Budget { + pub fn reduce_for_(&mut self, goal: &str, duration_offset: usize) { + if self.participating_goals.contains(&goal.to_string()) { + let iterator = self.time_budgets.iter_mut().enumerate(); + for (_, time_budget) in iterator { + if duration_offset >= time_budget.calendar_start_index + && duration_offset < time_budget.calendar_end_index + { + time_budget.scheduled += 1 + } + } + } + } + + pub(crate) fn is_within_budget( + &self, + hour_index: usize, + offset: usize, + activity_type: ActivityType, + ) -> bool { + let mut budget_cut_off_number: usize; + let mut is_allowed = true; + for time_budget in &self.time_budgets { + match activity_type { + ActivityType::SimpleGoal => { + budget_cut_off_number = time_budget.min_scheduled; + } + ActivityType::Budget => { + budget_cut_off_number = time_budget.min_scheduled; + } + ActivityType::GetToMinWeekBudget => { + if time_budget.calendar_end_index - time_budget.calendar_start_index > 24 { + //Week time_budget + budget_cut_off_number = time_budget.min_scheduled; // this allows leaving room for other goals to get to min before topping up + } else { + //Day time_budget + budget_cut_off_number = time_budget.max_scheduled; + } + } + ActivityType::TopUpWeekBudget => { + budget_cut_off_number = time_budget.max_scheduled; + } + } + //figure out how many of the hours in hour_index till hour_index + offset are in the time_budget window + let mut hours_in_time_budget_window = 0; + for local_offset in 0..offset { + if (hour_index + local_offset) >= time_budget.calendar_start_index + && (hour_index + local_offset) < time_budget.calendar_end_index + { + hours_in_time_budget_window += 1; + } + } + if (hour_index + offset) >= time_budget.calendar_start_index + && (hour_index + offset) < time_budget.calendar_end_index + && time_budget.scheduled + hours_in_time_budget_window > budget_cut_off_number + { + is_allowed = false; + } + } + is_allowed + } +} + +#[derive(Deserialize, Debug, Clone, PartialEq)] +pub enum TimeBudgetType { + Day, + Week, +} + +#[derive(Clone, Deserialize)] +pub struct TimeBudget { + pub time_budget_type: TimeBudgetType, + pub calendar_start_index: usize, + pub calendar_end_index: usize, + pub scheduled: usize, + pub min_scheduled: usize, + pub max_scheduled: usize, +} + +impl Debug for TimeBudget { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + write!( + f, + "\n{:?} budget from index {:?}-{:?}: Scheduled {:?} / {:?}-{:?}\n", + &self.time_budget_type, + &self.calendar_start_index, + &self.calendar_end_index, + &self.scheduled, + &self.min_scheduled, + &self.max_scheduled + ) + .unwrap(); + Ok(()) + } +} + +pub fn get_time_budgets_from(calendar: &Calendar, goal: &Goal) -> Vec { + let mut time_budgets: Vec = vec![]; + //get a time_budget for each day + for hour_index in 24..calendar.hours.capacity() - 24 { + if (hour_index) % 24 == 0 { + println!("Day boundary detected at hour_index {:?}", &hour_index); + let mut min = goal.budget_config.as_ref().unwrap().min_per_day; + let mut max = goal.budget_config.as_ref().unwrap().max_per_day; + if goal.filters.as_ref().unwrap().on_days.contains( + &calendar + .start_date_time + .sub(Duration::hours(24)) + .add(Duration::hours(hour_index as i64)) + .weekday(), + ) { + //OK + } else { + min = 0; + max = 0; + } + time_budgets.push(TimeBudget { + time_budget_type: TimeBudgetType::Day, + calendar_start_index: hour_index, + calendar_end_index: hour_index + 24, + scheduled: 0, + min_scheduled: min, + max_scheduled: max, + }); + } + } + + let mut start_pointer = 24; + //get a time_budget for each week + for hour_index in 24..calendar.hours.capacity() { + if (hour_index - 24) % (24 * 7) == 0 && hour_index > 24 { + println!("Week boundary detected at hour_index {:?}", &hour_index); + time_budgets.push(TimeBudget { + time_budget_type: TimeBudgetType::Week, + calendar_start_index: start_pointer, + calendar_end_index: hour_index, + scheduled: 0, + min_scheduled: goal.budget_config.as_ref().unwrap().min_per_week, + max_scheduled: goal.budget_config.as_ref().unwrap().max_per_week, + }); + start_pointer = hour_index + } + } + dbg!(&time_budgets); + time_budgets +} diff --git a/src/models/calendar.rs b/src/models/calendar.rs index 0c5d4c2c..b2619531 100644 --- a/src/models/calendar.rs +++ b/src/models/calendar.rs @@ -1,252 +1,372 @@ -use crate::legacy::input::Input; -use crate::legacy::output::{DayTasks, FinalTasks, Task}; -use crate::models::date::{DateTime, DateTimeRange}; -use crate::models::day::Day; -use crate::models::flexibility::Flexibility; -use crate::models::goal::Goal; -use std::cell::RefCell; +use super::budget::{get_time_budgets_from, Budget, TimeBudgetType}; +use super::goal::Goal; +use super::task::{DayTasks, FinalTasks, Task}; +use chrono::{Datelike, Days, Duration, NaiveDateTime, Weekday}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::fmt::{Debug, Formatter}; +use std::ops::{Add, Deref, Sub}; use std::rc::Rc; -pub type Goals = Vec>; -pub type Span = usize; -pub type Position = usize; -pub type FlexValue = usize; -pub type Unprocessed = RefCell>; -pub type Scheduled = RefCell)>>; +#[derive(Debug, PartialEq, Clone)] +pub enum Hour { + Free, + Occupied { + activity_index: usize, + activity_title: String, + activity_goalid: String, + }, //TODO: add goal id and budget id to occupied registration so budget object is not necessary anymore! +} -pub type Data = RefCell>; +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ImpossibleActivity { + pub id: String, + pub hours_missing: usize, + pub period_start_date_time: NaiveDateTime, + pub period_end_date_time: NaiveDateTime, +} pub struct Calendar { - day: DateTime, - - flexibilities: Data, - - unprocessed: Unprocessed, - - scheduled: Scheduled, - impossible: Scheduled, + pub start_date_time: NaiveDateTime, + pub end_date_time: NaiveDateTime, + pub hours: Vec>, + pub impossible_activities: Vec, + pub budgets: Vec, } impl Calendar { - pub fn new(input: &Input, goals: &Goals) -> Self { - let date_start = DateTime::from_naive_date_time(&input.calendar_start); - let date_end = DateTime::from_naive_date_time(&input.calendar_end); - let day = date_start.start_of_day(); - - let mut flexibilities = goals - .iter() - .map(|goal| get_flexibilities(goal.clone(), &date_start, &date_end)) - .collect::>(); - flexibilities.sort_by(|a, b| a.goal.id().cmp(&b.goal.id())); - let flexibilities = RefCell::new(flexibilities); - - let unprocessed: Unprocessed = RefCell::new((0..flexibilities.borrow().len()).collect()); - - let scheduled = RefCell::new(vec![]); - let impossible = RefCell::new(vec![]); - + pub fn new(start_date_time: NaiveDateTime, end_date_time: NaiveDateTime) -> Self { + let number_of_days = (end_date_time - start_date_time).num_days(); //Todo use this later to stop limiting compatible + println!( + "Calendar of {:?} days, from {:?} to {:?}", + &number_of_days, &start_date_time, &end_date_time + ); + let mut hours = Vec::with_capacity(48 + number_of_days as usize * 24); + for _ in 0..hours.capacity() { + hours.push(Rc::new(Hour::Free)); + } Self { - day, - - flexibilities, - - unprocessed, - - scheduled, - impossible, + start_date_time, + end_date_time, + hours, + impossible_activities: vec![], + budgets: vec![], } } - pub fn has_finished_scheduling(&self) -> bool { - self.unprocessed.borrow().is_empty() + pub fn get_week_day_of(&self, index_to_test: usize) -> Weekday { + if index_to_test > self.hours.capacity() - 1 { + panic!( + "Can't request weekday for index {:?} outside of calendar capacity {:?}\nIndexes start at 0.\n", + index_to_test, + self.hours.capacity() + ); + } + let date_time_of_index_to_test = self + .start_date_time + .sub(Days::new(1)) + .add(Duration::hours(index_to_test as i64)); + date_time_of_index_to_test.weekday() } - pub fn flexibility_at(&self, pos: Position) -> Option { - self.flexibilities.borrow().get(pos).cloned() - } - pub fn flexibility(&self, pos: Position) -> Option<(Position, FlexValue, Flexibility)> { - self.flexibility_at(pos) - .map(|f| (pos, f.day.flexibility(f.goal.min_span()), f)) - } - pub fn unprocessed(&self) -> Vec { - self.unprocessed.borrow().clone() - } - pub fn push_impossible(&self, position: Position, range: DateTimeRange) { - self.flexibility_at(position).unwrap().day.occupy(&range); - self.occupy_unprocessed(&range); - self.impossible.borrow_mut().push(( - position, - range, - self.flexibility_at(position).unwrap().goal.clone(), - )); - } - pub fn push_scheduled(&self, position: Position, range: DateTimeRange) { - self.flexibility_at(position).unwrap().day.occupy(&range); - self.occupy_unprocessed(&range); - self.scheduled.borrow_mut().push(( - position, - range, - self.flexibility_at(position).unwrap().goal.clone(), - )); - } - pub fn occupy_unprocessed(&self, range: &DateTimeRange) { - self.unprocessed - .borrow() - .iter() - .for_each(|pos| self.flexibility_at(*pos).unwrap().day.occupy(range)); - } - pub fn take(&self, position: Position) -> Option<(Flexibility, Vec)> { - let (selected, remaining): (Vec<_>, Vec<_>) = self - .unprocessed - .borrow() - .iter() - .partition(|&p| *p == position); - *self.unprocessed.borrow_mut() = remaining; - assert_eq!(selected.len(), 1); - selected - .first() - .map(|position| self.flexibility_at(*position)) - .unwrap_or(None) - .map(|f| (f, self.unprocessed())) + pub fn get_index_of(&self, date_time: NaiveDateTime) -> usize { + if date_time < self.start_date_time.sub(Duration::days(1)) + || date_time > self.end_date_time.add(Duration::days(1)) + { + // TODO: Fix magic number offset everywhere in code + panic!( + "can't request an index more than 1 day outside of calendar bounds for date {:?}\nCalendar starts at {:?} and ends at {:?}", date_time, self.start_date_time, self.end_date_time + ) + } + (date_time - self.start_date_time.checked_sub_days(Days::new(1)).unwrap()).num_hours() + as usize } - pub fn result(&self) -> FinalTasks { - let mut tasks = vec![]; - self.gather_tasks_with_filler(&mut tasks, &self.scheduled, false); - let mut impossible_tasks = vec![]; - self.gather_tasks(&mut impossible_tasks, &self.impossible, true); + pub fn print(&self) -> FinalTasks { + //TODO Fix this mess below - it works somehow but not readable at all... + let mut scheduled: Vec = vec![]; + let mut day_tasks = DayTasks { + day: self.start_date_time.date(), + tasks: Vec::with_capacity(1), + }; + let mut task_counter = 0; + let mut current_task = Task { + taskid: task_counter, + goalid: "free".to_string(), + title: "free".to_string(), + duration: 0, + start: self.start_date_time, + deadline: self.start_date_time, //just for init; will be overwritten + }; + for hour_offset in 24..(self.hours.capacity() - 24) { + if hour_offset % 24 == 0 && hour_offset != 24 { + // day boundary reached + println!("found day boundary at offset :{:?}", hour_offset); + // - push current to dayTasks and increase counter + current_task.deadline = current_task + .start + .add(Duration::hours(current_task.duration as i64)); + if current_task.duration > 0 { + day_tasks.tasks.push(current_task.clone()); + } + task_counter += 1; + current_task.taskid = task_counter; + // - push dayTasks copy to scheduled + scheduled.push(day_tasks); + // - update dayTasks for current day and reset Tasks vec + day_tasks = DayTasks { + day: self + .start_date_time + .date() + .add(Duration::days(hour_offset as i64 / 24 - 1)), + tasks: Vec::with_capacity(1), + }; + // - reset current_task and empty title to force new Task in loop + current_task.title = "".to_string(); + current_task.duration = 0; + } + match self.hours[hour_offset].clone().deref() { + Hour::Free => { + if current_task.title.eq(&"free".to_string()) { + current_task.duration += 1; + } else { + current_task.deadline = current_task + .start + .add(Duration::hours(current_task.duration as i64)); + if current_task.duration > 0 { + day_tasks.tasks.push(current_task.clone()); + task_counter += 1; + } + current_task.title = "free".to_string(); + current_task.goalid = "free".to_string(); + current_task.duration = 1; + current_task.start = self + .start_date_time + .add(Duration::hours(hour_offset as i64 - 24)); // TODO: Fix magic number offset everywhere in code + current_task.taskid = task_counter; + } + } + Hour::Occupied { + activity_index: _, + activity_title, + activity_goalid, + } => { + if current_task.title.eq(&"free".to_string()) + || current_task.title.ne(activity_title) + { + if current_task.duration > 0 { + current_task.deadline = current_task + .start + .add(Duration::hours(current_task.duration as i64)); + // TODO is this necessary? + day_tasks.tasks.push(current_task.clone()); + task_counter += 1; + } + current_task.duration = 1; + current_task.goalid = activity_goalid.clone(); + current_task.title = activity_title.clone(); + current_task.start = self + .start_date_time + .add(Duration::hours(hour_offset as i64 - 24)); // TODO: Fix magic number offset everywhere in code + current_task.taskid = task_counter; + } else { + current_task.duration += 1; + } + } + } + } + current_task.deadline = current_task + .start + .add(Duration::hours(current_task.duration as i64)); + if current_task.duration > 0 { + // TODO is this necessary? + day_tasks.tasks.push(current_task); + } + scheduled.push(day_tasks); FinalTasks { - scheduled: vec![DayTasks { - day: self.day.naive_date(), - tasks, - }], - impossible: vec![DayTasks { - day: self.day.naive_date(), - tasks: impossible_tasks, - }], + scheduled, + impossible: self.impossible_activities.clone(), } } - fn gather_tasks(&self, tasks: &mut Vec, slots: &Scheduled, impossible: bool) { - let mut slots = slots.borrow().to_vec(); - slots.sort_by(|a, b| a.1.cmp(&b.1)); - slots - .iter() - .enumerate() - .for_each(|(idx, (position, range, _goal))| { - let start = range.start().naive_date_time(); - let deadline = range.end().naive_date_time(); + pub fn add_budgets_from(&mut self, goals: &Vec) { + //fill goal_map and budget_ids + let mut goal_map: HashMap = HashMap::new(); + let mut budget_ids: Vec = vec![]; + for goal in goals { + goal_map.insert(goal.id.clone(), goal.clone()); + match goal.budget_config.as_ref() { + Some(budget_config) => { + //Check if budget_config is realistic - if let Some(f) = self.flexibility_at(*position) { - tasks.push(Task { - taskid: idx, - goalid: f.goal.id(), - title: f.goal.title(), - duration: f.goal.min_span(), - start, - deadline, - tags: vec![], - impossible, - }) + //check 1 + let mut min_per_day_sum = 0; + for _ in goal.filters.clone().unwrap().on_days { + min_per_day_sum += budget_config.min_per_day; + } + if min_per_day_sum > budget_config.min_per_week { + panic!("Sum of min_per_day {:?} is higher than min_per_week {:?} for goal {:?}", min_per_day_sum,budget_config.min_per_week, goal.title); + } + + //check 2 + if budget_config.max_per_day > budget_config.max_per_week { + panic!( + "max_per_day {:?} is higher than max_per_week {:?} for goal {:?}", + budget_config.max_per_day, budget_config.max_per_week, goal.title + ); + } + budget_ids.push(goal.id.clone()); } - }) - } - fn gather_tasks_with_filler(&self, tasks: &mut Vec, slots: &Scheduled, impossible: bool) { - let mut current = self.day.start_of_day(); - let mut filler_offset = 0; - let mut slots = slots.borrow().to_vec(); - slots.sort_by(|a, b| a.1.cmp(&b.1)); - slots - .iter() - .enumerate() - .for_each(|(idx, (position, range, _goal))| { - if current.lt(range.start()) { - let span = current.span_by(range.start()); - tasks.push(Task { - taskid: idx + filler_offset, - goalid: "free".to_string(), - title: "free".to_string(), - duration: span, - start: current.naive_date_time(), - deadline: current.inc_by(span).naive_date_time(), - tags: vec![], - impossible: false, + None => continue, + } + } + + for budget_id in budget_ids { + //TODO: extract in function get_all_descendants + //get all descendants + let mut descendants_added: Vec = vec![budget_id.clone()]; + //get the first children if any + let mut descendants: Vec = vec![]; + match goal_map.get(&budget_id).as_ref().unwrap().children.as_ref() { + Some(children) => { + descendants.append(children.clone().as_mut()); + } + None => { + self.budgets.push(Budget { + originating_goal_id: budget_id.clone(), + participating_goals: descendants_added, + time_budgets: get_time_budgets_from( + self, + goal_map.get(&budget_id).as_ref().unwrap(), + ), }); - filler_offset += 1; + continue; } - current = range.end().clone(); + } - let start = range.start().naive_date_time(); - let deadline = range.end().naive_date_time(); + loop { + //add children of each descendant until no more found + if descendants.is_empty() { + self.budgets.push(Budget { + originating_goal_id: budget_id.clone(), + participating_goals: descendants_added, + time_budgets: get_time_budgets_from( + self, + goal_map.get(&budget_id).as_ref().unwrap(), + ), + }); + break; + } + let descendant_of_which_to_add_children = descendants.pop().unwrap(); + descendants.extend( + goal_map + .get(&descendant_of_which_to_add_children) + .unwrap() + .children + .as_ref() + .unwrap() + .clone(), + ); + descendants_added.push(descendant_of_which_to_add_children); + } + } + } + + pub fn update_budgets_for(&mut self, goal: &str, duration_offset: usize) { + let iterator = self.budgets.iter_mut(); + for budget in iterator { + budget.reduce_for_(goal, duration_offset); + } + } - if let Some(f) = self.flexibility_at(*position) { - tasks.push(Task { - taskid: idx + filler_offset, - goalid: f.goal.id(), - title: f.goal.title(), - duration: f.goal.min_span(), - start, - deadline, - tags: vec![], - impossible, - }) + pub fn log_impossible_min_day_budgets(&mut self) { + let mut impossible_activities = vec![]; + for budget in &self.budgets { + for time_budget in &budget.time_budgets { + if time_budget.time_budget_type == TimeBudgetType::Day { + // Good + } else { + continue; } - }); - if current.lt(&self.day.end_of_day()) { - let span = current.span_by(&self.day.end_of_day()); - tasks.push(Task { - taskid: slots.len() + filler_offset, - goalid: "free".to_string(), - title: "free".to_string(), - duration: span, - start: current.naive_date_time(), - deadline: current.inc_by(span).naive_date_time(), - tags: vec![], - impossible: false, - }); + if time_budget.scheduled < time_budget.min_scheduled { + impossible_activities.push(ImpossibleActivity { + id: budget.originating_goal_id.clone(), + hours_missing: time_budget.min_scheduled - time_budget.scheduled, + period_start_date_time: self + .start_date_time + .add(Duration::hours(time_budget.calendar_start_index as i64)), + period_end_date_time: self + .start_date_time + .add(Duration::hours(time_budget.calendar_end_index as i64)), + }); + } + } } + self.impossible_activities.extend(impossible_activities); } -} -fn get_flexibilities(goal: Rc, start: &DateTime, end: &DateTime) -> Flexibility { - let goals = vec![goal]; - goals - .into_iter() - .map(|g| { - ( - g.clone(), - Flexibility { - goal: g, - day: Rc::new(Day::new(start.clone())), - }, - ) - }) - .map(|(g, f)| { - let day = f.day; - day.occupy_inverse_range(&DateTimeRange::new(start.clone(), end.clone())); - (g, Flexibility { goal: f.goal, day }) - }) - .map(|(g, f)| { - let day = f.day; - day.occupy_inverse_range(&g.day_filter(start)); - Flexibility { goal: f.goal, day } - }) - .collect::>() - .pop() - .unwrap() + pub fn log_impossible_min_week_budgets(&mut self) { + //TODO: merge with log_imossible_min_day_budgets, passing budget type as param + let mut impossible_activities = vec![]; + for budget in &self.budgets { + for time_budget in &budget.time_budgets { + if time_budget.time_budget_type == TimeBudgetType::Week { + // Good + } else { + continue; + } + if time_budget.scheduled < time_budget.min_scheduled { + impossible_activities.push(ImpossibleActivity { + id: budget.originating_goal_id.clone(), + hours_missing: time_budget.min_scheduled - time_budget.scheduled, + period_start_date_time: self + .start_date_time + .add(Duration::hours(time_budget.calendar_start_index as i64)), + period_end_date_time: self + .start_date_time + .add(Duration::hours(time_budget.calendar_end_index as i64)), + }); + } + } + } + self.impossible_activities.extend(impossible_activities); + } } - impl Debug for Calendar { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str( - &self - .flexibilities - .borrow() - .iter() - .map(|f| f.day.to_string()) - .collect::>() - .join("\n"), + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + writeln!(f).unwrap(); + for index in 0..self.hours.capacity() { + write!(f, "{:?} ", self.get_week_day_of(index)).unwrap(); + let mut index_string = index.to_string(); + if index > 23 { + index_string = index.to_string() + " " + &(index % 24).to_string(); + } + if self.hours[index] == Rc::new(Hour::Free) { + if Rc::weak_count(&self.hours[index]) == 0 { + writeln!(f, "{} -", index_string).unwrap(); + } else { + writeln!( + f, + "{} {:?} claims", + index_string, + Rc::weak_count(&self.hours[index]) + ) + .unwrap(); + } + } else { + writeln!(f, "{} {:?}", index_string, self.hours[index]).unwrap(); + } + } + writeln!( + f, + "{:?} impossible activities", + self.impossible_activities.len() ) + .unwrap(); + for budget in &self.budgets { + writeln!(f, "{:?}", &budget).unwrap(); + } + Ok(()) } } diff --git a/src/models/date.rs b/src/models/date.rs deleted file mode 100644 index 1e4223e0..00000000 --- a/src/models/date.rs +++ /dev/null @@ -1,348 +0,0 @@ -use crate::models::calendar::Span; -use crate::models::date::DateTimeRangeContainerResult::{ - FitAtEnd, FitAtStart, FitInBetween, NoFit, PerfectFit, -}; -use chrono::{Duration, NaiveDate, NaiveDateTime, NaiveTime}; -use lazy_static::lazy_static; -use std::cmp::Ordering; -use std::fmt::{Display, Formatter}; -use std::ops::Add; -use std::str::FromStr; - -lazy_static! { - static ref SLOT_DURATION: Duration = Duration::hours(1); -} - -#[derive(Debug, Clone)] -pub struct DateTime { - naive: NaiveDateTime, -} -impl DateTime { - pub fn from_naive_date_time(date_time: &NaiveDateTime) -> Self { - Self { - naive: normalize_date(date_time), - } - } - pub fn from_naive_date(date: &NaiveDate) -> Self { - Self { - naive: NaiveDateTime::new(*date, NaiveTime::default()), - } - } - pub fn from_naive_time(time: &NaiveTime) -> Self { - Self { - naive: normalize_date(&NaiveDateTime::new(NaiveDate::default(), *time)), - } - } - #[allow(dead_code)] - fn from_str(date_str: &str) -> Option { - NaiveDateTime::from_str(date_str) - .map(|ndt| Self::from_naive_date_time(&ndt)) - .ok() - } - pub fn make_range(&self, span: usize) -> DateTimeRange { - let mut current = self.naive; - for _ in 0..span { - current += *SLOT_DURATION; - } - DateTimeRange::new(self.clone(), Self::from_naive_date_time(¤t)) - } - pub fn inc(&self) -> Self { - self.inc_by(1) - } - pub fn inc_by(&self, span: usize) -> Self { - let mut out = self.clone(); - for _ in 0..span { - out.naive += *SLOT_DURATION; - } - out - } - pub fn dec(&self) -> Self { - self.dec_by(1) - } - pub fn dec_by(&self, span: usize) -> Self { - let mut out = self.clone(); - for _ in 0..span { - out.naive -= *SLOT_DURATION; - } - out - } - pub fn start_of_day(&self) -> DateTime { - DateTime::from_naive_date_time(&NaiveDateTime::new(self.naive.date(), NaiveTime::default())) - } - pub fn end_of_day(&self) -> DateTime { - let beginning = self.start_of_day(); - DateTime::from_naive_date_time(&beginning.naive.add(Duration::days(1))) - } - pub fn span_of_day(&self) -> Span { - let end_of_day = self.end_of_day(); - let mut current = self.start_of_day(); - let mut count = 0; - while current.lt(&end_of_day) { - count += 1; - current = current.inc(); - } - count - } - pub fn span_by(&self, other: &DateTime) -> Span { - let mut current = self.clone(); - let mut count: Span = 0; - while current.le(other) { - current = current.inc(); - count += 1; - } - count = count.saturating_sub(1); - count - } - pub fn with_new_time(&self, time: &DateTime) -> DateTime { - DateTime::from_naive_date_time(&NaiveDateTime::new(self.naive.date(), time.naive.time())) - } - pub fn time_after(&self, other: &Option) -> DateTime { - other - .as_ref() - .map(|time| self.with_new_time(time)) - .unwrap_or(self.start_of_day()) - } - pub fn time_before(&self, other: &Option) -> DateTime { - other - .as_ref() - .map(|time| self.with_new_time(time)) - .unwrap_or(self.end_of_day()) - } - pub fn naive_date(&self) -> NaiveDate { - self.naive.date() - } - pub fn naive_date_time(&self) -> NaiveDateTime { - self.naive - } -} -impl PartialEq for DateTime { - fn eq(&self, other: &Self) -> bool { - self.naive.eq(&other.naive) - } -} -impl PartialOrd for DateTime { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} -impl Eq for DateTime {} -impl Ord for DateTime { - fn cmp(&self, other: &Self) -> Ordering { - self.naive.cmp(&other.naive) - } -} -impl Display for DateTime { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.naive.fmt(f) - } -} -impl From<&DateTime> for NaiveDateTime { - fn from(date: &DateTime) -> Self { - date.naive - } -} - -#[derive(Debug, Clone, PartialEq)] -pub enum DateTimeRangeContainerResult { - NoFit, - PerfectFit, - FitAtStart(DateTimeRange, DateTimeRange), - FitAtEnd(DateTimeRange, DateTimeRange), - FitInBetween(DateTimeRange, DateTimeRange, DateTimeRange), -} - -#[derive(Debug, Clone)] -pub struct DateTimeRange { - start: DateTime, - end: DateTime, -} -impl DateTimeRange { - pub fn new(start: DateTime, end: DateTime) -> Self { - if start < end { - Self { start, end } - } else { - Self { end, start } - } - } - - pub fn start(&self) -> &DateTime { - &self.start - } - pub fn end(&self) -> &DateTime { - &self.end - } - - /// The span in slot durations in between the range - pub fn span(&self) -> usize { - let mut out = 0; - - let mut current = self.start.clone(); - while current <= self.end { - out += 1; - current.naive += *SLOT_DURATION; - } - - out - } - - pub fn contains_date_time(&self, date: &DateTime) -> bool { - self.start.naive.le(&date.naive) && date.naive.lt(&self.end.naive) - } - - pub fn contains(&self, range: &DateTimeRange) -> bool { - self.start <= range.start && range.end <= self.end - } - - pub fn shift(&self, span: i16) -> DateTimeRange { - if span.is_positive() { - Self { - start: self.start.inc_by(span as usize), - end: self.end.inc_by(span as usize), - } - } else { - Self { - start: self.start.dec_by(span as usize), - end: self.end.dec_by(span.unsigned_abs() as usize), - } - } - } - - pub fn is_fitting(&self, range: &DateTimeRange) -> DateTimeRangeContainerResult { - if self.contains(range) { - match (&range.start, &range.end) { - (start, end) if start == &self.start && end == &self.end => PerfectFit, - (start, _) if start == &self.start => FitAtStart( - DateTimeRange::new(range.start.clone(), range.end.clone()), - DateTimeRange::new(range.end.clone(), self.end.clone()), - ), - (_, end) if end == &self.end => FitAtEnd( - DateTimeRange::new(self.start.clone(), range.start.clone()), - DateTimeRange::new(range.start.clone(), range.end.clone()), - ), - _ => FitInBetween( - DateTimeRange::new(self.start.clone(), range.start.clone()), - DateTimeRange::new(range.start.clone(), range.end.clone()), - DateTimeRange::new(range.end.clone(), self.end.clone()), - ), - } - } else { - NoFit - } - } -} -impl PartialEq for DateTimeRange { - fn eq(&self, other: &Self) -> bool { - self.start().eq(other.start()) && self.end().eq(other.end()) - } -} -impl PartialOrd for DateTimeRange { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.start().cmp(other.start())) - } -} -impl Eq for DateTimeRange {} -impl Ord for DateTimeRange { - fn cmp(&self, other: &Self) -> Ordering { - self.start().cmp(other.start()) - } -} - -fn normalize_date(date_time: &NaiveDateTime) -> NaiveDateTime { - let mut out = NaiveDateTime::new(date_time.date(), NaiveTime::default()); - - loop { - out += *SLOT_DURATION; - if &out > date_time { - out -= *SLOT_DURATION; - break; - } - } - - out -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_date_time_range() { - let (range, date_start, date_end) = create_range("2022-01-01T00:00:00", 3); - assert_eq!(range.start(), &date_start); - assert_eq!(range.end(), &date_end); - - let range = range.shift(2); - assert_eq!(range.start(), &date_start.inc_by(2)); - assert_eq!(range.end(), &date_end.inc_by(2)); - - let date_between = DateTime::from_str("2022-01-01T04:00:00").unwrap(); - assert!(range.contains_date_time(&date_between)); - let date_between = DateTime::from_str("2022-01-01T03:00:00").unwrap(); - assert!(range.contains_date_time(&date_between)); - let date_between = DateTime::from_str("2022-01-01T01:00:00").unwrap(); - assert!(!range.contains_date_time(&date_between)); - let date_between = DateTime::from_str("2022-01-01T05:00:00").unwrap(); - assert!(!range.contains_date_time(&date_between)); - } - - #[test] - fn test_date_time_range_contains() { - let date_start = DateTime::from_str("2022-01-01T03:00:00").unwrap(); - let range = date_start.make_range(3); - - let range1 = DateTime::from_str("2022-01-01T04:00:00") - .unwrap() - .make_range(1); - assert!(range.contains(&range1)); - - let range1 = DateTime::from_str("2022-01-01T02:00:00") - .unwrap() - .make_range(6); - assert!(!range.contains(&range1)); - - let range1 = DateTime::from_str("2022-01-01T02:00:00") - .unwrap() - .make_range(4); - assert!(!range.contains(&range1)); - - let range1 = DateTime::from_str("2022-01-01T03:00:00") - .unwrap() - .make_range(4); - assert!(!range.contains(&range1)); - } - - #[test] - fn test_is_fitting() { - let (ref_range, _, _) = create_range("2022-01-01T00:00:00", 3); - - let (range, _, _) = create_range("2022-01-01T00:00:00", 3); - assert_eq!(ref_range.is_fitting(&range), PerfectFit); - - let (range, _, _) = create_range("2022-01-01T00:00:00", 2); - let (start, _, _) = create_range("2022-01-01T00:00:00", 2); - let (end, _, _) = create_range("2022-01-01T02:00:00", 1); - assert_eq!(ref_range.is_fitting(&range), FitAtStart(start, end)); - - let (range, _, _) = create_range("2022-01-01T01:00:00", 2); - let (start, _, _) = create_range("2022-01-01T00:00:00", 1); - let (end, _, _) = create_range("2022-01-01T01:00:00", 2); - assert_eq!(ref_range.is_fitting(&range), FitAtEnd(start, end)); - - let (range, _, _) = create_range("2022-01-01T01:00:00", 1); - let (start, _, _) = create_range("2022-01-01T00:00:00", 1); - let (mid, _, _) = create_range("2022-01-01T01:00:00", 1); - let (end, _, _) = create_range("2022-01-01T02:00:00", 1); - assert_eq!(ref_range.is_fitting(&range), FitInBetween(start, mid, end)); - - let (range, _, _) = create_range("2022-01-01T00:00:00", 5); - assert_eq!(ref_range.is_fitting(&range), NoFit); - } - - fn create_range(date: &str, span: usize) -> (DateTimeRange, DateTime, DateTime) { - let date_start = DateTime::from_str(date).unwrap(); - let date_end = date_start.inc_by(span); - assert!(date_start < date_end); - assert!(!(date_start >= date_end)); - (date_start.make_range(span), date_start, date_end) - } -} diff --git a/src/models/day.rs b/src/models/day.rs deleted file mode 100644 index 9c26b987..00000000 --- a/src/models/day.rs +++ /dev/null @@ -1,122 +0,0 @@ -use crate::models::calendar::{FlexValue, Span}; -use crate::models::date::{DateTime, DateTimeRange}; -use std::cell::RefCell; -use std::fmt::{Display, Formatter}; - -#[derive(Debug, PartialEq)] -enum Seat { - Occupied, - Free, -} - -#[derive(Debug, PartialEq)] -pub struct Day { - range: DateTimeRange, - seats: Vec>, -} -impl Day { - pub fn new(date: DateTime) -> Self { - let start = date.start_of_day(); - let end = date.end_of_day(); - let mut current = start.clone(); - - let mut seats = vec![]; - while current.lt(&end) { - seats.push(RefCell::new(Seat::Free)); - current = current.inc(); - } - - let range = DateTimeRange::new(start, end); - Self { range, seats } - } - pub fn occupy(&self, range: &DateTimeRange) { - if !self.range.contains(range) { - return; - } - - for idx in 0..self.seats.len() { - let date = self.range.start().start_of_day().inc_by(idx); - if range.contains_date_time(&date) { - *self.seats[idx].borrow_mut() = Seat::Occupied; - } - } - } - pub fn occupy_inverse_range(&self, range: &DateTimeRange) { - self.occupy(&DateTimeRange::new( - self.range.start().start_of_day(), - range.start().clone(), - )); - self.occupy(&DateTimeRange::new( - range.end().clone(), - self.range.end().start_of_day(), - )); - } - pub fn flexibility(&self, span: Span) -> FlexValue { - self.slots(span).len() - } - pub fn slots(&self, span: usize) -> Vec { - if self.seats.is_empty() { - return vec![]; - } - let mut out = vec![]; - let start_of_day = self.range.start(); - for idx in 0..self.seats.len() - span { - if self.all_free(idx, span) { - let date = start_of_day.inc_by(idx); - out.push(DateTimeRange::new(date.clone(), date.inc_by(span).clone())); - } - } - out - } - pub fn overlap(&self, range: &Vec) -> Vec<(usize, DateTimeRange)> { - let start_of_day = self.range.start(); - let mut out = vec![]; - for r in range { - let mut count = 0; - for (idx, seat) in self.seats.iter().enumerate() { - if r.contains_date_time(&start_of_day.inc_by(idx)) && *seat.borrow() == Seat::Free { - count += 1; - } - } - out.push(count); - } - out.iter() - .zip(range) - .map(|(idx, range)| (*idx, range.clone())) - .collect() - } - pub fn first_fit(&self, span: usize) -> DateTimeRange { - self.slots(span)[0].clone() - } - fn all_free(&self, idx: usize, span: usize) -> bool { - self.seats[idx..idx + span] - .iter() - .all(|seat| match *seat.borrow() { - Seat::Occupied => false, - Seat::Free => true, - }) - } - pub fn differences(&self, other: &Day) -> usize { - self.seats - .iter() - .zip(&other.seats) - .map(|(a, b)| if a == b { 0 } else { 1 }) - .sum() - } -} - -impl Display for Day { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str( - &self - .seats - .iter() - .map(|s| match *s.borrow() { - Seat::Free => ".", - Seat::Occupied => "#", - }) - .collect::>() - .join(""), - ) - } -} diff --git a/src/models/day_filter.rs b/src/models/day_filter.rs deleted file mode 100644 index 90749fdd..00000000 --- a/src/models/day_filter.rs +++ /dev/null @@ -1,68 +0,0 @@ -use crate::models::date::DateTime; -use chrono::NaiveTime; -use std::cmp::Ordering; - -#[derive(Debug, Clone)] -pub struct DayFilter { - apply_after: Option, - apply_before: Option, -} - -impl DayFilter { - /// Input string must be of format "02:34" - pub fn from_str(after_str: Option<&str>, before_str: Option<&str>) -> Self { - Self { - apply_after: Self::get_time(after_str), - apply_before: Self::get_time(before_str), - } - } - - pub fn after(&self, date: &DateTime) -> DateTime { - date.time_after(&self.apply_after) - } - pub fn before(&self, date: &DateTime) -> DateTime { - date.time_before(&self.apply_before) - } - - fn get_time(time_str: Option<&str>) -> Option { - time_str - .map(|time_str| { - if time_str.len() != 5 && &time_str[2..3] != ":" { - None - } else { - match ( - time_str[..2].parse::().ok(), - time_str[3..].parse::().ok(), - ) { - (Some(hour), Some(minute)) => NaiveTime::from_hms_opt(hour, minute, 0), - _ => None, - } - } - }) - .unwrap() - .or(None) - .map(|ref nt| DateTime::from_naive_time(nt)) - } -} - -impl Eq for DayFilter {} -impl PartialEq for DayFilter { - fn eq(&self, other: &Self) -> bool { - self.apply_after.eq(&other.apply_after) && self.apply_before.eq(&other.apply_before) - } -} - -impl Ord for DayFilter { - fn cmp(&self, other: &Self) -> Ordering { - let after = self.apply_after.cmp(&other.apply_after); - if after != Ordering::Equal { - return after; - } - self.apply_before.cmp(&other.apply_before) - } -} -impl PartialOrd for DayFilter { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} diff --git a/src/models/flexibility.rs b/src/models/flexibility.rs deleted file mode 100644 index 64804f94..00000000 --- a/src/models/flexibility.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::models::day::Day; -use crate::models::goal::Goal; -use std::rc::Rc; - -#[derive(Debug, Clone)] -pub struct Flexibility { - pub goal: Rc, - pub day: Rc, -} -impl PartialEq for Flexibility { - fn eq(&self, other: &Self) -> bool { - self.goal.eq(&other.goal) && self.day.eq(&other.day) - } -} diff --git a/src/models/goal.rs b/src/models/goal.rs index 44c4eead..2fdd498d 100644 --- a/src/models/goal.rs +++ b/src/models/goal.rs @@ -1,77 +1,74 @@ -use crate::models::calendar::Span; -use crate::models::date::{DateTime, DateTimeRange}; -use crate::models::day_filter::DayFilter; -use std::hash::{Hash, Hasher}; +use std::ops::{Add, Sub}; -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +use chrono::{Datelike, Duration, NaiveDateTime, Weekday}; +use serde::Deserialize; + +use super::calendar::Calendar; + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] pub struct Goal { - id: String, - title: String, + pub id: String, + #[serde(default)] + pub start: NaiveDateTime, + #[serde(default)] + pub deadline: NaiveDateTime, + #[serde(rename = "budget")] + pub budget_config: Option, + pub filters: Option, + pub min_duration: Option, + pub title: String, + pub children: Option>, +} - min_span: Option, +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Filters { + pub after_time: usize, + pub before_time: usize, + pub on_days: Vec, +} - day_filter: Option, +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct BudgetConfig { + pub min_per_day: usize, + pub max_per_day: usize, + pub min_per_week: usize, + pub max_per_week: usize, } + impl Goal { - #[allow(dead_code)] - pub(crate) fn new(id: &str, title: &str) -> Self { - Self { - id: id.to_string(), - title: title.to_string(), - min_span: None, - day_filter: None, + pub fn get_adj_start_deadline(&self, calendar: &Calendar) -> (NaiveDateTime, NaiveDateTime) { + let mut adjusted_goal_start = self.start; + if self.start.year() == 1970 { + adjusted_goal_start = calendar.start_date_time; } - } - pub fn id(&self) -> String { - self.id.clone() - } - pub fn title(&self) -> String { - self.title.clone() - } - pub fn min_span(&self) -> Span { - self.min_span.unwrap_or(1) - } - pub fn day_filter(&self, date: &DateTime) -> DateTimeRange { - let out = DateTimeRange::new( - self.day_filter - .as_ref() - .map(|f| f.after(date)) - .unwrap_or(date.start_of_day()), - self.day_filter - .as_ref() - .map(|f| f.before(date)) - .unwrap_or(date.end_of_day()), - ); - out - } -} -impl From<&crate::legacy::goal::Goal> for Goal { - fn from(goal: &crate::legacy::goal::Goal) -> Self { - #[inline] - fn to_string(hour: usize) -> String { - format!("{hour:0>2}:00") + let mut adjusted_goal_deadline = self.deadline; + if self.deadline.year() == 1970 { + adjusted_goal_deadline = calendar.end_date_time; } - let filter = goal.filters.clone().map(|f| { - DayFilter::from_str( - f.after_time.map(to_string).as_deref(), - f.before_time.map(to_string).as_deref(), - ) - }); - - Self { - id: goal.id.clone(), - title: goal.title.clone(), - - min_span: goal.min_duration, - - day_filter: filter, + if self.filters.is_none() { + return (adjusted_goal_start, adjusted_goal_deadline); } - } -} -impl Hash for Goal { - fn hash(&self, state: &mut H) { - self.id.hash(state); - self.title.hash(state); + let filter_option = self.filters.clone().unwrap(); + if filter_option.after_time < filter_option.clone().before_time { + //normal case + } else { + // special case where we know that compatible times cross the midnight boundary + println!( + "Special case adjusting start from {:?}", + &adjusted_goal_start + ); + adjusted_goal_start = adjusted_goal_start + .sub(Duration::hours(24)) + .add(Duration::hours(filter_option.after_time as i64)); + println!("... to {:?}", &adjusted_goal_start); + adjusted_goal_deadline = adjusted_goal_start.add(Duration::days( + (adjusted_goal_deadline - adjusted_goal_start).num_days() + 1, + )); + } + (adjusted_goal_start, adjusted_goal_deadline) } } diff --git a/src/models/mod.rs b/src/models/mod.rs index dc314a24..1085682f 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,6 +1,5 @@ +pub mod activity; +pub mod budget; pub mod calendar; -pub mod date; -pub mod day; -pub mod day_filter; -pub mod flexibility; pub mod goal; +pub mod task; diff --git a/src/models/task.rs b/src/models/task.rs new file mode 100644 index 00000000..4e4b9f67 --- /dev/null +++ b/src/models/task.rs @@ -0,0 +1,28 @@ +///Tasks are only used for outputting +use chrono::{NaiveDate, NaiveDateTime}; +use serde::{Deserialize, Serialize}; + +use super::calendar::ImpossibleActivity; + +#[derive(Deserialize, Serialize, Debug)] +pub struct FinalTasks { + pub scheduled: Vec, + pub impossible: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Task { + pub taskid: usize, + pub goalid: String, + pub title: String, + pub duration: usize, + pub start: NaiveDateTime, + pub deadline: NaiveDateTime, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct DayTasks { + pub day: NaiveDate, + pub tasks: Vec, +} diff --git a/src/services/activity_generator.rs b/src/services/activity_generator.rs new file mode 100644 index 00000000..5bf96a46 --- /dev/null +++ b/src/services/activity_generator.rs @@ -0,0 +1,95 @@ +use crate::models::{activity::Activity, budget::TimeBudgetType, calendar::Calendar, goal::Goal}; + +pub fn generate_simple_goal_activities(calendar: &Calendar, goals: &Vec) -> Vec { + dbg!(&goals); + let mut activities: Vec = Vec::with_capacity(goals.capacity()); + for goal in goals { + let mut goal_activities = Activity::get_activities_from_simple_goal(goal, calendar); + dbg!(&goal_activities); + activities.append(&mut goal_activities); + } + activities +} + +pub fn generate_budget_goal_activities(calendar: &Calendar, goals: &Vec) -> Vec { + dbg!(&goals); + let mut activities: Vec = Vec::with_capacity(goals.capacity()); + for goal in goals { + let mut goal_activities = Activity::get_activities_from_budget_goal(goal, calendar); + dbg!(&goal_activities); + activities.append(&mut goal_activities); + } + activities +} + +pub fn generate_get_to_week_min_budget_activities( + calendar: &Calendar, + goals: &[Goal], +) -> Vec { + let mut get_to_week_min_budget_activities = vec![]; + for budget in &calendar.budgets { + let mut is_min_week_reached = true; + for time_budget in &budget.time_budgets { + if time_budget.time_budget_type == TimeBudgetType::Week { //TODO: Assuming only one week time_budget per budget - need to make multi-week compatilble + // Good + } else { + continue; + } + if time_budget.scheduled < time_budget.min_scheduled { + is_min_week_reached = false; + } + } + if is_min_week_reached { + //Fine + continue; + } else { + let goal_to_use: &Goal = goals + .iter() + .find(|g| g.id.eq(&budget.originating_goal_id)) + .unwrap(); + for time_budget in &budget.time_budgets { + if time_budget.time_budget_type == TimeBudgetType::Day + && time_budget.scheduled == time_budget.min_scheduled + && time_budget.max_scheduled > time_budget.min_scheduled + { + get_to_week_min_budget_activities.extend( + Activity::get_activities_to_get_min_week_budget( + goal_to_use, + calendar, + time_budget, + ), + ); + } + } + } + } + dbg!(&get_to_week_min_budget_activities); + get_to_week_min_budget_activities +} + +pub fn generate_top_up_week_budget_activities( + calendar: &Calendar, + goals: &[Goal], +) -> Vec { + let mut top_up_activities = vec![]; + for budget in &calendar.budgets { + let goal_to_use: &Goal = goals + .iter() + .find(|g| g.id.eq(&budget.originating_goal_id)) + .unwrap(); + for time_budget in &budget.time_budgets { + if time_budget.time_budget_type == TimeBudgetType::Day + && time_budget.min_scheduled < time_budget.max_scheduled + && time_budget.scheduled < time_budget.max_scheduled + { + top_up_activities.extend(Activity::get_activities_to_top_up_week_budget( + goal_to_use, + calendar, + time_budget, + )); + } + } + } + dbg!(&top_up_activities); + top_up_activities +} diff --git a/src/services/activity_placer.rs b/src/services/activity_placer.rs new file mode 100644 index 00000000..2023b6c5 --- /dev/null +++ b/src/services/activity_placer.rs @@ -0,0 +1,118 @@ +use std::rc::Rc; + +use crate::models::{ + activity::{Activity, ActivityType, Status}, + calendar::{Calendar, Hour, ImpossibleActivity}, +}; + +pub fn place(calendar: &mut Calendar, mut activities: Vec) { + loop { + for activity in activities.iter_mut() { + activity.update_overlay_with(&calendar.budgets); + } + let act_index_to_schedule = find_act_index_to_schedule(&activities); + if act_index_to_schedule.is_none() { + println!("Tried to schedule activity index None"); + break; + } + if activities[act_index_to_schedule.unwrap()].goal_id.len() > 5 { + println!( + "Next to schedule: {:?} {:?}", + &activities[act_index_to_schedule.unwrap()].title, + &activities[act_index_to_schedule.unwrap()].goal_id[0..5] + ); + } else { + println!( + "Next to schedule: {:?} {:?}", + &activities[act_index_to_schedule.unwrap()].title, + &activities[act_index_to_schedule.unwrap()].goal_id + ); + } + let best_hour_index_and_size: Option<(usize, usize)> = + activities[act_index_to_schedule.unwrap()].get_best_scheduling_index_and_length(); + let best_hour_index: usize; + let best_size: usize; + if best_hour_index_and_size.is_some() { + best_hour_index = best_hour_index_and_size.unwrap().0; + best_size = best_hour_index_and_size.unwrap().1; + println!( + "Best index:{:?} and size {:?}", + &best_hour_index, &best_size + ); + } else { + activities[act_index_to_schedule.unwrap()].release_claims(); + if activities[act_index_to_schedule.unwrap()].activity_type == ActivityType::Budget { + activities[act_index_to_schedule.unwrap()].status = Status::Processed; + continue; + } else { + activities[act_index_to_schedule.unwrap()].status = Status::Impossible; + } + let impossible_activity = ImpossibleActivity { + id: activities[act_index_to_schedule.unwrap()].goal_id.clone(), + hours_missing: activities[act_index_to_schedule.unwrap()].duration_left, + period_start_date_time: calendar.start_date_time, + period_end_date_time: calendar.end_date_time, + }; + calendar.impossible_activities.push(impossible_activity); + continue; + } + println!("reserving {:?} hours...", best_size); + for duration_offset in 0..best_size { + Rc::make_mut(&mut calendar.hours[best_hour_index + duration_offset]); + calendar.hours[best_hour_index + duration_offset] = Rc::new(Hour::Occupied { + activity_index: act_index_to_schedule.unwrap(), + activity_title: activities[act_index_to_schedule.unwrap()].title.clone(), + activity_goalid: activities[act_index_to_schedule.unwrap()].goal_id.clone(), + }); + //TODO: activity doesn't need to know about time_budets => remove completely + calendar.update_budgets_for( + &activities[act_index_to_schedule.unwrap()].goal_id.clone(), + best_hour_index + duration_offset, + ); + activities[act_index_to_schedule.unwrap()].duration_left -= 1; + } + if activities[act_index_to_schedule.unwrap()].duration_left == 0 { + activities[act_index_to_schedule.unwrap()].status = Status::Scheduled; + (activities[act_index_to_schedule.unwrap()]).release_claims(); + } + + dbg!(&calendar); + } + dbg!(&calendar); +} + +fn find_act_index_to_schedule(activities: &[Activity]) -> Option { + let mut act_index_to_schedule = None; + for index in 0..activities.len() { + if activities[index].status == Status::Scheduled + || activities[index].status == Status::Impossible + || activities[index].status == Status::Processed + { + continue; + } + match act_index_to_schedule { + None => act_index_to_schedule = Some(index), + Some(_) => match activities[index].flex() { + 0 => { + println!("Found activity index {:?} with flex 0...", &index); + continue; + } + 1 => { + if activities[act_index_to_schedule.unwrap()].flex() == 1 { + break; + } else { + act_index_to_schedule = Some(index); + break; + } + } + _ => { + if activities[act_index_to_schedule.unwrap()].flex() < activities[index].flex() + { + act_index_to_schedule = Some(index); + } + } + }, + } + } + act_index_to_schedule +} diff --git a/src/services/mod.rs b/src/services/mod.rs new file mode 100644 index 00000000..f951abbd --- /dev/null +++ b/src/services/mod.rs @@ -0,0 +1,2 @@ +pub mod activity_generator; +pub mod activity_placer; diff --git a/tests/common.rs b/src/technical/input_output.rs similarity index 58% rename from tests/common.rs rename to src/technical/input_output.rs index 345d0f1c..f691a6f4 100644 --- a/tests/common.rs +++ b/src/technical/input_output.rs @@ -1,10 +1,21 @@ -use scheduler::legacy::{input::Input, output::FinalTasks}; +use crate::models::goal::Goal; +use crate::models::task::FinalTasks; +use chrono::NaiveDateTime; +use serde::Deserialize; use std::error::Error; use std::fs::File; use std::io::prelude::*; use std::io::BufReader; use std::path::Path; +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +pub struct Input { + pub start_date: NaiveDateTime, + pub end_date: NaiveDateTime, + pub goals: Vec, +} + pub fn get_input_from_json>(path: P) -> Result> { let file = File::open(path)?; let reader = BufReader::new(file); @@ -13,14 +24,15 @@ pub fn get_input_from_json>(path: P) -> Result>(path: P) -> Result { + println!("get_output_string_from_json\n"); let file = File::open(path).expect("Error reading file"); let reader = BufReader::new(file); let output: FinalTasks = serde_json::from_reader(reader)?; serde_json::to_string_pretty(&output) } -pub fn write_to_file>(path: P, actual_output: &str) -> Result<(), Box> { +pub fn write_to_file>(path: P, output: &str) -> Result<(), Box> { let mut file = File::create(path)?; - file.write_all(actual_output.as_bytes())?; + file.write_all(output.as_bytes())?; Ok(()) } diff --git a/src/technical/mod.rs b/src/technical/mod.rs new file mode 100644 index 00000000..91165e68 --- /dev/null +++ b/src/technical/mod.rs @@ -0,0 +1 @@ +pub mod input_output; diff --git a/tests/jsons/rename_outputs.sh b/tests/jsons/rename_outputs.sh deleted file mode 100755 index 3e4f5b88..00000000 --- a/tests/jsons/rename_outputs.sh +++ /dev/null @@ -1,7 +0,0 @@ -#! /bin/bash - -for d in */ ; do - # echo $(ls $d) - mv "$d/output.json" "$d/expected.json" - mv "$d/actual_output.json" "$d/observed.json" -done; \ No newline at end of file diff --git a/tests/jsons/stable/algorithm-challenge/expected.json b/tests/jsons/stable/algorithm-challenge/expected.json index a3ee71df..e9149383 100644 --- a/tests/jsons/stable/algorithm-challenge/expected.json +++ b/tests/jsons/stable/algorithm-challenge/expected.json @@ -1,63 +1,58 @@ { - "scheduled": [ + "scheduled": [ + { + "day": "2022-01-01", + "tasks": [ { - "day": "2022-01-01", - "tasks": [ - { - "taskid": 0, - "goalid": "free", - "title": "free", - "duration": 10, - "start": "2022-01-01T00:00:00", - "deadline": "2022-01-01T10:00:00" - }, - { - "taskid": 1, - "goalid": "2", - "title": "dentist", - "duration": 1, - "start": "2022-01-01T10:00:00", - "deadline": "2022-01-01T11:00:00" - }, - { - "taskid": 2, - "goalid": "1", - "title": "shopping", - "duration": 2, - "start": "2022-01-01T11:00:00", - "deadline": "2022-01-01T13:00:00" - }, - { - "taskid": 3, - "goalid": "4", - "title": "lunch", - "duration": 1, - "start": "2022-01-01T13:00:00", - "deadline": "2022-01-01T14:00:00" - }, - { - "taskid": 4, - "goalid": "3", - "title": "Visit friend", - "duration": 2, - "start": "2022-01-01T14:00:00", - "deadline": "2022-01-01T16:00:00" - }, - { - "taskid": 5, - "goalid": "free", - "title": "free", - "duration": 8, - "start": "2022-01-01T16:00:00", - "deadline": "2022-01-02T00:00:00" - } - ] - } - ], - "impossible": [ + "taskid": 0, + "goalid": "free", + "title": "free", + "duration": 10, + "start": "2022-01-01T00:00:00", + "deadline": "2022-01-01T10:00:00" + }, + { + "taskid": 1, + "goalid": "2", + "title": "dentist", + "duration": 1, + "start": "2022-01-01T10:00:00", + "deadline": "2022-01-01T11:00:00" + }, + { + "taskid": 2, + "goalid": "1", + "title": "shopping", + "duration": 2, + "start": "2022-01-01T11:00:00", + "deadline": "2022-01-01T13:00:00" + }, + { + "taskid": 3, + "goalid": "4", + "title": "lunch", + "duration": 1, + "start": "2022-01-01T13:00:00", + "deadline": "2022-01-01T14:00:00" + }, + { + "taskid": 4, + "goalid": "3", + "title": "Visit friend", + "duration": 2, + "start": "2022-01-01T14:00:00", + "deadline": "2022-01-01T16:00:00" + }, { - "day": "2022-01-01", - "tasks": [] + "taskid": 5, + "goalid": "free", + "title": "free", + "duration": 8, + "start": "2022-01-01T16:00:00", + "deadline": "2022-01-02T00:00:00" } - ] + ] + } + ], + "impossible": [] } \ No newline at end of file diff --git a/tests/jsons/stable/algorithm-challenge/input.json b/tests/jsons/stable/algorithm-challenge/input.json index 2a8551e8..39f08619 100644 --- a/tests/jsons/stable/algorithm-challenge/input.json +++ b/tests/jsons/stable/algorithm-challenge/input.json @@ -1,50 +1,34 @@ { - "startDate": "2022-01-01T00:00:00", - "endDate": "2022-01-02T00:00:00", - "goals": { - "1": { - "id": "1", - "title": "shopping", - "min_duration": 2, - "start": "2022-01-01T00:00:00", - "deadline": "2022-01-02T00:00:00", - "filters": { - "after_time": 9, - "before_time": 15 - } - }, - "2": { - "id": "2", - "title": "dentist", - "min_duration": 1, - "start": "2022-01-01T00:00:00", - "deadline": "2022-01-02T00:00:00", - "filters": { - "after_time": 10, - "before_time": 11 - } - }, - "3": { - "id": "3", - "title": "Visit friend", - "min_duration": 2, - "start": "2022-01-01T00:00:00", - "deadline": "2022-01-02T00:00:00", - "filters": { - "after_time": 10, - "before_time": 16 - } - }, - "4": { - "id": "4", - "title": "lunch", - "min_duration": 1, - "start": "2022-01-01T00:00:00", - "deadline": "2022-01-02T00:00:00", - "filters": { - "after_time": 12, - "before_time": 14 - } - } + "startDate": "2022-01-01T00:00:00", + "endDate": "2022-01-02T00:00:00", + "goals": [ + { + "id": "1", + "title": "shopping", + "minDuration": 2, + "start": "2022-01-01T09:00:00", + "deadline": "2022-01-01T15:00:00" + }, + { + "id": "2", + "title": "dentist", + "minDuration": 1, + "start": "2022-01-01T10:00:00", + "deadline": "2022-01-01T11:00:00" + }, + { + "id": "3", + "title": "Visit friend", + "minDuration": 2, + "start": "2022-01-01T10:00:00", + "deadline": "2022-01-01T16:00:00" + }, + { + "id": "4", + "title": "lunch", + "minDuration": 1, + "start": "2022-01-01T12:00:00", + "deadline": "2022-01-01T14:00:00" } - } \ No newline at end of file + ] +} \ No newline at end of file diff --git a/tests/jsons/stable/algorithm-challenge/observed.json b/tests/jsons/stable/algorithm-challenge/observed.json index 3c660bc7..e9149383 100644 --- a/tests/jsons/stable/algorithm-challenge/observed.json +++ b/tests/jsons/stable/algorithm-challenge/observed.json @@ -54,10 +54,5 @@ ] } ], - "impossible": [ - { - "day": "2022-01-01", - "tasks": [] - } - ] + "impossible": [] } \ No newline at end of file diff --git a/tests/jsons/stable/basic-1/expected.json b/tests/jsons/stable/basic-1/expected.json index 1a73fca8..5f66bc55 100644 --- a/tests/jsons/stable/basic-1/expected.json +++ b/tests/jsons/stable/basic-1/expected.json @@ -1,58 +1,58 @@ { - "scheduled": [{ - "day": "2022-01-01", - "tasks": [{ - "taskid": 0, - "goalid": "free", - "title": "free", - "duration": 10, - "start": "2022-01-01T00:00:00", - "deadline": "2022-01-01T10:00:00" - }, - { - "taskid": 1, - "goalid": "2", - "title": "dentist", - "duration": 1, - "start": "2022-01-01T10:00:00", - "deadline": "2022-01-01T11:00:00" - }, - { - "taskid": 2, - "goalid": "1", - "title": "shopping", - "duration": 1, - "start": "2022-01-01T11:00:00", - "deadline": "2022-01-01T12:00:00" - }, - { - "taskid": 3, - "goalid": "free", - "title": "free", - "duration": 1, - "start": "2022-01-01T12:00:00", - "deadline": "2022-01-01T13:00:00" - }, - { - "taskid": 4, - "goalid": "3", - "title": "exercise", - "duration": 1, - "start": "2022-01-01T13:00:00", - "deadline": "2022-01-01T14:00:00" - }, - { - "taskid": 5, - "goalid": "free", - "title": "free", - "duration": 10, - "start": "2022-01-01T14:00:00", - "deadline": "2022-01-02T00:00:00" - } - ] - }], - "impossible": [{ - "day": "2022-01-01", - "tasks": [] - }] + "scheduled": [ + { + "day": "2022-01-01", + "tasks": [ + { + "taskid": 0, + "goalid": "free", + "title": "free", + "duration": 10, + "start": "2022-01-01T00:00:00", + "deadline": "2022-01-01T10:00:00" + }, + { + "taskid": 1, + "goalid": "2", + "title": "dentist", + "duration": 1, + "start": "2022-01-01T10:00:00", + "deadline": "2022-01-01T11:00:00" + }, + { + "taskid": 2, + "goalid": "1", + "title": "shopping", + "duration": 1, + "start": "2022-01-01T11:00:00", + "deadline": "2022-01-01T12:00:00" + }, + { + "taskid": 3, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2022-01-01T12:00:00", + "deadline": "2022-01-01T13:00:00" + }, + { + "taskid": 4, + "goalid": "3", + "title": "exercise", + "duration": 1, + "start": "2022-01-01T13:00:00", + "deadline": "2022-01-01T14:00:00" + }, + { + "taskid": 5, + "goalid": "free", + "title": "free", + "duration": 10, + "start": "2022-01-01T14:00:00", + "deadline": "2022-01-02T00:00:00" + } + ] + } + ], + "impossible": [] } \ No newline at end of file diff --git a/tests/jsons/stable/basic-1/input.json b/tests/jsons/stable/basic-1/input.json index 4484466e..6f27f2ba 100644 --- a/tests/jsons/stable/basic-1/input.json +++ b/tests/jsons/stable/basic-1/input.json @@ -1,39 +1,27 @@ -{ - "startDate": "2022-01-01T00:00:00", - "endDate": "2022-01-02T00:00:00", - "goals": { - "1": { - "id": "1", - "title": "shopping", - "min_duration": 1, - "start": "2022-01-01T00:00:00", - "deadline": "2022-01-02T00:00:00", - "filters": { - "after_time": 10, - "before_time": 13 + { + "startDate": "2022-01-01T00:00:00", + "endDate": "2022-01-02T00:00:00", + "goals": [ + { + "id": "1", + "title": "shopping", + "minDuration": 1, + "start": "2022-01-01T10:00:00", + "deadline": "2022-01-01T13:00:00" + }, + { + "id": "2", + "title": "dentist", + "minDuration": 1, + "start": "2022-01-01T10:00:00", + "deadline": "2022-01-01T11:00:00" + }, + { + "id": "3", + "title": "exercise", + "minDuration": 1, + "start": "2022-01-01T13:00:00", + "deadline": "2022-01-01T18:00:00" } - }, - "2": { - "id": "2", - "title": "dentist", - "min_duration": 1, - "start": "2022-01-01T00:00:00", - "deadline": "2022-01-02T00:00:00", - "filters": { - "after_time": 10, - "before_time": 11 - } - }, - "3": { - "id": "3", - "title": "exercise", - "min_duration": 1, - "start": "2022-01-01T00:00:00", - "deadline": "2022-01-02T00:00:00", - "filters": { - "after_time": 13, - "before_time": 18 - } - } - } -} \ No newline at end of file + ] + } \ No newline at end of file diff --git a/tests/jsons/stable/basic-1/observed.json b/tests/jsons/stable/basic-1/observed.json index a017de16..5f66bc55 100644 --- a/tests/jsons/stable/basic-1/observed.json +++ b/tests/jsons/stable/basic-1/observed.json @@ -54,10 +54,5 @@ ] } ], - "impossible": [ - { - "day": "2022-01-01", - "tasks": [] - } - ] + "impossible": [] } \ No newline at end of file diff --git a/tests/jsons/stable/bug-start-missing/expected.json b/tests/jsons/stable/bug-start-missing/expected.json new file mode 100644 index 00000000..b1929395 --- /dev/null +++ b/tests/jsons/stable/bug-start-missing/expected.json @@ -0,0 +1,66 @@ +{ + "scheduled": [ + { + "day": "2022-01-01", + "tasks": [ + { + "taskid": 0, + "goalid": "887b021f-502e-4553-856c-e05104e440be", + "title": "Test", + "duration": 1, + "start": "2022-01-01T00:00:00", + "deadline": "2022-01-01T01:00:00" + }, + { + "taskid": 1, + "goalid": "free", + "title": "free", + "duration": 9, + "start": "2022-01-01T01:00:00", + "deadline": "2022-01-01T10:00:00" + }, + { + "taskid": 2, + "goalid": "2", + "title": "dentist", + "duration": 1, + "start": "2022-01-01T10:00:00", + "deadline": "2022-01-01T11:00:00" + }, + { + "taskid": 3, + "goalid": "1", + "title": "shopping", + "duration": 1, + "start": "2022-01-01T11:00:00", + "deadline": "2022-01-01T12:00:00" + }, + { + "taskid": 4, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2022-01-01T12:00:00", + "deadline": "2022-01-01T13:00:00" + }, + { + "taskid": 5, + "goalid": "3", + "title": "exercise", + "duration": 1, + "start": "2022-01-01T13:00:00", + "deadline": "2022-01-01T14:00:00" + }, + { + "taskid": 6, + "goalid": "free", + "title": "free", + "duration": 10, + "start": "2022-01-01T14:00:00", + "deadline": "2022-01-02T00:00:00" + } + ] + } + ], + "impossible": [] +} \ No newline at end of file diff --git a/tests/jsons/stable/bug-start-missing/input.json b/tests/jsons/stable/bug-start-missing/input.json new file mode 100644 index 00000000..b4129dcd --- /dev/null +++ b/tests/jsons/stable/bug-start-missing/input.json @@ -0,0 +1,33 @@ + { + "startDate": "2022-01-01T00:00:00", + "endDate": "2022-01-02T00:00:00", + "goals": [ + { + "id": "1", + "title": "shopping", + "minDuration": 1, + "start": "2022-01-01T10:00:00", + "deadline": "2022-01-01T13:00:00" + }, + { + "id": "2", + "title": "dentist", + "minDuration": 1, + "start": "2022-01-01T10:00:00", + "deadline": "2022-01-01T11:00:00" + }, + { + "id": "3", + "title": "exercise", + "minDuration": 1, + "start": "2022-01-01T13:00:00", + "deadline": "2022-01-01T18:00:00" + }, + { + "id": "887b021f-502e-4553-856c-e05104e440be", + "title": "Test", + "createdAt": "2024-01-08T19:44:40.695Z", + "minDuration": 1 + } + ] + } \ No newline at end of file diff --git a/tests/jsons/stable/bug-start-missing/observed.json b/tests/jsons/stable/bug-start-missing/observed.json new file mode 100644 index 00000000..b1929395 --- /dev/null +++ b/tests/jsons/stable/bug-start-missing/observed.json @@ -0,0 +1,66 @@ +{ + "scheduled": [ + { + "day": "2022-01-01", + "tasks": [ + { + "taskid": 0, + "goalid": "887b021f-502e-4553-856c-e05104e440be", + "title": "Test", + "duration": 1, + "start": "2022-01-01T00:00:00", + "deadline": "2022-01-01T01:00:00" + }, + { + "taskid": 1, + "goalid": "free", + "title": "free", + "duration": 9, + "start": "2022-01-01T01:00:00", + "deadline": "2022-01-01T10:00:00" + }, + { + "taskid": 2, + "goalid": "2", + "title": "dentist", + "duration": 1, + "start": "2022-01-01T10:00:00", + "deadline": "2022-01-01T11:00:00" + }, + { + "taskid": 3, + "goalid": "1", + "title": "shopping", + "duration": 1, + "start": "2022-01-01T11:00:00", + "deadline": "2022-01-01T12:00:00" + }, + { + "taskid": 4, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2022-01-01T12:00:00", + "deadline": "2022-01-01T13:00:00" + }, + { + "taskid": 5, + "goalid": "3", + "title": "exercise", + "duration": 1, + "start": "2022-01-01T13:00:00", + "deadline": "2022-01-01T14:00:00" + }, + { + "taskid": 6, + "goalid": "free", + "title": "free", + "duration": 10, + "start": "2022-01-01T14:00:00", + "deadline": "2022-01-02T00:00:00" + } + ] + } + ], + "impossible": [] +} \ No newline at end of file diff --git a/tests/jsons/stable/default-budgets/expected.json b/tests/jsons/stable/default-budgets/expected.json new file mode 100644 index 00000000..970aa17e --- /dev/null +++ b/tests/jsons/stable/default-budgets/expected.json @@ -0,0 +1,800 @@ +{ + "scheduled": [ + { + "day": "2024-01-08", + "tasks": [ + { + "taskid": 0, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 5, + "start": "2024-01-08T00:00:00", + "deadline": "2024-01-08T05:00:00" + }, + { + "taskid": 1, + "goalid": "445f787b-d742-4441-9744-e81c286aa3c8", + "title": "Me time 🧘🏽😌", + "duration": 1, + "start": "2024-01-08T05:00:00", + "deadline": "2024-01-08T06:00:00" + }, + { + "taskid": 2, + "goalid": "40842a7d-c282-406f-9cdf-3d1fbd8e4f61", + "title": "Breakfast πŸ₯πŸ₯£", + "duration": 1, + "start": "2024-01-08T06:00:00", + "deadline": "2024-01-08T07:00:00" + }, + { + "taskid": 3, + "goalid": "678eab49-960e-4519-ad0b-031a2f22aaba", + "title": "Work πŸ’ͺ🏽", + "duration": 5, + "start": "2024-01-08T07:00:00", + "deadline": "2024-01-08T12:00:00" + }, + { + "taskid": 4, + "goalid": "18be6978-ffac-46db-8b70-d321c311428a", + "title": "Lunch πŸ₯ͺ", + "duration": 1, + "start": "2024-01-08T12:00:00", + "deadline": "2024-01-08T13:00:00" + }, + { + "taskid": 5, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-08T13:00:00", + "deadline": "2024-01-08T14:00:00" + }, + { + "taskid": 6, + "goalid": "678eab49-960e-4519-ad0b-031a2f22aaba", + "title": "Work πŸ’ͺ🏽", + "duration": 3, + "start": "2024-01-08T14:00:00", + "deadline": "2024-01-08T17:00:00" + }, + { + "taskid": 7, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-08T17:00:00", + "deadline": "2024-01-08T18:00:00" + }, + { + "taskid": 8, + "goalid": "103b2eff-6ba5-47b5-ad4f-81d8e8ef5998", + "title": "Dinner 🍽️", + "duration": 1, + "start": "2024-01-08T18:00:00", + "deadline": "2024-01-08T19:00:00" + }, + { + "taskid": 9, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-08T19:00:00", + "deadline": "2024-01-08T20:00:00" + }, + { + "taskid": 10, + "goalid": "77e1f762-3a4f-44a3-8f24-1560641a3548", + "title": "Walk 🚢🏽", + "duration": 1, + "start": "2024-01-08T20:00:00", + "deadline": "2024-01-08T21:00:00" + }, + { + "taskid": 11, + "goalid": "f9a02a6a-8ba9-43d0-b4aa-c6503f77306f", + "title": "Family time πŸ₯°", + "duration": 1, + "start": "2024-01-08T21:00:00", + "deadline": "2024-01-08T22:00:00" + }, + { + "taskid": 12, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 2, + "start": "2024-01-08T22:00:00", + "deadline": "2024-01-09T00:00:00" + } + ] + }, + { + "day": "2024-01-09", + "tasks": [ + { + "taskid": 13, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 5, + "start": "2024-01-09T00:00:00", + "deadline": "2024-01-09T05:00:00" + }, + { + "taskid": 14, + "goalid": "445f787b-d742-4441-9744-e81c286aa3c8", + "title": "Me time 🧘🏽😌", + "duration": 1, + "start": "2024-01-09T05:00:00", + "deadline": "2024-01-09T06:00:00" + }, + { + "taskid": 15, + "goalid": "40842a7d-c282-406f-9cdf-3d1fbd8e4f61", + "title": "Breakfast πŸ₯πŸ₯£", + "duration": 1, + "start": "2024-01-09T06:00:00", + "deadline": "2024-01-09T07:00:00" + }, + { + "taskid": 16, + "goalid": "678eab49-960e-4519-ad0b-031a2f22aaba", + "title": "Work πŸ’ͺ🏽", + "duration": 5, + "start": "2024-01-09T07:00:00", + "deadline": "2024-01-09T12:00:00" + }, + { + "taskid": 17, + "goalid": "18be6978-ffac-46db-8b70-d321c311428a", + "title": "Lunch πŸ₯ͺ", + "duration": 1, + "start": "2024-01-09T12:00:00", + "deadline": "2024-01-09T13:00:00" + }, + { + "taskid": 18, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-09T13:00:00", + "deadline": "2024-01-09T14:00:00" + }, + { + "taskid": 19, + "goalid": "678eab49-960e-4519-ad0b-031a2f22aaba", + "title": "Work πŸ’ͺ🏽", + "duration": 3, + "start": "2024-01-09T14:00:00", + "deadline": "2024-01-09T17:00:00" + }, + { + "taskid": 20, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-09T17:00:00", + "deadline": "2024-01-09T18:00:00" + }, + { + "taskid": 21, + "goalid": "103b2eff-6ba5-47b5-ad4f-81d8e8ef5998", + "title": "Dinner 🍽️", + "duration": 1, + "start": "2024-01-09T18:00:00", + "deadline": "2024-01-09T19:00:00" + }, + { + "taskid": 22, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-09T19:00:00", + "deadline": "2024-01-09T20:00:00" + }, + { + "taskid": 23, + "goalid": "77e1f762-3a4f-44a3-8f24-1560641a3548", + "title": "Walk 🚢🏽", + "duration": 1, + "start": "2024-01-09T20:00:00", + "deadline": "2024-01-09T21:00:00" + }, + { + "taskid": 24, + "goalid": "f9a02a6a-8ba9-43d0-b4aa-c6503f77306f", + "title": "Family time πŸ₯°", + "duration": 1, + "start": "2024-01-09T21:00:00", + "deadline": "2024-01-09T22:00:00" + }, + { + "taskid": 25, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 2, + "start": "2024-01-09T22:00:00", + "deadline": "2024-01-10T00:00:00" + } + ] + }, + { + "day": "2024-01-10", + "tasks": [ + { + "taskid": 26, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 5, + "start": "2024-01-10T00:00:00", + "deadline": "2024-01-10T05:00:00" + }, + { + "taskid": 27, + "goalid": "445f787b-d742-4441-9744-e81c286aa3c8", + "title": "Me time 🧘🏽😌", + "duration": 1, + "start": "2024-01-10T05:00:00", + "deadline": "2024-01-10T06:00:00" + }, + { + "taskid": 28, + "goalid": "40842a7d-c282-406f-9cdf-3d1fbd8e4f61", + "title": "Breakfast πŸ₯πŸ₯£", + "duration": 1, + "start": "2024-01-10T06:00:00", + "deadline": "2024-01-10T07:00:00" + }, + { + "taskid": 29, + "goalid": "678eab49-960e-4519-ad0b-031a2f22aaba", + "title": "Work πŸ’ͺ🏽", + "duration": 5, + "start": "2024-01-10T07:00:00", + "deadline": "2024-01-10T12:00:00" + }, + { + "taskid": 30, + "goalid": "18be6978-ffac-46db-8b70-d321c311428a", + "title": "Lunch πŸ₯ͺ", + "duration": 1, + "start": "2024-01-10T12:00:00", + "deadline": "2024-01-10T13:00:00" + }, + { + "taskid": 31, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-10T13:00:00", + "deadline": "2024-01-10T14:00:00" + }, + { + "taskid": 32, + "goalid": "678eab49-960e-4519-ad0b-031a2f22aaba", + "title": "Work πŸ’ͺ🏽", + "duration": 3, + "start": "2024-01-10T14:00:00", + "deadline": "2024-01-10T17:00:00" + }, + { + "taskid": 33, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-10T17:00:00", + "deadline": "2024-01-10T18:00:00" + }, + { + "taskid": 34, + "goalid": "103b2eff-6ba5-47b5-ad4f-81d8e8ef5998", + "title": "Dinner 🍽️", + "duration": 1, + "start": "2024-01-10T18:00:00", + "deadline": "2024-01-10T19:00:00" + }, + { + "taskid": 35, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-10T19:00:00", + "deadline": "2024-01-10T20:00:00" + }, + { + "taskid": 36, + "goalid": "77e1f762-3a4f-44a3-8f24-1560641a3548", + "title": "Walk 🚢🏽", + "duration": 1, + "start": "2024-01-10T20:00:00", + "deadline": "2024-01-10T21:00:00" + }, + { + "taskid": 37, + "goalid": "f9a02a6a-8ba9-43d0-b4aa-c6503f77306f", + "title": "Family time πŸ₯°", + "duration": 1, + "start": "2024-01-10T21:00:00", + "deadline": "2024-01-10T22:00:00" + }, + { + "taskid": 38, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 2, + "start": "2024-01-10T22:00:00", + "deadline": "2024-01-11T00:00:00" + } + ] + }, + { + "day": "2024-01-11", + "tasks": [ + { + "taskid": 39, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 5, + "start": "2024-01-11T00:00:00", + "deadline": "2024-01-11T05:00:00" + }, + { + "taskid": 40, + "goalid": "445f787b-d742-4441-9744-e81c286aa3c8", + "title": "Me time 🧘🏽😌", + "duration": 1, + "start": "2024-01-11T05:00:00", + "deadline": "2024-01-11T06:00:00" + }, + { + "taskid": 41, + "goalid": "40842a7d-c282-406f-9cdf-3d1fbd8e4f61", + "title": "Breakfast πŸ₯πŸ₯£", + "duration": 1, + "start": "2024-01-11T06:00:00", + "deadline": "2024-01-11T07:00:00" + }, + { + "taskid": 42, + "goalid": "678eab49-960e-4519-ad0b-031a2f22aaba", + "title": "Work πŸ’ͺ🏽", + "duration": 5, + "start": "2024-01-11T07:00:00", + "deadline": "2024-01-11T12:00:00" + }, + { + "taskid": 43, + "goalid": "18be6978-ffac-46db-8b70-d321c311428a", + "title": "Lunch πŸ₯ͺ", + "duration": 1, + "start": "2024-01-11T12:00:00", + "deadline": "2024-01-11T13:00:00" + }, + { + "taskid": 44, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-11T13:00:00", + "deadline": "2024-01-11T14:00:00" + }, + { + "taskid": 45, + "goalid": "678eab49-960e-4519-ad0b-031a2f22aaba", + "title": "Work πŸ’ͺ🏽", + "duration": 3, + "start": "2024-01-11T14:00:00", + "deadline": "2024-01-11T17:00:00" + }, + { + "taskid": 46, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-11T17:00:00", + "deadline": "2024-01-11T18:00:00" + }, + { + "taskid": 47, + "goalid": "103b2eff-6ba5-47b5-ad4f-81d8e8ef5998", + "title": "Dinner 🍽️", + "duration": 1, + "start": "2024-01-11T18:00:00", + "deadline": "2024-01-11T19:00:00" + }, + { + "taskid": 48, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-11T19:00:00", + "deadline": "2024-01-11T20:00:00" + }, + { + "taskid": 49, + "goalid": "77e1f762-3a4f-44a3-8f24-1560641a3548", + "title": "Walk 🚢🏽", + "duration": 1, + "start": "2024-01-11T20:00:00", + "deadline": "2024-01-11T21:00:00" + }, + { + "taskid": 50, + "goalid": "f9a02a6a-8ba9-43d0-b4aa-c6503f77306f", + "title": "Family time πŸ₯°", + "duration": 1, + "start": "2024-01-11T21:00:00", + "deadline": "2024-01-11T22:00:00" + }, + { + "taskid": 51, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 2, + "start": "2024-01-11T22:00:00", + "deadline": "2024-01-12T00:00:00" + } + ] + }, + { + "day": "2024-01-12", + "tasks": [ + { + "taskid": 52, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 5, + "start": "2024-01-12T00:00:00", + "deadline": "2024-01-12T05:00:00" + }, + { + "taskid": 53, + "goalid": "445f787b-d742-4441-9744-e81c286aa3c8", + "title": "Me time 🧘🏽😌", + "duration": 1, + "start": "2024-01-12T05:00:00", + "deadline": "2024-01-12T06:00:00" + }, + { + "taskid": 54, + "goalid": "40842a7d-c282-406f-9cdf-3d1fbd8e4f61", + "title": "Breakfast πŸ₯πŸ₯£", + "duration": 1, + "start": "2024-01-12T06:00:00", + "deadline": "2024-01-12T07:00:00" + }, + { + "taskid": 55, + "goalid": "678eab49-960e-4519-ad0b-031a2f22aaba", + "title": "Work πŸ’ͺ🏽", + "duration": 5, + "start": "2024-01-12T07:00:00", + "deadline": "2024-01-12T12:00:00" + }, + { + "taskid": 56, + "goalid": "18be6978-ffac-46db-8b70-d321c311428a", + "title": "Lunch πŸ₯ͺ", + "duration": 1, + "start": "2024-01-12T12:00:00", + "deadline": "2024-01-12T13:00:00" + }, + { + "taskid": 57, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-12T13:00:00", + "deadline": "2024-01-12T14:00:00" + }, + { + "taskid": 58, + "goalid": "678eab49-960e-4519-ad0b-031a2f22aaba", + "title": "Work πŸ’ͺ🏽", + "duration": 3, + "start": "2024-01-12T14:00:00", + "deadline": "2024-01-12T17:00:00" + }, + { + "taskid": 59, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-12T17:00:00", + "deadline": "2024-01-12T18:00:00" + }, + { + "taskid": 60, + "goalid": "103b2eff-6ba5-47b5-ad4f-81d8e8ef5998", + "title": "Dinner 🍽️", + "duration": 1, + "start": "2024-01-12T18:00:00", + "deadline": "2024-01-12T19:00:00" + }, + { + "taskid": 61, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-12T19:00:00", + "deadline": "2024-01-12T20:00:00" + }, + { + "taskid": 62, + "goalid": "77e1f762-3a4f-44a3-8f24-1560641a3548", + "title": "Walk 🚢🏽", + "duration": 1, + "start": "2024-01-12T20:00:00", + "deadline": "2024-01-12T21:00:00" + }, + { + "taskid": 63, + "goalid": "f9a02a6a-8ba9-43d0-b4aa-c6503f77306f", + "title": "Family time πŸ₯°", + "duration": 1, + "start": "2024-01-12T21:00:00", + "deadline": "2024-01-12T22:00:00" + }, + { + "taskid": 64, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 2, + "start": "2024-01-12T22:00:00", + "deadline": "2024-01-13T00:00:00" + } + ] + }, + { + "day": "2024-01-13", + "tasks": [ + { + "taskid": 65, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 5, + "start": "2024-01-13T00:00:00", + "deadline": "2024-01-13T05:00:00" + }, + { + "taskid": 66, + "goalid": "445f787b-d742-4441-9744-e81c286aa3c8", + "title": "Me time 🧘🏽😌", + "duration": 1, + "start": "2024-01-13T05:00:00", + "deadline": "2024-01-13T06:00:00" + }, + { + "taskid": 67, + "goalid": "77e1f762-3a4f-44a3-8f24-1560641a3548", + "title": "Walk 🚢🏽", + "duration": 1, + "start": "2024-01-13T06:00:00", + "deadline": "2024-01-13T07:00:00" + }, + { + "taskid": 68, + "goalid": "40842a7d-c282-406f-9cdf-3d1fbd8e4f61", + "title": "Breakfast πŸ₯πŸ₯£", + "duration": 1, + "start": "2024-01-13T07:00:00", + "deadline": "2024-01-13T08:00:00" + }, + { + "taskid": 69, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-13T08:00:00", + "deadline": "2024-01-13T09:00:00" + }, + { + "taskid": 70, + "goalid": "a700170a-eb34-4162-a59a-3ce763f62205", + "title": "House chores πŸ‘πŸ§ΉπŸ› οΈ", + "duration": 1, + "start": "2024-01-13T09:00:00", + "deadline": "2024-01-13T10:00:00" + }, + { + "taskid": 71, + "goalid": "f9a02a6a-8ba9-43d0-b4aa-c6503f77306f", + "title": "Family time πŸ₯°", + "duration": 1, + "start": "2024-01-13T10:00:00", + "deadline": "2024-01-13T11:00:00" + }, + { + "taskid": 72, + "goalid": "0eac2855-7fc3-4947-a560-81b5ebe88572", + "title": "Hobby project πŸš‚πŸš‹", + "duration": 1, + "start": "2024-01-13T11:00:00", + "deadline": "2024-01-13T12:00:00" + }, + { + "taskid": 73, + "goalid": "18be6978-ffac-46db-8b70-d321c311428a", + "title": "Lunch πŸ₯ͺ", + "duration": 1, + "start": "2024-01-13T12:00:00", + "deadline": "2024-01-13T13:00:00" + }, + { + "taskid": 74, + "goalid": "f9a02a6a-8ba9-43d0-b4aa-c6503f77306f", + "title": "Family time πŸ₯°", + "duration": 1, + "start": "2024-01-13T13:00:00", + "deadline": "2024-01-13T14:00:00" + }, + { + "taskid": 75, + "goalid": "0eac2855-7fc3-4947-a560-81b5ebe88572", + "title": "Hobby project πŸš‚πŸš‹", + "duration": 2, + "start": "2024-01-13T14:00:00", + "deadline": "2024-01-13T16:00:00" + }, + { + "taskid": 76, + "goalid": "free", + "title": "free", + "duration": 2, + "start": "2024-01-13T16:00:00", + "deadline": "2024-01-13T18:00:00" + }, + { + "taskid": 77, + "goalid": "103b2eff-6ba5-47b5-ad4f-81d8e8ef5998", + "title": "Dinner 🍽️", + "duration": 1, + "start": "2024-01-13T18:00:00", + "deadline": "2024-01-13T19:00:00" + }, + { + "taskid": 78, + "goalid": "free", + "title": "free", + "duration": 3, + "start": "2024-01-13T19:00:00", + "deadline": "2024-01-13T22:00:00" + }, + { + "taskid": 79, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 2, + "start": "2024-01-13T22:00:00", + "deadline": "2024-01-14T00:00:00" + } + ] + }, + { + "day": "2024-01-14", + "tasks": [ + { + "taskid": 80, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 5, + "start": "2024-01-14T00:00:00", + "deadline": "2024-01-14T05:00:00" + }, + { + "taskid": 81, + "goalid": "445f787b-d742-4441-9744-e81c286aa3c8", + "title": "Me time 🧘🏽😌", + "duration": 1, + "start": "2024-01-14T05:00:00", + "deadline": "2024-01-14T06:00:00" + }, + { + "taskid": 82, + "goalid": "77e1f762-3a4f-44a3-8f24-1560641a3548", + "title": "Walk 🚢🏽", + "duration": 1, + "start": "2024-01-14T06:00:00", + "deadline": "2024-01-14T07:00:00" + }, + { + "taskid": 83, + "goalid": "40842a7d-c282-406f-9cdf-3d1fbd8e4f61", + "title": "Breakfast πŸ₯πŸ₯£", + "duration": 1, + "start": "2024-01-14T07:00:00", + "deadline": "2024-01-14T08:00:00" + }, + { + "taskid": 84, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-14T08:00:00", + "deadline": "2024-01-14T09:00:00" + }, + { + "taskid": 85, + "goalid": "a700170a-eb34-4162-a59a-3ce763f62205", + "title": "House chores πŸ‘πŸ§ΉπŸ› οΈ", + "duration": 1, + "start": "2024-01-14T09:00:00", + "deadline": "2024-01-14T10:00:00" + }, + { + "taskid": 86, + "goalid": "f9a02a6a-8ba9-43d0-b4aa-c6503f77306f", + "title": "Family time πŸ₯°", + "duration": 2, + "start": "2024-01-14T10:00:00", + "deadline": "2024-01-14T12:00:00" + }, + { + "taskid": 87, + "goalid": "18be6978-ffac-46db-8b70-d321c311428a", + "title": "Lunch πŸ₯ͺ", + "duration": 1, + "start": "2024-01-14T12:00:00", + "deadline": "2024-01-14T13:00:00" + }, + { + "taskid": 88, + "goalid": "f9a02a6a-8ba9-43d0-b4aa-c6503f77306f", + "title": "Family time πŸ₯°", + "duration": 1, + "start": "2024-01-14T13:00:00", + "deadline": "2024-01-14T14:00:00" + }, + { + "taskid": 89, + "goalid": "0eac2855-7fc3-4947-a560-81b5ebe88572", + "title": "Hobby project πŸš‚πŸš‹", + "duration": 1, + "start": "2024-01-14T14:00:00", + "deadline": "2024-01-14T15:00:00" + }, + { + "taskid": 90, + "goalid": "a700170a-eb34-4162-a59a-3ce763f62205", + "title": "House chores πŸ‘πŸ§ΉπŸ› οΈ", + "duration": 1, + "start": "2024-01-14T15:00:00", + "deadline": "2024-01-14T16:00:00" + }, + { + "taskid": 91, + "goalid": "free", + "title": "free", + "duration": 2, + "start": "2024-01-14T16:00:00", + "deadline": "2024-01-14T18:00:00" + }, + { + "taskid": 92, + "goalid": "103b2eff-6ba5-47b5-ad4f-81d8e8ef5998", + "title": "Dinner 🍽️", + "duration": 1, + "start": "2024-01-14T18:00:00", + "deadline": "2024-01-14T19:00:00" + }, + { + "taskid": 93, + "goalid": "free", + "title": "free", + "duration": 3, + "start": "2024-01-14T19:00:00", + "deadline": "2024-01-14T22:00:00" + }, + { + "taskid": 94, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 2, + "start": "2024-01-14T22:00:00", + "deadline": "2024-01-15T00:00:00" + } + ] + } + ], + "impossible": [] +} \ No newline at end of file diff --git a/tests/jsons/stable/default-budgets/input.json b/tests/jsons/stable/default-budgets/input.json new file mode 100644 index 00000000..b8c4aa06 --- /dev/null +++ b/tests/jsons/stable/default-budgets/input.json @@ -0,0 +1,251 @@ +{ + "startDate": "2024-01-08T00:00:00", + "endDate": "2024-01-15T00:00:00", + "goals": [ + { + "id": "44faf46c-73ad-4140-a5cb-d9a69f51859b", + "title": "Daily habits πŸ”", + "createdAt": "2024-01-08T10:59:47.134Z", + "children": [ + "breakfast", + "lunch", + "dinner", + "meTime", + "walk" + ] + }, + { + "id": "77e1f762-3a4f-44a3-8f24-1560641a3548", + "title": "Walk 🚢🏽", + "filters": { + "afterTime": 6, + "beforeTime": 21, + "onDays": [ + "mon", + "tue", + "wed", + "thu", + "fri", + "sat", + "sun" + ] + }, + "createdAt": "2024-01-08T10:59:47.135Z", + "budget": { + "minPerDay": 1, + "maxPerDay": 1, + "minPerWeek": 7, + "maxPerWeek": 7 + } + }, + { + "id": "0eac2855-7fc3-4947-a560-81b5ebe88572", + "title": "Hobby project πŸš‚πŸš‹", + "filters": { + "afterTime": 9, + "beforeTime": 24, + "onDays": [ + "mon", + "tue", + "wed", + "thu", + "fri", + "sat", + "sun" + ] + }, + "createdAt": "2024-01-08T10:59:47.130Z", + "budget": { + "minPerDay": 0, + "maxPerDay": 4, + "minPerWeek": 1, + "maxPerWeek": 4 + } + }, + { + "id": "a700170a-eb34-4162-a59a-3ce763f62205", + "title": "House chores πŸ‘πŸ§ΉπŸ› οΈ", + "filters": { + "afterTime": 9, + "beforeTime": 24, + "onDays": [ + "sat", + "sun" + ] + }, + "createdAt": "2024-01-08T10:59:47.131Z", + "budget": { + "minPerDay": 1, + "maxPerDay": 3, + "minPerWeek": 2, + "maxPerWeek": 3 + } + }, + { + "id": "f9a02a6a-8ba9-43d0-b4aa-c6503f77306f", + "title": "Family time πŸ₯°", + "filters": { + "afterTime": 9, + "beforeTime": 24, + "onDays": [ + "mon", + "tue", + "wed", + "thu", + "fri", + "sat", + "sun" + ] + }, + "createdAt": "2024-01-08T10:59:47.132Z", + "budget": { + "minPerDay": 1, + "maxPerDay": 6, + "minPerWeek": 10, + "maxPerWeek": 10 + } + }, + { + "id": "445f787b-d742-4441-9744-e81c286aa3c8", + "title": "Me time 🧘🏽😌", + "filters": { + "afterTime": 5, + "beforeTime": 23, + "onDays": [ + "mon", + "tue", + "wed", + "thu", + "fri", + "sat", + "sun" + ] + }, + "createdAt": "2024-01-08T10:59:47.135Z", + "budget": { + "minPerDay": 1, + "maxPerDay": 1, + "minPerWeek": 7, + "maxPerWeek": 7 + } + }, + { + "id": "678eab49-960e-4519-ad0b-031a2f22aaba", + "title": "Work πŸ’ͺ🏽", + "filters": { + "afterTime": 6, + "beforeTime": 18, + "onDays": [ + "mon", + "tue", + "wed", + "thu", + "fri" + ] + }, + "createdAt": "2024-01-08T10:59:47.133Z", + "budget": { + "minPerDay": 6, + "maxPerDay": 10, + "minPerWeek": 40, + "maxPerWeek": 40 + } + }, + { + "id": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "filters": { + "afterTime": 22, + "beforeTime": 7, + "onDays": [ + "mon", + "tue", + "wed", + "thu", + "fri", + "sat", + "sun" + ] + }, + "createdAt": "2024-01-08T10:59:47.130Z", + "budget": { + "minPerDay": 6, + "maxPerDay": 8, + "minPerWeek": 42, + "maxPerWeek": 52 + } + }, + { + "id": "40842a7d-c282-406f-9cdf-3d1fbd8e4f61", + "title": "Breakfast πŸ₯πŸ₯£", + "filters": { + "afterTime": 6, + "beforeTime": 9, + "onDays": [ + "mon", + "tue", + "wed", + "thu", + "fri", + "sat", + "sun" + ] + }, + "createdAt": "2024-01-08T10:59:47.137Z", + "budget": { + "minPerDay": 1, + "maxPerDay": 1, + "minPerWeek": 7, + "maxPerWeek": 7 + } + }, + { + "id": "18be6978-ffac-46db-8b70-d321c311428a", + "title": "Lunch πŸ₯ͺ", + "filters": { + "afterTime": 12, + "beforeTime": 14, + "onDays": [ + "mon", + "tue", + "wed", + "thu", + "fri", + "sat", + "sun" + ] + }, + "createdAt": "2024-01-08T10:59:47.137Z", + "budget": { + "minPerDay": 1, + "maxPerDay": 1, + "minPerWeek": 7, + "maxPerWeek": 7 + } + }, + { + "id": "103b2eff-6ba5-47b5-ad4f-81d8e8ef5998", + "title": "Dinner 🍽️", + "filters": { + "afterTime": 18, + "beforeTime": 20, + "onDays": [ + "mon", + "tue", + "wed", + "thu", + "fri", + "sat", + "sun" + ] + }, + "createdAt": "2024-01-08T10:59:47.137Z", + "budget": { + "minPerDay": 1, + "maxPerDay": 1, + "minPerWeek": 7, + "maxPerWeek": 7 + } + } + ] +} \ No newline at end of file diff --git a/tests/jsons/stable/default-budgets/observed.json b/tests/jsons/stable/default-budgets/observed.json new file mode 100644 index 00000000..970aa17e --- /dev/null +++ b/tests/jsons/stable/default-budgets/observed.json @@ -0,0 +1,800 @@ +{ + "scheduled": [ + { + "day": "2024-01-08", + "tasks": [ + { + "taskid": 0, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 5, + "start": "2024-01-08T00:00:00", + "deadline": "2024-01-08T05:00:00" + }, + { + "taskid": 1, + "goalid": "445f787b-d742-4441-9744-e81c286aa3c8", + "title": "Me time 🧘🏽😌", + "duration": 1, + "start": "2024-01-08T05:00:00", + "deadline": "2024-01-08T06:00:00" + }, + { + "taskid": 2, + "goalid": "40842a7d-c282-406f-9cdf-3d1fbd8e4f61", + "title": "Breakfast πŸ₯πŸ₯£", + "duration": 1, + "start": "2024-01-08T06:00:00", + "deadline": "2024-01-08T07:00:00" + }, + { + "taskid": 3, + "goalid": "678eab49-960e-4519-ad0b-031a2f22aaba", + "title": "Work πŸ’ͺ🏽", + "duration": 5, + "start": "2024-01-08T07:00:00", + "deadline": "2024-01-08T12:00:00" + }, + { + "taskid": 4, + "goalid": "18be6978-ffac-46db-8b70-d321c311428a", + "title": "Lunch πŸ₯ͺ", + "duration": 1, + "start": "2024-01-08T12:00:00", + "deadline": "2024-01-08T13:00:00" + }, + { + "taskid": 5, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-08T13:00:00", + "deadline": "2024-01-08T14:00:00" + }, + { + "taskid": 6, + "goalid": "678eab49-960e-4519-ad0b-031a2f22aaba", + "title": "Work πŸ’ͺ🏽", + "duration": 3, + "start": "2024-01-08T14:00:00", + "deadline": "2024-01-08T17:00:00" + }, + { + "taskid": 7, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-08T17:00:00", + "deadline": "2024-01-08T18:00:00" + }, + { + "taskid": 8, + "goalid": "103b2eff-6ba5-47b5-ad4f-81d8e8ef5998", + "title": "Dinner 🍽️", + "duration": 1, + "start": "2024-01-08T18:00:00", + "deadline": "2024-01-08T19:00:00" + }, + { + "taskid": 9, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-08T19:00:00", + "deadline": "2024-01-08T20:00:00" + }, + { + "taskid": 10, + "goalid": "77e1f762-3a4f-44a3-8f24-1560641a3548", + "title": "Walk 🚢🏽", + "duration": 1, + "start": "2024-01-08T20:00:00", + "deadline": "2024-01-08T21:00:00" + }, + { + "taskid": 11, + "goalid": "f9a02a6a-8ba9-43d0-b4aa-c6503f77306f", + "title": "Family time πŸ₯°", + "duration": 1, + "start": "2024-01-08T21:00:00", + "deadline": "2024-01-08T22:00:00" + }, + { + "taskid": 12, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 2, + "start": "2024-01-08T22:00:00", + "deadline": "2024-01-09T00:00:00" + } + ] + }, + { + "day": "2024-01-09", + "tasks": [ + { + "taskid": 13, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 5, + "start": "2024-01-09T00:00:00", + "deadline": "2024-01-09T05:00:00" + }, + { + "taskid": 14, + "goalid": "445f787b-d742-4441-9744-e81c286aa3c8", + "title": "Me time 🧘🏽😌", + "duration": 1, + "start": "2024-01-09T05:00:00", + "deadline": "2024-01-09T06:00:00" + }, + { + "taskid": 15, + "goalid": "40842a7d-c282-406f-9cdf-3d1fbd8e4f61", + "title": "Breakfast πŸ₯πŸ₯£", + "duration": 1, + "start": "2024-01-09T06:00:00", + "deadline": "2024-01-09T07:00:00" + }, + { + "taskid": 16, + "goalid": "678eab49-960e-4519-ad0b-031a2f22aaba", + "title": "Work πŸ’ͺ🏽", + "duration": 5, + "start": "2024-01-09T07:00:00", + "deadline": "2024-01-09T12:00:00" + }, + { + "taskid": 17, + "goalid": "18be6978-ffac-46db-8b70-d321c311428a", + "title": "Lunch πŸ₯ͺ", + "duration": 1, + "start": "2024-01-09T12:00:00", + "deadline": "2024-01-09T13:00:00" + }, + { + "taskid": 18, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-09T13:00:00", + "deadline": "2024-01-09T14:00:00" + }, + { + "taskid": 19, + "goalid": "678eab49-960e-4519-ad0b-031a2f22aaba", + "title": "Work πŸ’ͺ🏽", + "duration": 3, + "start": "2024-01-09T14:00:00", + "deadline": "2024-01-09T17:00:00" + }, + { + "taskid": 20, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-09T17:00:00", + "deadline": "2024-01-09T18:00:00" + }, + { + "taskid": 21, + "goalid": "103b2eff-6ba5-47b5-ad4f-81d8e8ef5998", + "title": "Dinner 🍽️", + "duration": 1, + "start": "2024-01-09T18:00:00", + "deadline": "2024-01-09T19:00:00" + }, + { + "taskid": 22, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-09T19:00:00", + "deadline": "2024-01-09T20:00:00" + }, + { + "taskid": 23, + "goalid": "77e1f762-3a4f-44a3-8f24-1560641a3548", + "title": "Walk 🚢🏽", + "duration": 1, + "start": "2024-01-09T20:00:00", + "deadline": "2024-01-09T21:00:00" + }, + { + "taskid": 24, + "goalid": "f9a02a6a-8ba9-43d0-b4aa-c6503f77306f", + "title": "Family time πŸ₯°", + "duration": 1, + "start": "2024-01-09T21:00:00", + "deadline": "2024-01-09T22:00:00" + }, + { + "taskid": 25, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 2, + "start": "2024-01-09T22:00:00", + "deadline": "2024-01-10T00:00:00" + } + ] + }, + { + "day": "2024-01-10", + "tasks": [ + { + "taskid": 26, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 5, + "start": "2024-01-10T00:00:00", + "deadline": "2024-01-10T05:00:00" + }, + { + "taskid": 27, + "goalid": "445f787b-d742-4441-9744-e81c286aa3c8", + "title": "Me time 🧘🏽😌", + "duration": 1, + "start": "2024-01-10T05:00:00", + "deadline": "2024-01-10T06:00:00" + }, + { + "taskid": 28, + "goalid": "40842a7d-c282-406f-9cdf-3d1fbd8e4f61", + "title": "Breakfast πŸ₯πŸ₯£", + "duration": 1, + "start": "2024-01-10T06:00:00", + "deadline": "2024-01-10T07:00:00" + }, + { + "taskid": 29, + "goalid": "678eab49-960e-4519-ad0b-031a2f22aaba", + "title": "Work πŸ’ͺ🏽", + "duration": 5, + "start": "2024-01-10T07:00:00", + "deadline": "2024-01-10T12:00:00" + }, + { + "taskid": 30, + "goalid": "18be6978-ffac-46db-8b70-d321c311428a", + "title": "Lunch πŸ₯ͺ", + "duration": 1, + "start": "2024-01-10T12:00:00", + "deadline": "2024-01-10T13:00:00" + }, + { + "taskid": 31, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-10T13:00:00", + "deadline": "2024-01-10T14:00:00" + }, + { + "taskid": 32, + "goalid": "678eab49-960e-4519-ad0b-031a2f22aaba", + "title": "Work πŸ’ͺ🏽", + "duration": 3, + "start": "2024-01-10T14:00:00", + "deadline": "2024-01-10T17:00:00" + }, + { + "taskid": 33, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-10T17:00:00", + "deadline": "2024-01-10T18:00:00" + }, + { + "taskid": 34, + "goalid": "103b2eff-6ba5-47b5-ad4f-81d8e8ef5998", + "title": "Dinner 🍽️", + "duration": 1, + "start": "2024-01-10T18:00:00", + "deadline": "2024-01-10T19:00:00" + }, + { + "taskid": 35, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-10T19:00:00", + "deadline": "2024-01-10T20:00:00" + }, + { + "taskid": 36, + "goalid": "77e1f762-3a4f-44a3-8f24-1560641a3548", + "title": "Walk 🚢🏽", + "duration": 1, + "start": "2024-01-10T20:00:00", + "deadline": "2024-01-10T21:00:00" + }, + { + "taskid": 37, + "goalid": "f9a02a6a-8ba9-43d0-b4aa-c6503f77306f", + "title": "Family time πŸ₯°", + "duration": 1, + "start": "2024-01-10T21:00:00", + "deadline": "2024-01-10T22:00:00" + }, + { + "taskid": 38, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 2, + "start": "2024-01-10T22:00:00", + "deadline": "2024-01-11T00:00:00" + } + ] + }, + { + "day": "2024-01-11", + "tasks": [ + { + "taskid": 39, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 5, + "start": "2024-01-11T00:00:00", + "deadline": "2024-01-11T05:00:00" + }, + { + "taskid": 40, + "goalid": "445f787b-d742-4441-9744-e81c286aa3c8", + "title": "Me time 🧘🏽😌", + "duration": 1, + "start": "2024-01-11T05:00:00", + "deadline": "2024-01-11T06:00:00" + }, + { + "taskid": 41, + "goalid": "40842a7d-c282-406f-9cdf-3d1fbd8e4f61", + "title": "Breakfast πŸ₯πŸ₯£", + "duration": 1, + "start": "2024-01-11T06:00:00", + "deadline": "2024-01-11T07:00:00" + }, + { + "taskid": 42, + "goalid": "678eab49-960e-4519-ad0b-031a2f22aaba", + "title": "Work πŸ’ͺ🏽", + "duration": 5, + "start": "2024-01-11T07:00:00", + "deadline": "2024-01-11T12:00:00" + }, + { + "taskid": 43, + "goalid": "18be6978-ffac-46db-8b70-d321c311428a", + "title": "Lunch πŸ₯ͺ", + "duration": 1, + "start": "2024-01-11T12:00:00", + "deadline": "2024-01-11T13:00:00" + }, + { + "taskid": 44, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-11T13:00:00", + "deadline": "2024-01-11T14:00:00" + }, + { + "taskid": 45, + "goalid": "678eab49-960e-4519-ad0b-031a2f22aaba", + "title": "Work πŸ’ͺ🏽", + "duration": 3, + "start": "2024-01-11T14:00:00", + "deadline": "2024-01-11T17:00:00" + }, + { + "taskid": 46, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-11T17:00:00", + "deadline": "2024-01-11T18:00:00" + }, + { + "taskid": 47, + "goalid": "103b2eff-6ba5-47b5-ad4f-81d8e8ef5998", + "title": "Dinner 🍽️", + "duration": 1, + "start": "2024-01-11T18:00:00", + "deadline": "2024-01-11T19:00:00" + }, + { + "taskid": 48, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-11T19:00:00", + "deadline": "2024-01-11T20:00:00" + }, + { + "taskid": 49, + "goalid": "77e1f762-3a4f-44a3-8f24-1560641a3548", + "title": "Walk 🚢🏽", + "duration": 1, + "start": "2024-01-11T20:00:00", + "deadline": "2024-01-11T21:00:00" + }, + { + "taskid": 50, + "goalid": "f9a02a6a-8ba9-43d0-b4aa-c6503f77306f", + "title": "Family time πŸ₯°", + "duration": 1, + "start": "2024-01-11T21:00:00", + "deadline": "2024-01-11T22:00:00" + }, + { + "taskid": 51, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 2, + "start": "2024-01-11T22:00:00", + "deadline": "2024-01-12T00:00:00" + } + ] + }, + { + "day": "2024-01-12", + "tasks": [ + { + "taskid": 52, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 5, + "start": "2024-01-12T00:00:00", + "deadline": "2024-01-12T05:00:00" + }, + { + "taskid": 53, + "goalid": "445f787b-d742-4441-9744-e81c286aa3c8", + "title": "Me time 🧘🏽😌", + "duration": 1, + "start": "2024-01-12T05:00:00", + "deadline": "2024-01-12T06:00:00" + }, + { + "taskid": 54, + "goalid": "40842a7d-c282-406f-9cdf-3d1fbd8e4f61", + "title": "Breakfast πŸ₯πŸ₯£", + "duration": 1, + "start": "2024-01-12T06:00:00", + "deadline": "2024-01-12T07:00:00" + }, + { + "taskid": 55, + "goalid": "678eab49-960e-4519-ad0b-031a2f22aaba", + "title": "Work πŸ’ͺ🏽", + "duration": 5, + "start": "2024-01-12T07:00:00", + "deadline": "2024-01-12T12:00:00" + }, + { + "taskid": 56, + "goalid": "18be6978-ffac-46db-8b70-d321c311428a", + "title": "Lunch πŸ₯ͺ", + "duration": 1, + "start": "2024-01-12T12:00:00", + "deadline": "2024-01-12T13:00:00" + }, + { + "taskid": 57, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-12T13:00:00", + "deadline": "2024-01-12T14:00:00" + }, + { + "taskid": 58, + "goalid": "678eab49-960e-4519-ad0b-031a2f22aaba", + "title": "Work πŸ’ͺ🏽", + "duration": 3, + "start": "2024-01-12T14:00:00", + "deadline": "2024-01-12T17:00:00" + }, + { + "taskid": 59, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-12T17:00:00", + "deadline": "2024-01-12T18:00:00" + }, + { + "taskid": 60, + "goalid": "103b2eff-6ba5-47b5-ad4f-81d8e8ef5998", + "title": "Dinner 🍽️", + "duration": 1, + "start": "2024-01-12T18:00:00", + "deadline": "2024-01-12T19:00:00" + }, + { + "taskid": 61, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-12T19:00:00", + "deadline": "2024-01-12T20:00:00" + }, + { + "taskid": 62, + "goalid": "77e1f762-3a4f-44a3-8f24-1560641a3548", + "title": "Walk 🚢🏽", + "duration": 1, + "start": "2024-01-12T20:00:00", + "deadline": "2024-01-12T21:00:00" + }, + { + "taskid": 63, + "goalid": "f9a02a6a-8ba9-43d0-b4aa-c6503f77306f", + "title": "Family time πŸ₯°", + "duration": 1, + "start": "2024-01-12T21:00:00", + "deadline": "2024-01-12T22:00:00" + }, + { + "taskid": 64, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 2, + "start": "2024-01-12T22:00:00", + "deadline": "2024-01-13T00:00:00" + } + ] + }, + { + "day": "2024-01-13", + "tasks": [ + { + "taskid": 65, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 5, + "start": "2024-01-13T00:00:00", + "deadline": "2024-01-13T05:00:00" + }, + { + "taskid": 66, + "goalid": "445f787b-d742-4441-9744-e81c286aa3c8", + "title": "Me time 🧘🏽😌", + "duration": 1, + "start": "2024-01-13T05:00:00", + "deadline": "2024-01-13T06:00:00" + }, + { + "taskid": 67, + "goalid": "77e1f762-3a4f-44a3-8f24-1560641a3548", + "title": "Walk 🚢🏽", + "duration": 1, + "start": "2024-01-13T06:00:00", + "deadline": "2024-01-13T07:00:00" + }, + { + "taskid": 68, + "goalid": "40842a7d-c282-406f-9cdf-3d1fbd8e4f61", + "title": "Breakfast πŸ₯πŸ₯£", + "duration": 1, + "start": "2024-01-13T07:00:00", + "deadline": "2024-01-13T08:00:00" + }, + { + "taskid": 69, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-13T08:00:00", + "deadline": "2024-01-13T09:00:00" + }, + { + "taskid": 70, + "goalid": "a700170a-eb34-4162-a59a-3ce763f62205", + "title": "House chores πŸ‘πŸ§ΉπŸ› οΈ", + "duration": 1, + "start": "2024-01-13T09:00:00", + "deadline": "2024-01-13T10:00:00" + }, + { + "taskid": 71, + "goalid": "f9a02a6a-8ba9-43d0-b4aa-c6503f77306f", + "title": "Family time πŸ₯°", + "duration": 1, + "start": "2024-01-13T10:00:00", + "deadline": "2024-01-13T11:00:00" + }, + { + "taskid": 72, + "goalid": "0eac2855-7fc3-4947-a560-81b5ebe88572", + "title": "Hobby project πŸš‚πŸš‹", + "duration": 1, + "start": "2024-01-13T11:00:00", + "deadline": "2024-01-13T12:00:00" + }, + { + "taskid": 73, + "goalid": "18be6978-ffac-46db-8b70-d321c311428a", + "title": "Lunch πŸ₯ͺ", + "duration": 1, + "start": "2024-01-13T12:00:00", + "deadline": "2024-01-13T13:00:00" + }, + { + "taskid": 74, + "goalid": "f9a02a6a-8ba9-43d0-b4aa-c6503f77306f", + "title": "Family time πŸ₯°", + "duration": 1, + "start": "2024-01-13T13:00:00", + "deadline": "2024-01-13T14:00:00" + }, + { + "taskid": 75, + "goalid": "0eac2855-7fc3-4947-a560-81b5ebe88572", + "title": "Hobby project πŸš‚πŸš‹", + "duration": 2, + "start": "2024-01-13T14:00:00", + "deadline": "2024-01-13T16:00:00" + }, + { + "taskid": 76, + "goalid": "free", + "title": "free", + "duration": 2, + "start": "2024-01-13T16:00:00", + "deadline": "2024-01-13T18:00:00" + }, + { + "taskid": 77, + "goalid": "103b2eff-6ba5-47b5-ad4f-81d8e8ef5998", + "title": "Dinner 🍽️", + "duration": 1, + "start": "2024-01-13T18:00:00", + "deadline": "2024-01-13T19:00:00" + }, + { + "taskid": 78, + "goalid": "free", + "title": "free", + "duration": 3, + "start": "2024-01-13T19:00:00", + "deadline": "2024-01-13T22:00:00" + }, + { + "taskid": 79, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 2, + "start": "2024-01-13T22:00:00", + "deadline": "2024-01-14T00:00:00" + } + ] + }, + { + "day": "2024-01-14", + "tasks": [ + { + "taskid": 80, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 5, + "start": "2024-01-14T00:00:00", + "deadline": "2024-01-14T05:00:00" + }, + { + "taskid": 81, + "goalid": "445f787b-d742-4441-9744-e81c286aa3c8", + "title": "Me time 🧘🏽😌", + "duration": 1, + "start": "2024-01-14T05:00:00", + "deadline": "2024-01-14T06:00:00" + }, + { + "taskid": 82, + "goalid": "77e1f762-3a4f-44a3-8f24-1560641a3548", + "title": "Walk 🚢🏽", + "duration": 1, + "start": "2024-01-14T06:00:00", + "deadline": "2024-01-14T07:00:00" + }, + { + "taskid": 83, + "goalid": "40842a7d-c282-406f-9cdf-3d1fbd8e4f61", + "title": "Breakfast πŸ₯πŸ₯£", + "duration": 1, + "start": "2024-01-14T07:00:00", + "deadline": "2024-01-14T08:00:00" + }, + { + "taskid": 84, + "goalid": "free", + "title": "free", + "duration": 1, + "start": "2024-01-14T08:00:00", + "deadline": "2024-01-14T09:00:00" + }, + { + "taskid": 85, + "goalid": "a700170a-eb34-4162-a59a-3ce763f62205", + "title": "House chores πŸ‘πŸ§ΉπŸ› οΈ", + "duration": 1, + "start": "2024-01-14T09:00:00", + "deadline": "2024-01-14T10:00:00" + }, + { + "taskid": 86, + "goalid": "f9a02a6a-8ba9-43d0-b4aa-c6503f77306f", + "title": "Family time πŸ₯°", + "duration": 2, + "start": "2024-01-14T10:00:00", + "deadline": "2024-01-14T12:00:00" + }, + { + "taskid": 87, + "goalid": "18be6978-ffac-46db-8b70-d321c311428a", + "title": "Lunch πŸ₯ͺ", + "duration": 1, + "start": "2024-01-14T12:00:00", + "deadline": "2024-01-14T13:00:00" + }, + { + "taskid": 88, + "goalid": "f9a02a6a-8ba9-43d0-b4aa-c6503f77306f", + "title": "Family time πŸ₯°", + "duration": 1, + "start": "2024-01-14T13:00:00", + "deadline": "2024-01-14T14:00:00" + }, + { + "taskid": 89, + "goalid": "0eac2855-7fc3-4947-a560-81b5ebe88572", + "title": "Hobby project πŸš‚πŸš‹", + "duration": 1, + "start": "2024-01-14T14:00:00", + "deadline": "2024-01-14T15:00:00" + }, + { + "taskid": 90, + "goalid": "a700170a-eb34-4162-a59a-3ce763f62205", + "title": "House chores πŸ‘πŸ§ΉπŸ› οΈ", + "duration": 1, + "start": "2024-01-14T15:00:00", + "deadline": "2024-01-14T16:00:00" + }, + { + "taskid": 91, + "goalid": "free", + "title": "free", + "duration": 2, + "start": "2024-01-14T16:00:00", + "deadline": "2024-01-14T18:00:00" + }, + { + "taskid": 92, + "goalid": "103b2eff-6ba5-47b5-ad4f-81d8e8ef5998", + "title": "Dinner 🍽️", + "duration": 1, + "start": "2024-01-14T18:00:00", + "deadline": "2024-01-14T19:00:00" + }, + { + "taskid": 93, + "goalid": "free", + "title": "free", + "duration": 3, + "start": "2024-01-14T19:00:00", + "deadline": "2024-01-14T22:00:00" + }, + { + "taskid": 94, + "goalid": "49b05463-56a0-4af5-9034-83822abf24f6", + "title": "Sleep πŸ˜΄πŸŒ™", + "duration": 2, + "start": "2024-01-14T22:00:00", + "deadline": "2024-01-15T00:00:00" + } + ] + } + ], + "impossible": [] +} \ No newline at end of file diff --git a/tests/jsons/stable/sleep-1/expected.json b/tests/jsons/stable/sleep-1/expected.json new file mode 100644 index 00000000..9dd39b95 --- /dev/null +++ b/tests/jsons/stable/sleep-1/expected.json @@ -0,0 +1,237 @@ +{ + "scheduled": [ + { + "day": "2022-01-01", + "tasks": [ + { + "taskid": 0, + "goalid": "1", + "title": "sleep", + "duration": 6, + "start": "2022-01-01T00:00:00", + "deadline": "2022-01-01T06:00:00" + }, + { + "taskid": 1, + "goalid": "free", + "title": "free", + "duration": 16, + "start": "2022-01-01T06:00:00", + "deadline": "2022-01-01T22:00:00" + }, + { + "taskid": 2, + "goalid": "1", + "title": "sleep", + "duration": 2, + "start": "2022-01-01T22:00:00", + "deadline": "2022-01-02T00:00:00" + } + ] + }, + { + "day": "2022-01-02", + "tasks": [ + { + "taskid": 3, + "goalid": "1", + "title": "sleep", + "duration": 6, + "start": "2022-01-02T00:00:00", + "deadline": "2022-01-02T06:00:00" + }, + { + "taskid": 4, + "goalid": "free", + "title": "free", + "duration": 16, + "start": "2022-01-02T06:00:00", + "deadline": "2022-01-02T22:00:00" + }, + { + "taskid": 5, + "goalid": "1", + "title": "sleep", + "duration": 2, + "start": "2022-01-02T22:00:00", + "deadline": "2022-01-03T00:00:00" + } + ] + }, + { + "day": "2022-01-03", + "tasks": [ + { + "taskid": 6, + "goalid": "1", + "title": "sleep", + "duration": 6, + "start": "2022-01-03T00:00:00", + "deadline": "2022-01-03T06:00:00" + }, + { + "taskid": 7, + "goalid": "free", + "title": "free", + "duration": 16, + "start": "2022-01-03T06:00:00", + "deadline": "2022-01-03T22:00:00" + }, + { + "taskid": 8, + "goalid": "1", + "title": "sleep", + "duration": 2, + "start": "2022-01-03T22:00:00", + "deadline": "2022-01-04T00:00:00" + } + ] + }, + { + "day": "2022-01-04", + "tasks": [ + { + "taskid": 9, + "goalid": "1", + "title": "sleep", + "duration": 6, + "start": "2022-01-04T00:00:00", + "deadline": "2022-01-04T06:00:00" + }, + { + "taskid": 10, + "goalid": "free", + "title": "free", + "duration": 16, + "start": "2022-01-04T06:00:00", + "deadline": "2022-01-04T22:00:00" + }, + { + "taskid": 11, + "goalid": "1", + "title": "sleep", + "duration": 2, + "start": "2022-01-04T22:00:00", + "deadline": "2022-01-05T00:00:00" + } + ] + }, + { + "day": "2022-01-05", + "tasks": [ + { + "taskid": 12, + "goalid": "1", + "title": "sleep", + "duration": 6, + "start": "2022-01-05T00:00:00", + "deadline": "2022-01-05T06:00:00" + }, + { + "taskid": 13, + "goalid": "free", + "title": "free", + "duration": 16, + "start": "2022-01-05T06:00:00", + "deadline": "2022-01-05T22:00:00" + }, + { + "taskid": 14, + "goalid": "1", + "title": "sleep", + "duration": 2, + "start": "2022-01-05T22:00:00", + "deadline": "2022-01-06T00:00:00" + } + ] + }, + { + "day": "2022-01-06", + "tasks": [ + { + "taskid": 15, + "goalid": "1", + "title": "sleep", + "duration": 6, + "start": "2022-01-06T00:00:00", + "deadline": "2022-01-06T06:00:00" + }, + { + "taskid": 16, + "goalid": "free", + "title": "free", + "duration": 16, + "start": "2022-01-06T06:00:00", + "deadline": "2022-01-06T22:00:00" + }, + { + "taskid": 17, + "goalid": "1", + "title": "sleep", + "duration": 2, + "start": "2022-01-06T22:00:00", + "deadline": "2022-01-07T00:00:00" + } + ] + }, + { + "day": "2022-01-07", + "tasks": [ + { + "taskid": 18, + "goalid": "1", + "title": "sleep", + "duration": 6, + "start": "2022-01-07T00:00:00", + "deadline": "2022-01-07T06:00:00" + }, + { + "taskid": 19, + "goalid": "free", + "title": "free", + "duration": 16, + "start": "2022-01-07T06:00:00", + "deadline": "2022-01-07T22:00:00" + }, + { + "taskid": 20, + "goalid": "1", + "title": "sleep", + "duration": 2, + "start": "2022-01-07T22:00:00", + "deadline": "2022-01-08T00:00:00" + } + ] + }, + { + "day": "2022-01-08", + "tasks": [ + { + "taskid": 21, + "goalid": "1", + "title": "sleep", + "duration": 6, + "start": "2022-01-08T00:00:00", + "deadline": "2022-01-08T06:00:00" + }, + { + "taskid": 22, + "goalid": "free", + "title": "free", + "duration": 16, + "start": "2022-01-08T06:00:00", + "deadline": "2022-01-08T22:00:00" + }, + { + "taskid": 23, + "goalid": "1", + "title": "sleep", + "duration": 2, + "start": "2022-01-08T22:00:00", + "deadline": "2022-01-09T00:00:00" + } + ] + } + ], + "impossible": [] +} \ No newline at end of file diff --git a/tests/jsons/stable/sleep-1/input.json b/tests/jsons/stable/sleep-1/input.json new file mode 100644 index 00000000..ec9b283b --- /dev/null +++ b/tests/jsons/stable/sleep-1/input.json @@ -0,0 +1,31 @@ +{ + "startDate": "2022-01-01T00:00:00", + "endDate": "2022-01-09T00:00:00", + "goals": [ + { + "id": "1", + "title": "sleep", + "start": "2022-01-01T00:00:00", + "deadline": "2022-01-09T00:00:00", + "budget": { + "minPerDay": 8, + "maxPerDay": 8, + "minPerWeek": 56, + "maxPerWeek": 56 + }, + "filters": { + "afterTime": 22, + "beforeTime": 6, + "onDays": [ + "mon", + "tue", + "wed", + "thu", + "fri", + "sat", + "sun" + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/jsons/validated/sleep-1/observed.json b/tests/jsons/stable/sleep-1/observed.json similarity index 91% rename from tests/jsons/validated/sleep-1/observed.json rename to tests/jsons/stable/sleep-1/observed.json index 571c43a2..9dd39b95 100644 --- a/tests/jsons/validated/sleep-1/observed.json +++ b/tests/jsons/stable/sleep-1/observed.json @@ -233,38 +233,5 @@ ] } ], - "impossible": [ - { - "day": "2022-01-01", - "tasks": [] - }, - { - "day": "2022-01-02", - "tasks": [] - }, - { - "day": "2022-01-03", - "tasks": [] - }, - { - "day": "2022-01-04", - "tasks": [] - }, - { - "day": "2022-01-05", - "tasks": [] - }, - { - "day": "2022-01-06", - "tasks": [] - }, - { - "day": "2022-01-07", - "tasks": [] - }, - { - "day": "2022-01-08", - "tasks": [] - } - ] + "impossible": [] } \ No newline at end of file diff --git a/tests/jsons/validated/sleep-1/expected.json b/tests/jsons/validated/sleep-1/expected.json deleted file mode 100644 index e22cb03e..00000000 --- a/tests/jsons/validated/sleep-1/expected.json +++ /dev/null @@ -1,270 +0,0 @@ -{ - "scheduled": [ - { - "day": "2022-01-01", - "tasks": [ - { - "taskid": 0, - "goalid": "1", - "title": "sleep", - "duration": 6, - "start": "2022-01-01T00:00:00", - "deadline": "2022-01-01T06:00:00" - }, - { - "taskid": 1, - "goalid": "free", - "title": "free", - "duration": 16, - "start": "2022-01-01T06:00:00", - "deadline": "2022-01-01T22:00:00" - }, - { - "taskid": 2, - "goalid": "1", - "title": "sleep", - "duration": 2, - "start": "2022-01-01T22:00:00", - "deadline": "2022-01-02T00:00:00" - } - ] - }, - { - "day": "2022-01-02", - "tasks": [ - { - "taskid": 3, - "goalid": "1", - "title": "sleep", - "duration": 6, - "start": "2022-01-02T00:00:00", - "deadline": "2022-01-02T06:00:00" - }, - { - "taskid": 4, - "goalid": "free", - "title": "free", - "duration": 16, - "start": "2022-01-02T06:00:00", - "deadline": "2022-01-02T22:00:00" - }, - { - "taskid": 5, - "goalid": "1", - "title": "sleep", - "duration": 2, - "start": "2022-01-02T22:00:00", - "deadline": "2022-01-03T00:00:00" - } - ] - }, - { - "day": "2022-01-03", - "tasks": [ - { - "taskid": 6, - "goalid": "1", - "title": "sleep", - "duration": 6, - "start": "2022-01-03T00:00:00", - "deadline": "2022-01-03T06:00:00" - }, - { - "taskid": 7, - "goalid": "free", - "title": "free", - "duration": 16, - "start": "2022-01-03T06:00:00", - "deadline": "2022-01-03T22:00:00" - }, - { - "taskid": 8, - "goalid": "1", - "title": "sleep", - "duration": 2, - "start": "2022-01-03T22:00:00", - "deadline": "2022-01-04T00:00:00" - } - ] - }, - { - "day": "2022-01-04", - "tasks": [ - { - "taskid": 9, - "goalid": "1", - "title": "sleep", - "duration": 6, - "start": "2022-01-04T00:00:00", - "deadline": "2022-01-04T06:00:00" - }, - { - "taskid": 10, - "goalid": "free", - "title": "free", - "duration": 16, - "start": "2022-01-04T06:00:00", - "deadline": "2022-01-04T22:00:00" - }, - { - "taskid": 11, - "goalid": "1", - "title": "sleep", - "duration": 2, - "start": "2022-01-04T22:00:00", - "deadline": "2022-01-05T00:00:00" - } - ] - }, - { - "day": "2022-01-05", - "tasks": [ - { - "taskid": 12, - "goalid": "1", - "title": "sleep", - "duration": 6, - "start": "2022-01-05T00:00:00", - "deadline": "2022-01-05T06:00:00" - }, - { - "taskid": 13, - "goalid": "free", - "title": "free", - "duration": 16, - "start": "2022-01-05T06:00:00", - "deadline": "2022-01-05T22:00:00" - }, - { - "taskid": 14, - "goalid": "1", - "title": "sleep", - "duration": 2, - "start": "2022-01-05T22:00:00", - "deadline": "2022-01-06T00:00:00" - } - ] - }, - { - "day": "2022-01-06", - "tasks": [ - { - "taskid": 15, - "goalid": "1", - "title": "sleep", - "duration": 6, - "start": "2022-01-06T00:00:00", - "deadline": "2022-01-06T06:00:00" - }, - { - "taskid": 16, - "goalid": "free", - "title": "free", - "duration": 16, - "start": "2022-01-06T06:00:00", - "deadline": "2022-01-06T22:00:00" - }, - { - "taskid": 17, - "goalid": "1", - "title": "sleep", - "duration": 2, - "start": "2022-01-06T22:00:00", - "deadline": "2022-01-07T00:00:00" - } - ] - }, - { - "day": "2022-01-07", - "tasks": [ - { - "taskid": 18, - "goalid": "1", - "title": "sleep", - "duration": 6, - "start": "2022-01-07T00:00:00", - "deadline": "2022-01-07T06:00:00" - }, - { - "taskid": 19, - "goalid": "free", - "title": "free", - "duration": 16, - "start": "2022-01-07T06:00:00", - "deadline": "2022-01-07T22:00:00" - }, - { - "taskid": 20, - "goalid": "1", - "title": "sleep", - "duration": 2, - "start": "2022-01-07T22:00:00", - "deadline": "2022-01-08T00:00:00" - } - ] - }, - { - "day": "2022-01-08", - "tasks": [ - { - "taskid": 21, - "goalid": "1", - "title": "sleep", - "duration": 6, - "start": "2022-01-08T00:00:00", - "deadline": "2022-01-08T06:00:00" - }, - { - "taskid": 22, - "goalid": "free", - "title": "free", - "duration": 16, - "start": "2022-01-08T06:00:00", - "deadline": "2022-01-08T22:00:00" - }, - { - "taskid": 23, - "goalid": "1", - "title": "sleep", - "duration": 2, - "start": "2022-01-08T22:00:00", - "deadline": "2022-01-09T00:00:00" - } - ] - } - ], - "impossible": [ - { - "day": "2022-01-01", - "tasks": [] - }, - { - "day": "2022-01-02", - "tasks": [] - }, - { - "day": "2022-01-03", - "tasks": [] - }, - { - "day": "2022-01-04", - "tasks": [] - }, - { - "day": "2022-01-05", - "tasks": [] - }, - { - "day": "2022-01-06", - "tasks": [] - }, - { - "day": "2022-01-07", - "tasks": [] - }, - { - "day": "2022-01-08", - "tasks": [] - } - ] - } \ No newline at end of file diff --git a/tests/jsons/validated/sleep-1/input.json b/tests/jsons/validated/sleep-1/input.json deleted file mode 100644 index c572ab07..00000000 --- a/tests/jsons/validated/sleep-1/input.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "startDate": "2022-01-01T00:00:00", - "endDate": "2022-01-09T00:00:00", - "goals": { - "1": { - "id": "1", - "title": "sleep", - "budget": { - "minPerDay": 8, - "maxPerDay": 8, - "minPerWeek": 56, - "maxPerWeek": 56 - }, - "filters": { - "after_time": 22, - "before_time": 6 - } - } - } -} \ No newline at end of file diff --git a/todos.md b/todos.md deleted file mode 100644 index c4394d87..00000000 --- a/todos.md +++ /dev/null @@ -1,17 +0,0 @@ -# Open - -- [ ] TimeFilter should not be usize. NaiveDateTime instead -- [ ] Why has Slot an End Date? Shouldn't it always be 1h after start_date -- [ ] normalize input dates of goal to the full hour - - -# Closed - -- [x] Change GoalMap to HashMap - premature optimization is the root of all evil -- [x] Input struct remove calendar from field name and use start_date and end_date instead -- [x] sort Repetition in a nice way - -# Abendoned - - - diff --git a/utilities/rename_inputs.sh b/utilities/rename_inputs.sh deleted file mode 100755 index bacb015c..00000000 --- a/utilities/rename_inputs.sh +++ /dev/null @@ -1,6 +0,0 @@ -#! /bin/bash - -for d in */ ; do - # echo $(ls $d) - mv "$d/input2.json" "$d/input.json" -done; \ No newline at end of file diff --git a/utilities/rename_outputs.sh b/utilities/rename_outputs.sh deleted file mode 100755 index 42dec941..00000000 --- a/utilities/rename_outputs.sh +++ /dev/null @@ -1,6 +0,0 @@ -#! /bin/bash - -for d in */ ; do - # echo $(ls $d) - mv "$d/output2.json" "$d/output.json" -done; \ No newline at end of file