(* CSCE 314, Homework 1, Provided Code *)

(* You might choose to uncomment these, like the lecture code does *)
(* #utop_prompt_dummy
   let _ = UTop.set_show_box false *)

(* Use these functions to extract parts of a date *)
let fst3 (x,_,_) = x (* gets the first element of a triple *)
let snd3 (_,x,_) = x (* gets the second element of a triple *)
let thd3 (_,_,x) = x (* gets the third element of a triple *)

(**
 * TODO: Complete the 12 function bindings described in the assignment.  For the first (2), 
 * we have given you the correct first line and an incorrect function body.
 *)

(*You will write 12 OCaml functions (and tests for them) related to calendar dates.
In all problems, a “date” is an OCaml value of type int * int * int, where the first part is the day, the
second part is the month, and the third part is the year. For example, (15, 1, 2021) represents the 15th
day of the first month (January) in the year 2021.
A “reasonable” date has a positive year, a month between 1 and 12, and a day no greater than 31 (or less
depending on the month). Your solutions need to work correctly only for reasonable dates, but do not check
for reasonable dates (that is a challenge problem) and many of your functions will naturally work correctly
for some non-reasonable dates.
A “day of year” is a number from 1 to 365 where, for example, 33 represents February 2. (We ignore leap
years except in one challenge problem.)
To access elements of an date, use the provided functions fst3, snd3, and thd3 to access the first, second,
and third components accordingly.*)

(*Solutions should be:
Correct
In good style, including indentation and line breaks
Written using features discussed in class. In particular, you must not use OCaml’s mutable references
or arrays. (Why would you?) Also do not use pattern-matching; it is the focus of the next assignment.*)

(*Here is a “cheatsheet” of library functions and operations that you may find helpful when completing your
solution, in addtion to using the provided fst3, snd3, and thd3.
List.hd (* get the head of the list *)
List.tl (* get the tail of the list *)
@ (* infix operator to append two lists *)
^ (* infix operator to concatenate two strings *)
string_of_int (* convert an integer into a string *)
Option.get (* if the argument = Some value, return value, otherwise raise an exception *)
Option.is_some (* return true if the argument = Some value *)
Option.is_none (* return true if the argument = None *)
3*)

(* 1 *) (* DONE *)
(*Write a function is_older that takes two dates and evaluates to true or false. It evaluates to true if
the first argument is a date that comes before the second argument. (If the two dates are the same,
the result is false.)*)
(*val is_older : (int * int * int) * (int * int * int) -> bool*)
let is_older ((date1 : int * int * int), (date2 : int * int * int)) =

  if thd3 date1 < thd3 date2 then true
  else if thd3 date1 > thd3 date2 then false
  
  (* if we get to this point, years are equal, so compare months *)
  else
    if snd3 date1 < snd3 date2 then true
    else if snd3 date1 > snd3 date2 then false

    (* months are equal, so compare days *)
    else
      if fst3 date1 < fst3 date2 then true
      else false

(* 2 *)
(*Write a function number_in_month that takes a list of dates and a month (i.e., an int) and returns
how many dates in the list are in the given month.*)
(*val number_in_month : (int * int * int) list * int -> int*)
let rec number_in_month ((dates : (int * int * int) list), (month : int)) =
  if dates = [] then 0
  else let count_rest = number_in_month (List.tl dates, month) in
  if snd3 (List.hd dates) = month then 1 + count_rest else count_rest

(*3*)
(*Write a function number_in_months that takes a list of dates and a list of months (i.e., an int list)
and returns the number of dates in the list of dates that are in any of the months in the list of months.
Assume the list of months has no number repeated. Hint: Use your answer to the previous problem.*)
(*val number_in_months : (int * int * int) list * int list -> int*)
let rec number_in_months ((dates : (int * int * int) list), (months : int list)) : int =
  if months = [] then 0
  else number_in_month (dates, List.hd months) + number_in_months (dates, List.tl months)

(*4*)
(*Write a function dates_in_month that takes a list of dates and a month (i.e., an int) and returns a
list holding the dates from the argument list of dates that are in the month. The returned list should
contain dates in the order they were originally given.*)
(* val dates_in_month : (int * int * int) list -> int -> (int * int * int) list *)

let dates_in_month (dates : (int * int * int) list) (month : int) : (int * int * int) list =
  List.filter (fun (_, m, _) -> m = month) dates

(*5*)
(*Write a function dates_in_months that takes a list of dates and a list of months (i.e., an int list)
and returns a list holding the dates from the argument list of dates that are in any of the months in
the list of months. Assume the list of months has no number repeated. Hint: Use your answer to the
previous problem and OCaml’s list-append operator (@)*)
(*val dates_in_months : (int * int * int) list -> int list -> (int * int * int) list*)
let rec dates_in_months (dates : (int * int * int) list) (months : int list) : (int * int * int) list =
  if months = [] then []
  else let hd_month = List.hd months in
       let tl_months = List.tl months in
        dates_in_month dates hd_month @ dates_in_months dates tl_months

