From b8d343e0ded8b39eee8a8295675a26c7677ac319 Mon Sep 17 00:00:00 2001
From: San Jacobs
Date: Tue, 5 Nov 2024 14:36:54 +0100
Subject: Init

---
 build.sh  |   1 +
 main.odin |  45 ++++
 time.odin | 853 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 899 insertions(+)
 create mode 100755 build.sh
 create mode 100644 main.odin
 create mode 100644 time.odin

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
-- 
cgit v1.2.1