diff options
author | San Jacobs | 2024-11-05 14:36:54 +0100 |
---|---|---|
committer | San Jacobs | 2024-11-05 14:36:54 +0100 |
commit | b8d343e0ded8b39eee8a8295675a26c7677ac319 (patch) | |
tree | 7ca2c9f9c26401df645ff7a3518196e3ce723f2a | |
download | statics-master.tar.gz statics-master.tar.bz2 statics-master.zip |
-rwxr-xr-x | build.sh | 1 | ||||
-rw-r--r-- | main.odin | 45 | ||||
-rw-r--r-- | time.odin | 853 |
3 files changed, 899 insertions, 0 deletions
diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..d50604a --- /dev/null +++ b/build.sh @@ -0,0 +1 @@ +odin run . diff --git a/main.odin b/main.odin new file mode 100644 index 0000000..da72be7 --- /dev/null +++ b/main.odin @@ -0,0 +1,45 @@ +package main + +import "core:fmt" +import "core:os" + +dayrate : f64 = 3500 + +main :: proc() { + + arg_count := len(os.args)-1 + + total_hours : f64 = 0; + for i in 1..=arg_count { + + fmt.printf("%d: ", i) + + timeblocks, ok := importICS(os.args[i]) + + if ok { + hours : f64 = 0 + for each_block in timeblocks { + hours += f64(hourcount(each_block)) + } + minutes := int(f64(hours-f64(int(hours)))*60.0) + fmt.println(os.args[i]) + fmt.printf(" Hour count: %f\nHours & Minutes: %02d:%02d\n\n", + hours, int(hours), minutes) + total_hours += hours + } else { + // Noffin i guess + fmt.printf("\n\n") + } + } + + fmt.printf("\nTOTAL\n\n") + + total_final_hour_fraction : f64 = total_hours - f64(int(total_hours)); + + total_minutes := int(total_final_hour_fraction*60.0) + + fmt.printf(" Hour count: %f\nHours & Minutes: %02d:%02d\n", + total_hours, int(total_hours), total_minutes) + + return +} diff --git a/time.odin b/time.odin new file mode 100644 index 0000000..9bb06be --- /dev/null +++ b/time.odin @@ -0,0 +1,853 @@ +package main + +import math "core:math" +import "core:fmt" +import "core:os" +import "core:strings" +import "core:strconv" +import "core:slice" +import "core:sort" + +// +// --- STRUCTURES --- +// + +Weekday :: enum{ + Monday, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday, + Sunday, +} + +Delta :: struct { + minutes : int, + hours : int, + days : int, +} + +Moment :: struct { + minutes : int, + hours : int, + day : int, + month : int, + year : int, +} + +Timeblock :: struct { + start : Moment, + end : Moment, + value : f32, + reason : string, +} + +Fractionpair :: struct { + start : f32, + end : f32, +} + + +Workday :: struct { + call : Moment, + wrap : Moment, + planned_wrap : Moment, + + // blocks is over 12, + // because lunch breaks + // cause more blocks + blocks : [16]Timeblock, + + // Fractions store how long + // since the workday's + // preceding midnight + // a timesplit occurs. + // They're pairs so they + // can exactly map to each + // timeblock's start and end + fractions : [16]Fractionpair, + total_timeblocks : int, + price: f32, +} + + +// +// --- MAJOR PROCEDURES --- +// + +new_workday :: proc(previous_wrap : Moment, + calltime : Moment, + wraptime : Moment, + planned_wraptime : Moment) -> (workday: Workday) { + + workday.call = calltime + workday.wrap = wraptime + workday.planned_wrap = planned_wraptime + + using workday + + initial_block: Timeblock = {call, + // Paragraph 6.7 says that up to 2 hours of unused warned overtime counts as worktime, + // though so that at least one hour of the unused overtime is not counted. + // (It's unclear if an 8-hour day that ends 3 hours in counts as having 5 hours of unused overtime) + time_max(time_clamp(sub(planned_wrap, {0, 1, 0}), wrap, add(wrap, {0, 2, 0})), + add(call, {0, 4, 0})), 1, ""} + // ^ Minimum 4 hour day ^ + + // BUG: I think this is causing a bug where if the day ends at 6 and gets extended, + // it doesn't split the day at 6 and set the work-time before that to be extra expensive + + sp_length :: 11 + splitpoints:= [sp_length]Moment{ // --$-- Points where the price may change --$-- // + + // TODO: Replace this terribleness with a system for parsing simple, user-editable rulseset-files + + add(previous_wrap, {0, 10, 0}), // Sleepbreach, 10 hours after previous wrap, aka. turnover + {0, 5, call.day, call.month, call.year}, // 2 hours before 7, aka 5 + {0, 6, call.day, call.month, call.year}, // 6 in the morning + add(call, {0, 8, 0}), // Normal 8 hours of work + add(call, {0, 9, 0}), // 1st hour of overtime is over + add(call, {0, 11, 0}), // 3rd hour of overtime is over + planned_wraptime, // End of warned overtime + add(call, {0, 14, 0}), // The 14-hour mark + {0, 22, call.day, call.month, call.year}, // 22:00 in the evening + add({0, 23, call.day, call.month, call.year}, {0, 1, 0}), // Midnight + add({0, 23, call.day, call.month, call.year}, {0, 7, 0}), // 06:00 the next morning + } + + // Eliminate planned wrap, if it occurs within normal 8-hour period. + // This is to make sure the first period of time becomes a pure 8 hours, + // which makes detecting the main section of the workday easier. + if sortable(splitpoints[6]) < sortable(splitpoints[3]) { + splitpoints[6] = splitpoints[3]; + } + + splitpoints_sorted: [sp_length]Moment = splitpoints + slice.sort_by(splitpoints_sorted[:], lessMoment) + + for each_point, i in splitpoints_sorted { + fmt.printf("Splitpoint %2i: %s\n", i+1, toString(each_point)) + } + + working_block: Timeblock = initial_block + fmt.println("working_block: ", toString(working_block)) + + j: int = 0 + for each_point in splitpoints_sorted { + // If each splitpoint moment is within the workday, and is not equal to the start of the current block + if sortable(each_point) > sortable(call) && // vvvvvvvvvvvvvvvvvvvvvvvv this complication is here to cover + sortable(each_point) < sortable(time_max(wrap, add(call, {0, 4, 0}))) && // for the 4-hour minimum day. + each_point != working_block.start { + + blocks[j], working_block = timesplit(working_block, each_point) + j += 1 + //fmt.println("Split and wrote:", j) + } + } + blocks[j] = working_block + j += 1 + total_timeblocks = j + + // This line is commented out because it shouldn't be needed. + //slice.sort_by(blocks[:], lessTimeblock) + + + fmt.println(total_timeblocks) + + for block, i in &blocks { + if i >= total_timeblocks do break + + //using Weekday + + if lessEq(block.end, splitpoints[0]) do upvalue(&block, 3, "Sleep-breach") // +200% for sleep-breach + if block.start.hours >= 22 do upvalue(&block, 2, "Night") // Work at night, aka. between 22:00 and 06:00 + if (block.end.hours == 6 && block.end.minutes == 0) || block.end.hours <= 5 do upvalue(&block, 2, "Night") // is +100% + if greatEq(block.start, splitpoints[3]) { + upvalue(&block, 1.5, "Overtime") + if getweekday(block.start) == .Saturday do upvalue(&block, 2, "Saturday Overtime") + } + if greatEq(block.start, splitpoints[5]) do upvalue(&block, 2, "Overtime") // End of 3-hour cheap planned overtime + if greatEq(block.start, planned_wrap) && greatEq(block.start, splitpoints[4]) do upvalue(&block, 2, "Overtime") // Unwarned OT + if greatEq(block.start, splitpoints[7]) do upvalue(&block, 3, "Far overtime") // +200% beyond 14th hours is +100% + if getweekday(block.start) == .Saturday do upvalue(&block, 1.5, "Saturday") // Saturdays are +50% + if getweekday(block.start) == .Sunday do upvalue(&block, 2, "Sunday") // Sundays are +100% + + if !(less(call, Moment{0, 7, call.day, call.month, call.year}) && + less(time_min(add(call, Delta{0,8,0}), wrap), Moment{0, 17, call.day, call.month, call.year} )) { + // This was added for rule 6.11c, but in a world without a defined normal workday, + // that rule is already covered already by 6.11g, so this is empty. + } + + + // Holidays! + if (block.start.day==1) && (block.start.month==1) { upvalue(&block, 2, "New year"); continue} + if (block.start.day==1) && (block.start.month==5) { upvalue(&block, 2, "1st of May"); continue} + if (block.start.day==17) && (block.start.month==5) { upvalue(&block, 2, "17th of May"); continue} + if (block.start.day==25 || block.start.day==26) && block.start.month==12 { upvalue(&block, 2, "Christmas"); continue} + easter: Moment = gaussEaster(block.start.year) + if (block.start.day == sub(easter, {0,0,3}).day) && block.start.month == sub(easter, {0,0,3}).month { upvalue(&block, 2, "Maundy Thursday"); continue} + if (block.start.day == sub(easter, {0,0,2}).day) && block.start.month == sub(easter, {0,0,2}).month { upvalue(&block, 2, "Good Friday"); continue} + if (block.start.day == easter.day) && (block.start.month == easter.month) { upvalue(&block, 2, "Easter"); continue} + if (block.start.day == add(easter, {0,0,1}).day) && (block.start.month == add(easter, {0,0,1}).month) { upvalue(&block, 2, "Easter"); continue} + if (block.start.day == add(easter, {0,0,39}).day) && (block.start.month == add(easter, {0,0,39}).month) { upvalue(&block, 2, "Feast of the Ascension"); continue} + if (block.start.day == add(easter, {0,0,49}).day) && (block.start.month == add(easter, {0,0,49}).month) { upvalue(&block, 2, "Pentecost"); continue} + if (block.start.day == add(easter, {0,0,50}).day) && (block.start.month == add(easter, {0,0,50}).month) { upvalue(&block, 2, "Pentecost Monday"); continue} + } + + for each_block, i in blocks { + fmt.printf("Block %2i: %s $f: %i%% %s\n", i+1, toString(each_block), int((each_block.value-1)*100), each_block.reason) + price += f32(f64(dayrate/7.5) * f64(hourcount(each_block)) * f64(each_block.value)) + } + + return +} + +lunch :: proc(workday: ^Workday, lunch_start: Moment, lunch_end: Moment) { + + // + // This basically cuts out part of the workday + // + // |-------|---|-------|----|---------|--------------| + // |--lunch--| + // |-------|---|----| |-------|--------------| + // This ^ works now! + + if lunch_start == lunch_end do return + assert(less(lunch_start, lunch_end), "ERROR: Bad Lunch! Lunch ends before it starts") + + start_index: int + end_index: int + for block, i in workday.blocks { + if (great(lunch_start, block.start) && less(lunch_start, block.end)) || (block.start == lunch_start) { + start_index = i + } + if (great(lunch_end, block.start) && less(lunch_end, block.end)) || (block.end == lunch_end) { + end_index = i + } + } + + assert(start_index <= end_index, "ERROR: Bad Lunch! start_index greater than end_index") + + span: int = end_index - start_index + + // TODO: This is bad and can definitely be simplified and done in a more principled way + // But right now it works perfectly, and is much better than it used to be in the C++ version + switch span { + case 0: + fmt.println("Start and end are in the same block") + switch { + case (lunch_start == workday.blocks[start_index].start) && (lunch_end == workday.blocks[end_index].end): + popBlock(workday, start_index) + case lunch_start == workday.blocks[start_index].start: + workday.blocks[start_index].start = lunch_end + case lunch_end == workday.blocks[end_index].end: + workday.blocks[end_index].end = lunch_start + case: + growBlocks(workday, start_index) + end_index += 1 + workday.blocks[start_index].end = lunch_start + workday.blocks[end_index].start = lunch_end + } + + case 1: + fmt.println("Start and end span one gap") + switch { + case (lunch_start == workday.blocks[start_index].start) && (lunch_end == workday.blocks[end_index].end): + popBlock(workday, start_index, 2) + case lunch_start == workday.blocks[start_index].start: + workday.blocks[end_index].start = lunch_end + popBlock(workday, start_index) + case lunch_end == workday.blocks[end_index].end: + workday.blocks[start_index].end = lunch_start + popBlock(workday, end_index) + case: + workday.blocks[end_index].start = lunch_end + workday.blocks[start_index].end = lunch_start + } + + case 2..=len(workday.blocks): + fmt.println("Start and end span more than one gap") + switch { + case (lunch_start == workday.blocks[start_index].start) && (lunch_end == workday.blocks[end_index].end): + popBlock(workday, start_index, span+1) + case lunch_start == workday.blocks[start_index].start: + workday.blocks[end_index].start = lunch_end + popBlock(workday, start_index, span) + case lunch_end == workday.blocks[end_index].end: + workday.blocks[start_index].end = lunch_start + popBlock(workday, start_index+1, span) + case: + workday.blocks[end_index].start = lunch_end + workday.blocks[start_index].end = lunch_start + popBlock(workday, start_index+1, span-1) + } + } +} + +windIndividual :: proc(input_moment: ^Moment, + minutes: int, + hours: int, + days: int) { + + // Adding minutes + input_moment.minutes += minutes + for input_moment.minutes > 59 { + input_moment.minutes -= 60 + input_moment.hours += 1 + } + for input_moment.minutes < 0 { + input_moment.minutes += 60 + input_moment.hours -= 1 + } + + // Adding hours + input_moment.hours += hours + for input_moment.hours > 23 { + input_moment.hours -= 24 + input_moment.day += 1 + } + for input_moment.hours < 0 { + input_moment.hours += 24 + input_moment.day -= 1 + } + + // Adding days + input_moment.day += days + current_month_length: int = days_in(input_moment.month, input_moment.year) + + for input_moment.day > current_month_length { + input_moment.day -= current_month_length + input_moment.month += 1 + if input_moment.month > 12 { + input_moment.month -= 12 + input_moment.year += 1 + } + current_month_length = days_in(input_moment.month, input_moment.year) + } + + for input_moment.day < 1 { + input_moment.month -= 1 + if input_moment.month < 1 { + input_moment.month += 12 + input_moment.year -= 1 + } + current_month_length = days_in(input_moment.month, input_moment.year) + input_moment.day += current_month_length + } + + return +} +windByDelta :: proc(moment: ^Moment, delta: Delta) { + using delta + wind(moment, minutes, hours, days) + return +} +wind :: proc{windIndividual, windByDelta} + +timesplit :: proc(block: Timeblock, splitpoint: Moment) -> (first_half: Timeblock, second_half: Timeblock) { + // Splits a timeblock at splitpoint. + + if sortable(splitpoint) < sortable(block.start) || + sortable(splitpoint) > sortable(block.end) || + splitpoint == block.start || splitpoint == block.end { + fmt.println("WHOOPS: Splitpoint is outside timeblock range!") + fmt.println("Timeblock:", toString(block)) + fmt.println("Splitpoint:", toString(splitpoint)) + second_half = block + return + } + + first_half = {block.start, splitpoint, block.value, block.reason} + second_half = {splitpoint, block.end, block.value, block.reason} + + return +} + +upvalue :: proc(input_block: ^Timeblock, value: f32, reason: string) { + block: ^Timeblock = input_block + if value > block.value { + block.value = value + block.reason = reason + } +} + +importICS :: proc(path: string, verbose: bool = false) -> ([dynamic]Timeblock, bool) { + output: [dynamic]Timeblock + + c : Timeblock + + raw, ok := os.read_entire_file_from_filename(path) + content := string(raw) + + i := 1 + line_nr := 1 + + if !ok { + // TODO: Actually check the content to see if it is an ICS file. + fmt.eprintf("ERROR: No file found at: \"%v\"", path) + return output, false + } + + for line in strings.split_lines_iterator(&content) { + + // BUG: This assumes that there will never be a line shorter than 10. + // That means this will try reading out of bounds at some point. + + if line[0:10]=="DTSTART;TZ" { + // grab the timestamp from the end of the line, and set start to it + + ll := len(line) + if verbose do fmt.println("Found a DTSTART!") + if verbose do fmt.println("length of line:", ll) + if verbose do fmt.println(line) + date_start : int + + if verbose do fmt.printf("Time: %s:%s\n", line[ll-6:ll-4], line[ll-4:ll-2]) + + if verbose do fmt.printf("Hours: %s\n", line[ll-6:ll-4]) + c.start.hours = strconv.atoi(line[ll-6:ll-4]) + + if verbose do fmt.printf("Minutes: %s\n", line[ll-4:ll-2]) + c.start.minutes = strconv.atoi(line[ll-4:ll-2]) + + if verbose do fmt.printf("Day: %s\n", line[ll-9:ll-7]) + c.start.day = strconv.atoi(line[ll-9:ll-7]) + + if verbose do fmt.printf("Month: %s\n", line[ll-11:ll-9]) + c.start.month = strconv.atoi(line[ll-11:ll-9]) + + if verbose do fmt.printf("Year: %s\n", line[ll-15:ll-11]) + c.start.year = strconv.atoi(line[ll-15:ll-11]) + } + if line[0:5]=="DTEND" { + // grab the timestamp from the end of the line, and set end to it + ll := len(line) + if verbose do fmt.println("Found a DTEND!") + if verbose do fmt.println(line) + + + if verbose do fmt.printf("Time: %s:%s\n", line[ll-6:ll-4], line[ll-4:ll-2]) + + if verbose do fmt.printf("Hours: %s\n", line[ll-6:ll-4]) + c.end.hours = strconv.atoi(line[ll-6:ll-4]) + + if verbose do fmt.printf("Minutes: %s\n", line[ll-4:ll-2]) + c.end.minutes = strconv.atoi(line[ll-4:ll-2]) + + if verbose do fmt.printf("Day: %s\n", line[ll-9:ll-7]) + c.end.day = strconv.atoi(line[ll-9:ll-7]) + + if verbose do fmt.printf("Month: %s\n", line[ll-11:ll-9]) + c.end.month = strconv.atoi(line[ll-11:ll-9]) + + if verbose do fmt.printf("Year: %s\n", line[ll-15:ll-11]) + c.end.year = strconv.atoi(line[ll-15:ll-11]) + + } + + // TODO: This is checking if the years are 0 to make sure it hasn't read from + // from a line containing "DTSTART;VALUE" instead of "DTSTART;TZID" + // VALUE days are events that are set to last the entire day, + // as opposed to having a defined start and end point. + // + // This should eventually not be needed, because these days + // should also be imported based on the session's default-day settings + + if line=="END:VEVENT" && (c.end.year != 0) && (c.start.year != 0) { + if verbose do fmt.println(line) + c.value = 1 + append(&output, c) + + blank_timeblock: Timeblock + c = blank_timeblock + + if verbose do fmt.println("\n\n", i, line_nr, "\n\n") + i += 1 + } + line_nr += 1 + } + return output, true +} + + +// +// --- BASIC OPERATIONS --- +// + +add :: proc(moment: Moment, delta: Delta) -> (output: Moment) { + output = moment + wind(&output, delta) + return +} +sub :: proc(moment: Moment, delta: Delta) -> (output: Moment) { + output = moment + using delta + wind(&output, minutes*-1, hours*-1, days*-1) + return +} +maxMoment :: proc(moment_a: Moment, moment_b: Moment) -> Moment { + if sortable(moment_a) > sortable(moment_b) do return moment_a + return moment_b +} +maxDelta :: proc(delta_a: Delta, delta_b: Delta) -> Delta { + if sortable(delta_a) > sortable(delta_b) do return delta_a + return delta_b +} +time_max :: proc{maxDelta, maxMoment} + +minMoment :: proc(moment_a: Moment, moment_b: Moment) -> Moment { + if sortable(moment_a) < sortable(moment_b) do return moment_a + return moment_b +} +minDelta :: proc(delta_a: Delta, delta_b: Delta) -> Delta { + if sortable(delta_a) < sortable(delta_b) do return delta_a + return delta_b +} +time_min :: proc{minDelta, minMoment} + +clampMoment :: proc(moment: Moment, moment_min: Moment, moment_max: Moment) -> Moment { + return time_min(time_max(moment, moment_min), moment_max) +} +clampDelta :: proc(delta: Delta, delta_min: Delta, delta_max: Delta) -> Delta { + return time_min(time_max(delta, delta_min), delta_max) +} +time_clamp :: proc{clampMoment, clampDelta} + +greatMoment :: proc(moment_a: Moment, moment_b: Moment) -> bool { + return bool(sortable(moment_a) > sortable(moment_b)) +} +greatDelta :: proc(delta_a: Delta, delta_b: Delta) -> bool { + return bool(sortable(delta_a) > sortable(delta_b)) +} +great :: proc{greatMoment, greatDelta} + +lessMoment :: proc(moment_a: Moment, moment_b: Moment) -> bool { + return bool(sortable(moment_a) < sortable(moment_b)) +} +lessDelta :: proc(delta_a: Delta, delta_b: Delta) -> bool { + return bool(sortable(delta_a) < sortable(delta_b)) +} +lessTimeblock :: proc(block_a: Timeblock, block_b: Timeblock) -> bool { + if block_b.start == {0, 0, 0, 0, 0} do return true + if block_a.start == {0, 0, 0, 0, 0} do return false + return bool(sortable(block_a.start) < sortable(block_b.start)) +} +lessWorkday :: proc(day_a: Workday, day_b: Workday) -> bool { + return bool(sortable(day_a.call) < sortable(day_b.call)) +} +lessWorkdayPtr :: proc(day_a: ^Workday, day_b: ^Workday) -> bool { + return bool(sortable(day_a.call) < sortable(day_b.call)) +} +less :: proc{lessMoment, lessDelta, lessTimeblock, lessWorkday} + +lessEqMoment :: proc(moment_a: Moment, moment_b: Moment) -> bool { + return moment_a==moment_b || less(moment_a, moment_b) +} +lessEq :: proc{lessEqMoment} + +greatEqMoment :: proc(moment_a: Moment, moment_b: Moment) -> bool { + return moment_a == moment_b || great(moment_a, moment_b) +} +greatEq :: proc{greatEqMoment} + + +diff :: proc(moment_a: Moment, moment_b: Moment) -> (acc: Delta) { + // FIXME: This seems to cause either infinite loops or crashes sometimes + + // Uses what I call an accumulator-decumulator design + // Count how long it takes to approach a benchmark, + // and that count is the difference + + acc = {0, 0, 0} + if moment_a == moment_b do return + + // smallest operand becomes benchmark to approach + reverse: bool = sortable(moment_a) < sortable(moment_b) + bench : Moment + dec : Moment + if reverse { + bench = moment_a + dec = moment_b + } else { + bench = moment_b + dec = moment_a + } + + // It is possible to write something that does this in months at a time, instead of days, + // which would be faster, but I am not expecting to have to do this with such + // long periods of time, so screw that. + for ((dec.year - bench.year) > 1 || + (dec.month - bench.month) > 1 || + (dec.day - bench.day) > 1) { + wind(&dec, 0, 0, -1) + acc.days += 1 + } + + for (dec.hours - bench.hours > 1) { + wind(&dec, 0, -1, 0) + acc.hours += 1 + } + for acc.hours > 23 { + acc.hours -= 24 + acc.days += 1 + } + + for dec != bench { + wind(&dec, -1, 0, 0) + acc.minutes += 1 + } + for acc.minutes > 59 { + acc.minutes -= 60 + acc.hours += 1 + } + + // Repeating this is a little bit ugly, but it works + for acc.hours > 23 { + acc.hours -= 24 + acc.days += 1 + } + + return +} + +sortableTimeDelta :: proc(delta: Delta) -> (output: u64) { + using delta + output, _ = strconv.parse_u64(fmt.tprintf("1%3i%2i%2i", days, hours, minutes)) + return +} +sortableTimeMoment :: proc(moment: Moment) -> (output: u64) { + using moment + output, _ = strconv.parse_u64(fmt.tprintf("%4i%2i%2i%2i%2i", year, month, day, hours, minutes)) + return +} +sortable :: proc{sortableTimeMoment, sortableTimeDelta} + +deltaToString :: proc(delta: Delta) -> (output: string) { + using delta + + if hours == 0 && + days == 0 && + minutes == 0 { + return "None" + } + + cat_array : [dynamic]string + printed_prev : bool = false + + if days>0 { + buf: [5]byte + append(&cat_array, fmt.tprint(days)) + if days < 2 { + append(&cat_array, " day") + } else { + append(&cat_array, " days") + } + printed_prev = true + } + + if hours>0 { + + if printed_prev do append(&cat_array, ", ") + + buf: [5]byte + append(&cat_array, fmt.tprint(hours)) + if hours < 2 { + append(&cat_array, " hour") + } else { + append(&cat_array, " hours") + } + printed_prev = true + } + + if minutes>0 { + + if printed_prev do append(&cat_array, ", ") + + buf: [5]byte + append(&cat_array, fmt.tprint(minutes)) + if minutes < 2 { + append(&cat_array, " minute") + } else { + append(&cat_array, " minutes") + } + } + + output = strings.concatenate(cat_array[:]) + return +} + + +momentToString :: proc(moment: Moment) -> (output: string) { + using moment + + cat_array: [dynamic]string + + output = fmt.tprintf("%4i-%2i-%2i %2i:%2i", year, month, day, hours, minutes) + + return +} +timeblockToString :: proc(block: Timeblock) -> (output: string) { + using block + s: [3]string = {toString(start), " -> ", toString(end)} + output = strings.concatenate(s[:]) + return +} +toString :: proc{deltaToString, momentToString, timeblockToString} + + + +clockprintMoment :: proc(moment: Moment) -> string { + using moment + return fmt.tprintf("%2i:%2i", hours, minutes) +} +clockprintTimeblock :: proc(block: Timeblock) -> string { + using block + return fmt.tprintf("%s -> %s", clockprint(start), clockprint(end)) +} +clockprint :: proc{clockprintTimeblock, clockprintMoment} + +dayprintMoment :: proc(moment: Moment) -> string { + using moment + return fmt.tprintf("%4i-%2i-%2i", year, month, day) +} +dayprintTimeblock :: proc(block: Timeblock) -> string { + using block + return fmt.tprintf("%s -> %s", dayprint(start), dayprint(end)) +} +dayprint :: proc{dayprintTimeblock, dayprintMoment} + +popBlock :: proc(workday: ^Workday, index: int, count: int = 1) { + using workday + when ODIN_DEBUG do fmt.printf("popBlock() running to remove %i block(s) from index %i\n", count, index) + for i in index..<len(blocks)-count { + when ODIN_DEBUG do fmt.printf("Putting the contents of %i/%i into %i\n", i+count, len(blocks)-1, i) + blocks[i] = blocks[i+count] + } + for i in len(blocks)-count-1..<len(blocks) { + blocks[i] = {{0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, 0, ""} + } + total_timeblocks -= count +} + +growBlocks :: proc(workday: ^Workday, index: int, count: int = 1) { + using workday + fmt.printf("growBlocks() running to make space for %i block(s) at index %i\n", count, index) + for i: int = len(blocks)-1-count; i>=index; i-=1 { + fmt.printf("Putting the contents of %i/%i into %i\n", i+count, len(blocks)-1, i) + blocks[i+count] = blocks[i] + } + //for i in index..<index+count { + // blocks[i] = {{0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, 0, ""} + //} + total_timeblocks += count +} + +getweekday :: proc(moment: Moment) -> Weekday { + y: int = moment.year + t: []int = { 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 } + y -= int(moment.month < 3) + return Weekday((y + y / 4 - y / 100 + y / 400 + t[moment.month - 1] + moment.day - 1) % 7) +} + +hourcount :: proc(block: Timeblock) -> f32 { + using block + delta: Delta = diff(end, start) + using delta + return f32(f32(minutes)/60 + + f32(hours) + + f32(days) * 24) +} + +daycount :: proc(delta: Delta) -> f32 { + using delta + assert(delta != {0,0,0}) + return f32(f32(minutes)/60/24 + + f32(hours)/24 + + f32(days) ) +} + +days_in :: proc(month: int, year: int) -> int { + switch month { + case 1: + return 31; + case 2: + if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)){ + return 29; + } + return 28; + case 3: + return 31; + case 4: + return 30; + case 5: + return 31; + case 6: + return 30; + case 7: + return 31; + case 8: + return 31; + case 9: + return 30; + case 10: + return 31; + case 11: + return 30; + case 12: + return 31; + } + fmt.printf("You just found month nr: %i. Something is very wrong.\n", month) + fmt.assertf(month < 13 && month > 0, "You tried to get the days in month %i!\n", month) + return 30 +} + +gaussEaster :: proc(year: int) -> Moment { + // Thanks to Carl Friedrich Gauss for the algorythm + // Thanks rahulhegde97, bansal_rtk_, code_hunt, sanjoy_62, simranarora5sos + // and aashutoshparoha on GeeksForGeeks for the implementation I based this on. + A, B, C, P, Q, M, N, D, E: f64 + easter_month: int = 0 + easter_day: int = 0 + + A = f64(year % 19) + B = f64(year % 4) + C = f64(year % 7) + P = f64(math.floor(f64(year / 100.0))) + + Q = math.floor((13 + 8 * P) / 25.0) + + M = f64(int(15 - Q + P - math.floor(f64(P / 4))) % 30) + + N = f64(int(4 + P - math.floor(P / 4)) % 7) + + D = f64(int(19 * A + M) % 30) + + E = f64(int(2 * B + 4 * C + 6 * D + N) % 7) + + days: int = int(22 + D + E) + easter_day = days + + if (D == 29) && (E == 6) { + // A corner case when D is 29 + easter_month = 4 + easter_day = 19 + } else if (D == 28) && (E == 6) { + // Another corner case, when D is 28 + easter_month = 4 + easter_day = 18 + } else { + // If days > 31, move to April + // April = 4th Month + if (days > 31) { + easter_month = 04 + easter_day = days-31 + } else { + // Otherwise, stay on March + // March = 3rd Month + easter_month = 03 + } + } + + return {0, 0, easter_day, easter_month, year} +}
\ No newline at end of file |