(* 6 *)
(*Write a function get_nth that takes a list of strings and a positive int n and returns the nth element
of the list where the head of the list is 1st. Do not worry about the case where the list has too few
elements: your function may apply List.hd or List.tl to the empty list in this case, which is okay.*)
(*val get_nth : string list -> int -> string*)
let rec get_nth (lst : string list) (n : int) : string =
  if n = 1 then List.hd lst
  else get_nth (List.tl lst) (n - 1)


(* 7 *)
(*Write a function string_of_date that takes a date and returns a string of the form September-10-2015
(for example). Use the operator ^ for concatenating strings and the library function string_of_int
for converting an int to a string. For producing the month part, do not use a bunch of conditionals.
Instead, use a list holding 12 strings and your answer to the previous problem. For consistency, use
hyphens exactly as in the example and use English month names: January, February, March, April,
May, June, July, August, September, October, November, December.*)
(*val string_of_date : int * int * int -> string*)

(*val string_of_date : int * int * int -> string*)
let string_of_date (date : int * int * int) : string =
  let months = ["January"; "February"; "March"; "April"; "May"; "June"; 
    "July"; "August"; "September"; "October"; "November"; "December"] in
  (get_nth months (snd3 date)) ^ "-" ^ 
  (string_of_int (fst3 date)) ^ "-" ^ 
  (string_of_int (thd3 date))

(*8*)
(*Write a function number_before_reaching_sum that takes an int called sum, which you can assume
is positive, and an int list, which you can assume contains all positive numbers, and returns an int.
You should return an int n such that the first n elements of the list add to less than sum, but the first
n + 1 elements of the list add to sum or more. Assume the entire list sums to more than the passed in
value; it is okay for an exception to occur if this is not the case.*)
(*val number_before_reaching_sum : int * int list -> int = <fun>*)

let rec number_before_reaching_sum (sum : int) (numbers : int list) : int =
  let rec aux sum numbers count total =
    if total >= sum then count
    else aux sum (List.tl numbers) (count + 1) (total + List.hd numbers)
  in aux sum numbers 0 0

(*9*)
(*Write a function what_month that takes a day of year (i.e., an int between 1 and 365) and returns
what month that day is in (1 for January, 2 for February, etc.). Use a list holding 12 integers and your
answer to the previous problem.*)
(*val what_month : int -> int = <fun>*)

let what_month (day : int) : int =
  let months = [0; 31; 59; 90; 120; 151; 181; 212; 243; 273; 304; 334] in
  let rec find_month day months month = 
    if day > List.hd (List.tl months) then find_month day (List.tl months) (month + 1)
    else month
  in find_month day months 1

(*10*)
(*Write a function month_range that takes two days of the year day1 and day2 and returns an int list
[m1;m2;...;mn] where m1 is the month of day1, m2 is the month of day1+1, . . . , and mn is the month
of day day2. Note the result will have length day2 - day1 + 1 or length 0 if day1 > day2.*)

let month_range (day1 : int) (day2 : int) : int list =
  let rec aux d1 d2 =
    if d1 > d2 then []
    else what_month d1 :: aux (d1 + 1) d2
  in aux day1 day2

(* 11 *)
(*Write a function oldest that takes a list of dates and evaluates to an (int*int*int) option. It
evaluates to None if the list has no dates else Some d where the date d is the oldest date in the list.*)
(* val oldest : (int * int * int) list -> (int * int * int) option = <fun> *)

let oldest (dates : (int * int * int) list) : (int * int * int) option =
  let rec find_oldest oldest dates =
    if dates = [] then oldest
    else let current_date = List.hd dates in
    let rest_dates = List.tl dates in
    if is_older (current_date, Option.get oldest) then find_oldest (Some current_date) rest_dates
    else find_oldest oldest rest_dates
  in
  if dates = [] then None else find_oldest (Some (List.hd dates)) (List.tl dates)

(*12*)
(*Write a function cumulative_sum that takes a list of numbers and returns a list of the partial sums
of these numbers. For example, cumulative_sum [12;27;13] = [12;39;52]. Hint: Use a helper
function that takes two arguments.*)
(*val cumulative_sum : int list -> int list = <fun>*)

let cumulative_sum (numbers : int list) : int list =
  let rec aux numbers acc =
    if numbers = [] then []
    else let head = List.hd numbers in
    let total = head + acc in
    total :: aux (List.tl numbers) total
  in aux numbers 0

  
(*13*)
(*Challenge Problem: Write functions number_in_months_challenge and dates_in_months_challenge
that are like your solutions to problems 3 and 5 except having a month in the second argument multiple
times has no more effect than having it once. (Hint: Remove duplicates, then use previous work.)*)

let unique_list lst =
  let the_function_that_adds_stuff_if_they_arent_included_in_the_list list2 x =
    if List.mem x list2 then list2
    else x :: list2
  in
  let result = List.fold_left the_function_that_adds_stuff_if_they_arent_included_in_the_list [] lst in
  List.rev result

