(* $Id: netdate.mlp,v 2.1 2001/10/17 04:42:33 pdoane Exp $ * ---------------------------------------------------------------------- * *) (* Thanks to Nicolas George for contributing the parsing and format code *) type token = | Number of int | Day of int | Month of int | Meridian of bool | Zone of int | Dst | Plus | Minus | Comma | Colon | Slash | Invalid ;; let tokens_list = [ "january", Month 1; "jan", Month 1; "february", Month 2; "feb", Month 2; "march", Month 3; "mar", Month 3; "april", Month 4; "apr", Month 4; "may", Month 5; "june", Month 6; "jun", Month 6; "july", Month 7; "jul", Month 7; "august", Month 8; "aug", Month 8; "september", Month 9; "sept", Month 9; "sep", Month 9; "october", Month 10; "oct", Month 10; "november", Month 11; "nov", Month 11; "december", Month 12; "dec", Month 12; "sunday", Day 0; "sun", Day 0; "monday", Day 1; "mon", Day 1; "tuesday", Day 2; "tues", Day 2; "tue", Day 2; "wednesday", Day 3; "wednes", Day 3; "wed", Day 3; "thursday", Day 4; "thur", Day 4; "thurs", Day 4; "thu", Day 4; "friday", Day 5; "fri", Day 5; "saturday", Day 6; "sat", Day 6; "am", Meridian false; "pm", Meridian true; "gmt", Zone ( 0000); "ut", Zone ( 0000); "utc", Zone ( 0000); "wet", Zone ( 0000); "bst", Zone ( 0100); (*DST*) "cet", Zone ( 0100); "cest", Zone ( 0200); "met", Zone ( 0100); "mewt", Zone ( 0100); "mest", Zone ( 0200); (*DST*) "mesz", Zone ( 0200); (*DST*) "swt", Zone ( 0100); "sst", Zone ( 0200); (*DST*) "fwt", Zone ( 0100); "fst", Zone ( 0100); (*DST*) "eet", Zone ( 0200); "bt", Zone ( 0300); "zp4", Zone ( 0400); "zp5", Zone ( 0500); "zp6", Zone ( 0600); "wast", Zone ( 0700); "wadt", Zone ( 0800); (*DST*) "cct", Zone ( 0800); "jst", Zone ( 0900); "east", Zone ( 1000); "eadt", Zone ( 1100); (*DST*) "gst", Zone ( 1000); "nzt", Zone ( 1200); "nzst", Zone ( 1200); "nzdt", Zone ( 1300); (*DST*) "idle", Zone ( 1200); "idlw", Zone (-1200); "nt", Zone (-1100); "hst", Zone (-1000); "hdt", Zone (-0900); (*DST*) "cat", Zone (-1000); "ahst", Zone (-1000); "ydt", Zone (-0800); (*DST*) "yst", Zone (-0900); "pst", Zone (-0800); "pdt", Zone (-0700); (*DST*) "mst", Zone (-0700); "mdt", Zone (-0600); (*DST*) "cst", Zone (-0600); "cdt", Zone (-0500); (*DST*) "est", Zone (-0500); "edt", Zone (-0400); (*DST*) "ast", Zone (-0400); "adt", Zone (-0300); (*DST*) "wat", Zone (-0100); "at", Zone (-0200); ] ;; let find_token = let tokens = Hashtbl.create 53 in let add_token (name, value) = Hashtbl.add tokens name value in List.iter add_token tokens_list; function name -> try Hashtbl.find tokens name with Not_found -> Invalid ;; let tokens_of_string str = let rec scan_any = parser | [< ' ('0'..'9') as c; rest >] -> scan_number (int_of_char c - 48) rest | [< ' ('a'..'z' | 'A'..'Z') as c; rest >] -> let b = Buffer.create 16 in Buffer.add_char b (Char.lowercase c); scan_word b rest | [< ''('; rest >] -> scan_comment 0 rest | [< ' (' ' | '\t'); rest >] -> scan_any rest | [< ''+'; rest >] -> [< 'Plus; scan_any rest >] | [< ''-'; rest >] -> [< 'Minus; scan_any rest >] | [< '':'; rest >] -> [< 'Colon; scan_any rest >] | [< '','; rest >] -> [< 'Comma; scan_any rest >] | [< ''/'; rest >] -> [< 'Slash; scan_any rest >] | [< ' _ ; rest >] -> [< 'Invalid; scan_any rest >] | [< >] -> [< >] and scan_number a = parser | [< ' ('0'..'9') as c; rest >] -> scan_number (a * 10 + (int_of_char c - 48)) rest | [< rest >] -> [< 'Number a; scan_any rest >] and scan_word b = parser | [< ' ('a'..'z' | 'A'..'Z') as c; rest >] -> Buffer.add_char b (Char.lowercase c); scan_word b rest | [< ''.'; rest >] -> scan_word b rest | [< rest >] -> [< 'find_token (Buffer.contents b); scan_any rest >] and scan_comment n = parser | [< '')'; rest >] -> if n = 0 then scan_any rest else scan_comment (n - 1) rest | [< ''('; rest >] -> scan_comment (n + 1) rest | [< '_; rest >] -> scan_comment n rest in scan_any (Stream.of_string str) ;; type t = { year : int; (* complete year *) month : int; (* 1..12 *) day : int; (* 1..31 *) hour : int; minute : int; second : int; zone : int; (* in minutes; 60 = UTC+0100 *) week_day : int (* 0 = sunday; -1 if not given *) } ;; let parse str = let tokens = tokens_of_string str in let hour = ref None and minute = ref None and second = ref None and zone = ref None and week_day = ref None and day = ref None and month = ref None and year = ref None in let add_data ?h ?m ?s ?mdn ?tz ?dst ?wd ?md ?mo ?y () = let may_store r = function | None -> () | v when !r = None -> r := v | _ -> invalid_arg "Parse.date" in let tz = match (tz, dst) with | (Some tz, Some true) -> Some (tz - 100) | _ -> tz in let tz = match tz with | None -> None | Some x -> Some (x mod 100 + 60 * (x / 100)) in let h = match h with | None -> None | Some h -> match mdn with | None when h >= 0 && h <= 23 -> Some h | Some false when h > 0 && h <= 11 -> Some h | Some false when h = 12 -> Some 0 | Some true when h > 0 && h <= 11 -> Some (h + 12) | Some true when h = 12 -> Some 12 in let y = match y with | None -> None | Some y when y >= 100 -> Some y | Some y when y < 69 -> Some (2000 + y) | Some y -> Some (1900 + y) in may_store hour h; may_store minute m; may_store second s; may_store zone tz; may_store week_day wd; may_store day md; may_store month mo; may_store year y in let rec scan_gen = parser | [< 'Number n; rest >] -> scan_number n rest | [< 'Zone tz; dst = scan_dst; rest >] -> add_data ~tz ?dst (); scan_gen rest | [< 'Day wd; _ = scan_opt_coma; rest >] -> add_data ~wd (); scan_gen rest | [< 'Month mo; 'Number md; rest >] -> scan_date_m mo md rest | [< '_ >] -> invalid_arg "Parse.date" | [< >] -> () and scan_number n = parser | [< 'Meridian mdn; rest >] -> add_data ~h:n ~mdn (); scan_gen rest | [< 'Colon; 'Number m; rest >] -> scan_hour n m rest | [< 'Slash; 'Number m; rest >] -> scan_date_s n m rest | [< 'Minus; rest >] -> scan_date_d n rest | [< 'Month mo; rest >] -> add_data ~md:n ~mo (); scan_gen rest | [< rest >] -> add_data ~y:n (); scan_gen rest and scan_hour h m = parser | [< 'Colon; 'Number s; rest >] -> scan_hour_second h m s rest | [< tz = scan_tz; rest >] -> add_data ~h ~m ~tz (); scan_gen rest | [< mdn = scan_opt_meridian; rest >] -> add_data ~h ~m ?mdn (); scan_gen rest and scan_tz = parser | [< 'Plus; 'Number tz >] -> tz | [< 'Minus; 'Number tz >] -> -tz and scan_hour_second h m s = parser | [< tz = scan_tz; rest >] -> add_data ~h ~m ~s ~tz (); scan_gen rest | [< mdn = scan_opt_meridian; rest >] -> add_data ~h ~m ~s ?mdn (); scan_gen rest and scan_date_s n m = parser | [< 'Slash; 'Number p; rest >] -> if n >= 1000 then add_data ~y:n ~mo:m ~md:p () else add_data ~y:p ~mo:n ~md:m (); scan_gen rest | [< rest >] -> add_data ~mo:n ~md:n (); scan_gen rest and scan_date_d n = parser | [< 'Number mo; 'Minus; 'Number md; rest >] -> add_data ~y:n ~mo ~md (); scan_gen rest | [< 'Month mo; 'Minus; 'Number y; rest >] -> add_data ~y ~mo ~md:n (); scan_gen rest and scan_date_m mo md = parser | [< 'Comma; 'Number y; rest >] -> add_data ~y ~mo ~md (); scan_gen rest | [< rest >] -> add_data ~mo ~md (); scan_gen rest and scan_dst = parser | [< 'Dst >] -> Some true | [< >] -> None and scan_opt_coma = parser | [< 'Comma >] -> () | [< >] -> () and scan_opt_meridian = parser | [< 'Meridian mdn >] -> Some mdn | [< >] -> None in (try scan_gen tokens; with Stream.Error _ -> invalid_arg "Parse.date"); let may_get r = match !r with | None -> invalid_arg "Parse.date" | Some r -> r in let get_default d r = match !r with | None -> d | Some r -> r in let month = may_get month in if month < 1 || month > 12 then invalid_arg "Parse.date"; { year = may_get year; month = month; day = may_get day; hour = get_default 0 hour; minute = get_default 0 minute; second = get_default 0 second; zone = get_default 0 zone; week_day = get_default (-1) week_day } ;; let months_start = [| 0; 31; 59; 90; 120; 151; 181; 212; 243; 273; 304; 334 |] ;; let is_leap year = year mod 4 = 0 && (year mod 100 <> 0 || year mod 400 = 0) ;; let since_epoch date = if date.month < 1 || date.month > 12 then invalid_arg "Parse.since_epoch"; let in_day = float_of_int (date.hour * 3600 + (date.minute - date.zone) * 60 + date.second) in let days = date.year * 365 + (date.year + 3) / 4 - (date.year + 99) / 100 + (date.year + 399) / 400 - 719528 in let days = days + months_start.(date.month - 1) + date.day - 1 in let days = if is_leap date.year && date.month > 2 then days + 1 else days in 86400.0 *. (float_of_int days) +. in_day ;; let parse_epoch str = since_epoch (parse str) ;; let create ?(zone = 0) time = let time = time +. (float_of_int (zone * 60)) in let days = floor (time /. 86400.0) in let in_day = int_of_float (time -. 86400.0 *. days) in let days = days +. 719528.0 in let n400 = floor (days /. 146097.0) in let r400 = int_of_float (days -. n400 *. 146097.0) in let n400 = int_of_float n400 in let (n100, r100) = if r400 < 36525 then (0, r400) else ((r400 - 1) / 36524, (r400 - 1) mod 36524) in let (n4, r4) = if n100 = 0 then (r100 / 1461, r100 mod 1461) else if r100 < 1460 then (0, r100) else ((r100 + 1) / 1461, (r100 + 1) mod 1461) in let (n1, r1) = if n4 = 0 && n100 <> 0 then (r4 / 365, r4 mod 365) else if r4 < 366 then (0, r4) else ((r4 - 1) / 365, (r4 - 1) mod 365) in let year = 400 * n400 + 100 * n100 + 4 * n4 + n1 in let month_start = if is_leap year then fun m -> months_start.(m) + (if m > 1 then 1 else 0) else fun m -> months_start.(m) in let month_guess = r1 / 29 in let month = if month_guess = 12 then 11 else if r1 >= month_start month_guess then month_guess else month_guess - 1 in let second = in_day mod 60 and minutes = in_day / 60 in let minute = minutes mod 60 and hour = minutes / 60 in { year = year; month = month + 1; day = r1 - (month_start month) + 1; hour = hour; minute = minute; second = second; zone = zone; week_day = int_of_float (mod_float (days +. 6.0) 7.0) } ;; let full_day_names = [| "Sunday"; "Monday"; "Tuesday"; "Wednesday"; "Thursday"; "Friday"; "Saturday" |] ;; let abbr_day_names = [| "Sun"; "Mon"; "Tue"; "Wed"; "Thu"; "Fri"; "Sat" |] ;; let full_month_names = [| "January"; "February"; "March"; "April"; "May"; "June"; "Jully"; "August"; "September"; "October"; "November"; "December" |] ;; let abbr_month_names = [| "Jan"; "Feb"; "Mar"; "Apr"; "May"; "Jun"; "Jul"; "Aug"; "Sep"; "Oct"; "Nov"; "Dec" |] ;; let format_to out_ch ~fmt date = let add_char c = out_ch#output_char c and add_string s = out_ch#output_string s in let fail () = invalid_arg "Parse.format_date" in let add_digits w b n = if n > b * 10 then fail (); let rec aux b n = add_char (char_of_int (48 + n / b)); if b >= 10 then aux (b / 10) (n mod b) in if w then ( let rec aux_spaces b = if n > b || b < 10 then ( aux b n ) else ( add_char ' '; aux_spaces (b / 10) ) in aux_spaces b ) else ( aux b n ) in let wd () = if date.week_day < 0 || date.week_day > 6 then fail (); date.week_day in let yd () = months_start.(date.month - 1) + date.day in let rec do_format = function | 'a' -> add_string abbr_day_names.(date.week_day) | 'A' -> add_string full_day_names.(date.week_day) | 'b' | 'h' -> add_string abbr_month_names.(date.month - 1) | 'B' -> add_string full_month_names.(date.month - 1) | 'C' -> add_digits false 10 (date.year / 100) | 'd' -> add_digits false 10 date.day | 'e' -> add_digits true 10 date.day | 'H' -> add_digits false 10 date.hour | 'I' -> add_digits false 10 (match date.hour mod 12 with 0 -> 12 | d -> d) | 'j' -> add_digits false 100 (yd ()) | 'k' -> add_digits true 10 date.hour | 'l' -> add_digits true 10 (match date.hour mod 12 with 0 -> 12 | d -> d) | 'm' -> add_digits false 10 date.month | 'M' -> add_digits false 10 date.minute | 'n' -> add_char '\n' | 'p' -> add_string (if date.hour >= 12 then "PM" else "AM") | 'P' -> add_string (if date.hour >= 12 then "pm" else "am") | 'S' -> add_digits false 10 date.second | 't' -> add_char '\t' | 'u' -> add_digits false 1 (match wd () with 0 -> 7 | n -> n) | 'y' -> add_digits false 10 (date.year mod 100) | 'Y' -> add_digits false 1000 date.year | 'z' -> let (s, z) = if date.zone >= 0 then ('+', date.zone) else ('-', -date.zone) in add_char s; add_digits false 10 (z / 60); add_digits false 10 (z mod 60) | 'U' -> add_digits false 10 (((yd ()) - (wd ()) + 6) / 7) | 'V' -> failwith "TODO" | 'W' -> failwith "TODO" | 'w' -> add_digits false 1 (wd ()) | '%' -> add_char '%' | 'c' -> do_format 'a'; add_char ' '; do_format 'b'; add_char ' '; do_format 'e'; add_char ' '; do_format 'T'; add_char ' '; do_format 'Y' | 'D' | 'x' -> do_format 'm'; add_char '/'; do_format 'd'; add_char '/'; do_format 'y' | 'r' -> do_format 'I'; add_char ':'; do_format 'M'; add_char ':'; do_format 'S'; add_char ' '; do_format 'p' | 'R' -> do_format 'H'; add_char ':'; do_format 'M' | 'T' | 'X' -> do_format 'R'; add_char ':'; do_format 'S' | _ -> fail () in let rec aux i = if i = String.length fmt then () else match fmt.[i] with | '%' when i = String.length fmt - 1 -> fail () | '%' -> do_format fmt.[i + 1]; aux (i + 2) | c -> add_char c; aux (i + 1) in try aux 0 with _ -> fail () ;; let format ~fmt date = let b = Buffer.create (String.length fmt * 2) in format_to (new Netchannels.output_buffer b) ~fmt date; Buffer.contents b ;; (* Calculate local zone offset in minutes *) let localzone = let t = Unix.time () in let gt = Unix.gmtime t and lt = Unix.localtime t in let min_diff = (lt.Unix.tm_hour * 60 + lt.Unix.tm_min) - (gt.Unix.tm_hour * 60 + gt.Unix.tm_min) in let day_diff = lt.Unix.tm_yday - gt.Unix.tm_yday in if day_diff < -1 || day_diff = 1 then (* local day is UTC day + 1 *) min_diff + 24*60 else if day_diff > 1 || day_diff = -1 then (* local day is UTC day - 1 *) min_diff - 24*60 else (* local day is UTC day *) min_diff ;; (* The format routines above may want to support internationalization * in the future. The following must use the English conventions * described in the relevant RFCs. *) let mk_mail_date ?zone t = format "%a, %d %b %Y %H:%M:%S %z" (create ?zone t) ;; let mk_usenet_date ?zone t = format "%A, %d-%b-%y %H:%M:%S %z" (create ?zone t) ;; (* ====================================================================== * History: * * $Log: netdate.mlp,v $ * Revision 2.1 2001/10/17 04:42:33 pdoane * Use camlp4 for streams * * Revision 2.2 2001/09/29 15:26:53 pdoane * New date parsing/formatting routines * * Revision 2.1 2001/09/14 14:22:34 stolpmann * Initial revision (sourceforge) * * * ====================================================================== * Revision 1.1 2001/08/30 19:50:42 gerd * Initial revision. * * *)