let number_in_months_challenge dates months =
  let unique_months = unique_list months in
  number_in_months (dates, unique_months)

let dates_in_months_challenge dates months =
  let unique_months = unique_list months in
  dates_in_months dates unique_months
(*14*)
(*Challenge Problem: Write a function reasonable_date that takes a date and determines if it
describes a real date in the common era. A “real date” has a positive year (year 0 did not exist), a
month between 1 and 12, and a day appropriate for the month. Solutions should properly handle leap
years. Leap years are years that are either divisible by 400 or divisible by 4 but not divisible by 100.
(Do not worry about days possibly lost in the conversion to the Gregorian calendar in the Late 1500s.)*)

let is_leap_year year =
  (year mod 400 = 0) || (year mod 100 <> 0 && year mod 4 = 0)
  (* 
  Checks if a given year is a leap year or not
  A leap year is exactly divisible by 4 except for century years,
  unless said century year is perfectly divisible by 400
  Therefore if the year is not divisible by 100 and is divisible by 4, it's a leap year
  *)

let days_in_month (month : int) (year : int) : int =
  if month = 2 then
    (if is_leap_year year then 29 else 28)
  else if month = 4 || month = 6 || month = 9 || month = 11 then
    30
  else if month = 1 || month = 3 || month = 5 || month = 7 || month = 8 || month = 10 || month = 12 then
    31
  else
    0 (* invalid month *)

let reasonable_date (date : int * int * int) : bool =
  let day = fst3 date and month = snd3 date and year = thd3 date in
  year > 0 && month >= 1 && month <= 12 && day >= 1 && day <= days_in_month month year
  (* 
  The function `reasonable_date` checks if a given date is reasonable or valid. 

  The function takes a tuple of three integers as input, representing the day, month, and year of a date. 

  The function works as follows:
  - It first extracts the day, month, and year from the input tuple using the helper functions `fst3`, `snd3`, and `thd3`.
  - It then checks if the year is greater than 0, as a year of 0 or less would not be valid.
  - It checks if the month is between 1 and 12, inclusive, as these are the valid month numbers.
  - It checks if the day is between 1 and the number of days in the given month of the given year, inclusive. 
    The number of days in the month is determined by calling the `days_in_month` function. 
    This check ensures that the day number is valid for the given month and year (for example, February 30 would not be valid).
  - If all these conditions are met, the function returns true, indicating that the date is reasonable. 
    If any of these conditions are not met, the function returns false.
*)

question04.ml (0/6)
Compile error.
Usually this is because of a type checking issue; Make sure your function is the expected type!

File "test_files/question04.ml", line 19, characters 66-72:
19 |     let is_equal = List.fold_left (fun acc v -> acc || List.mem v result) true expected
                                                                       ^^^^^^
Error: This expression has type int -> (int * int * int) list
       but an expression was expected of type 'a list
question05.ml (0/7)
Compile error.
Usually this is because of a type checking issue; Make sure your function is the expected type!

File "test_files/question05.ml", line 20, characters 66-72:
20 |     let is_equal = List.fold_left (fun acc v -> acc || List.mem v result) true expected
                                                                       ^^^^^^
Error: This expression has type int list -> (int * int * int) list
       but an expression was expected of type 'a list
question06.ml (0/7)
Compile error.
Usually this is because of a type checking issue; Make sure your function is the expected type!

File "test_files/question06.ml", line 20, characters 4-14:
20 |   ) test_cases;
         ^^^^^^^^^^
Error: This expression has type ((string list * int) * string * int) list
       but an expression was expected of type
         (string list * (int -> string) * int) list
       Type string list * int is not compatible with type string list 
question07.ml (7/7)
question08.ml (0/7)
Compile error.
Usually this is because of a type checking issue; Make sure your function is the expected type!

File "test_files/question08.ml", line 20, characters 4-14:
20 |   ) test_cases;
         ^^^^^^^^^^
Error: This expression has type ((int * int list) * int * int) list
       but an expression was expected of type
         (int * (int list -> int) * int) list
       Type int * int list is not compatible with type int 
question09.ml (0/7)
Error during execution.

Fatal error: exception Failure("hd")
question10.ml (0/7)
Compile error.
Usually this is because of a type checking issue; Make sure your function is the expected type!

File "test_files/question10.ml", line 20, characters 4-14:
20 |   ) test_cases;
         ^^^^^^^^^^
Error: This expression has type ((int * int) * int list * int) list
       but an expression was expected of type
         (int * (int -> int list) * int) list
       Type int * int is not compatible with type int 
question11.ml (7/7)
question12.ml (7/7)
question13.ml (0/5)
Compile error.
Usually this is because of a type checking issue; Make sure your function is the expected type!

File "test_files/question13.ml", line 20, characters 17-42:
20 |     let result = dates_in_months_challange input in
                      ^^^^^^^^^^^^^^^^^^^^^^^^^
Error: Unbound value dates_in_months_challange
Hint: Did you mean dates_in_months_challenge?