(*
	$Id: Util.m,v 1.1 2000/11/28 18:42:43 dreeves Exp dreeves $
	Daniel Reeves
	Miscellaneous useful mathematica functions.
   TODO: goofy failures if a symbol is referenced before it is defined.
         The solution is probably to define all the usage statements first.
         PS: Equilibria.m has a solution to this problem.
*)

BeginPackage["Util`", {"MathIO`"}];
Begin["`Private`"];

Util`verboseTiming::usage = "verboseTiming[predictedTime_][expression_] is like Timing[expression] but with other stuff printed.";
SetAttributes[verboseTiming, HoldRest];
verboseTiming[predictedTime_, expression_] := Module[{t},
  Print["BEGIN run for ", predictedTime, "s starting at ", 
        ToDate[AbsoluteTime[]], 
        " till ", ToDate[AbsoluteTime[] + predictedTime]];
  t = Timing[expression];
  Print["END at ", ToDate[AbsoluteTime[]]];
  t]


(* TODO: the name "body" interferes with mail[]; what the hell is that about *)
Util`doWhile::usage = "doWhile[body,test] implements a C-style do loop.";
SetAttributes[doWhile, HoldAll];
doWhile[bodyTODO_, test_] := 
  While[True, Evaluate[bodyTODO]; If[!Evaluate[test], Break[]]]


(* TODO: the path to lockfile could change *)
Util`lockFile::usage = "Locks the specified file for atomic writes.";
lockFile[filename_String] := Module[{returnVal},
  returnVal = Run[(*"/usr/um/bin/" <> *) "lockfile " <> filename <> ".lock"];
  If[returnVal =!= 0, 
    pout["WARNING: lockfile didn't work; trying a different path...\n"];
    returnVal = Run["/usr/um/bin/lockfile " <> filename <> ".lock"];
  ];
  If[returnVal =!= 0, 
    pout["WARNING: lockfile still didn't work; trying a different path...\n"];
    returnVal = Run["/usr/bin/lockfile " <> filename <> ".lock"];
  ];
  If[returnVal =!= 0,
    pout["WARNING: lockfile *still* didn't work; giving up...\n"];
  ];

  pout["DIAGNOSTICS: lockfile returned ", returnVal, "\n"];
  returnVal
]

Util`unlockFile::usage = "Unlocks the specified file.";
unlockFile[filename_String] := Run["rm -f " <> filename <> ".lock"];


Util`registerProcess::usage = 
  "registerProcess[proccesIdFile, info] appends the process id and machine 
  name and info string to the processIdFile.  My perl script signal-all.pl 
  can be run with this file as an arg to send all the processes in the file 
  a signal.  It's a good idea to call this function at the beginning of a 
  program that runs for a long time and may be invoked on many machines.";
registerProcess[processIdFile_String, info_String] := Module[{r},
  lockFile[processIdFile];
  r = OpenAppend[processIdFile];
  (* TODO: make sure no newlines in info. *)
  WriteString[r, strout[$MachineName, If[$MachineDomain =!= "", ".", ""],
    $MachineDomain, " ", $ProcessID, " ", info, "\n"]];
  Close[r];
  unlockFile[processIdFile];
]


Util`shuffle::usage = "Takes a list and returns a random permutation of it.";
shuffle[l_List] := #2& @@@ Sort[{Random[],#}& /@ l]
  
  
Util`getFile::usage = 
  "getFile[filename] returns the contents of the file as a string.";
getFile[f_String] := Module[{s = Import[f, "Text"]}, 
  If[s === $Failed, "[ERROR OPENING " <> f <> "]", s]
]

Util`runThruString::usage = "Sends a string as stdin to a command and 
  returns the commands output as a string.";
runThruString[cmd_String, input_String] := Module[{in, out},
  in = "!echo \"" <> input <> "\" | " <> cmd;
  out = Import[in, "Text"];
  If[out === $Failed, "[ERROR PIPING FROM " <> i n<> "]", out]
]

(*
runThru[cmd_String, input_String] := Module[{s},
   WriteString[s = OpenTemporary[], input];
   Import["!"<>cmd<>" < " <> Close[s], "Text"]
]
*)

Util`tupleMax::usage = 
  "tupleMax[list] returns the tuple that is lexicographically maximal.";
tupleMax[l_List] := Fold[If[OrderedQ[{#1, #2}], #2, #1]&, First[l], Rest[l]]

Util`tupleMin::usage = 
  "tupleMin[list] returns the tuple that is lexicographically minimal.";
tupleMin[l_List] := Fold[If[OrderedQ[{#1, #2}], #1, #2]&, First[l], Rest[l]]


Util`argMax::usage = 
  "argMax[f, domain] returns the element of domain for which f of 
  that element is maximal -- breaks ties in favor of first occurrence.";
SetAttributes[argMax, HoldFirst];
(* argMax[f_, dom_List] := tupleMax[{f[#],#}& /@ dom][[2]] *)
argMax[f_, dom_List] := Fold[If[f[#1]>=f[#2], #1, #2]&, First[dom], Rest[dom]]

Util`argMin::usage = 
  "argMin[f, domain] returns the element of domain for which f of 
  that element is minimal.";
SetAttributes[argMin, HoldFirst];
argMin[f_, dom_List] := argMax[-f[#]&, dom]


(* TODO: this serendipitously works if niceValue is either an int or 
   a string, but that's probably being too sloppy. *)
Util`reniceMyself::usage = 
  "reniceMyself[niceValue] renices this process and its parent process.";
reniceMyself[niceValue_] :=
  Run[strout["renice ", niceValue, " ", $ProcessID, " ", $ParentProcessID]]


Util`lockPut::usage = 
  "Locks a file, then does a normal put, then unlocks the file.";
lockPut[stuff__, filename_String] := (
  Get["!lockfile " <> filename <> ".lock"];  (* TODO: use lockFile... *)
  Put[stuff, filename];
  Get["!rm -f " <> filename <> ".lock"];
)

Util`lockSave::usage =
  "Locks a file, then does a normal Save, then unlocks the file.";
SetAttributes[lockSave, HoldRest];
lockSave[filename_String, stuff_] := (
  Run["lockfile " <> filename <> ".lock"];  (* TODO: use lockfile *)
  Save[filename, stuff];
  Run["rm -f " <> filename <> ".lock"];
)


Util`SYSTEMINFO::usage = 
  "A string with things like machine and user name and process IDs.";
SYSTEMINFO := strout[
  "$MachineName: ", $MachineName, 
  "\n$MachineDomain: ", $MachineDomain, 
  "\n$UserName: ", $UserName, 
  "\n$ProcessID: ", $ProcessID, 
  "\n$ParentProcessID: ", $ParentProcessID, 
  "\n$CommandLine: ", $CommandLine, 
  "\nARGV: ", Global`ARGV, "\n"
]


Util`ENV::usage = 
  "A string showing all operating system environment variables.";
ENV = Read["!/usr/bin/env", Record, RecordSeparators -> {}];
Close["!/usr/bin/env"];


Util`intersperse::usage = 
  "Like Perl's join function.";
intersperse[delim_String, l_List]:=
  Module[{s}, StringJoin @@ Drop[s[#,delim]& /@ l /. s->Sequence, -1]]

(* TODO: decide which is better, this or the version above *)
Util`intersperse2::usage = 
  "Like Perl's join function.";
intersperse2[delim_String, l:{__String}] :=
  StringJoin @@ Drop[Flatten@Transpose[{l, Table[delim, {Length[l]}]}], -1]


Util`mail::usage = "\n
  mail[\n
    to -> {list of addresses},  \n
    from -> \"Some Name <some@email.address>\",  (* or just email adr *) \n
    subject -> \"string\", \n
    body -> \"string\", (* see SYSTEMINFO and BODY *)\n
    headers -> {list of \"Header: stuff\" strings}  (* addit'l headers *)].\n
  Use Options[mail] to see default options.";
Util`to::usage = "Option for mail; gives list of email addresses to send to.";
Util`from::usage = "Option for mail; see usage for mail.";
Util`subject::usage = "Option for mail; see usage for mail.";
Util`body::usage = "Option for mail; see usage for mail.";
Util`headers::usage = "Option for mail; see usage for mail.";
Options[mail] := {
  to -> {"dreeves@eecs.umich.edu"},
  from -> "MY-MMA-AGENT <dreeves@maria.physics.lsa.umich.edu>",
  subject -> strout["OUTPUT from process ", $ProcessID, " on ", $MachineName],
  body -> strout[SYSTEMINFO, "\nEnvironment:\n\n", ENV],
  headers -> {}
};
mail[opts___] := Module[ {opt, allHeaders, stream},
  opt[x_] := x /. {opts} /. Options[mail];
  allHeaders = Join[opt[headers], {
    "Subject: " <> opt[subject],
    "From: " <> opt[from],
    "To: " <> intersperse[", ", opt[to]]}
  ];
  stream = OpenWrite["!/usr/lib/sendmail -oi -t"];
  WriteString[stream, intersperse["\n", allHeaders], "\n\n", opt[body]];
  Close[stream];
]


(* TODO: don't have this tied up with mail.  
   Add an optional parameter for a function to call on assertion failure, 
   which could do the mail-sending. 
*)
Util`assert::usage = 
  "assert[test_,info_String] makes sure that test evaluates to True, 
  otherwise it sends mail and calls Exit[1].  Also, prints to STDERR.  
  Info is just extra info about what we were doing or why this 
  assertion should be true.";
SetAttributes[assert, HoldFirst];
assert[test_, info_String:""] := If[test =!= True,
  With[{error = strout["FAILED ASSERTION: ", HoldForm[test]]},
    pout[error, "\n"];
    If[info != "", pout["INFO: ", info, "\n"]];
    mail[subject -> strout[error, " (", info, ")"]];
    Exit[1]
  ]
]


Util`domain::usage = 
  "List of keys (as lists) for a hash.";
SetAttributes[removeHead, {HoldAll}];
removeHead[h_[args___]] := {args}
domain[s_] := removeHead @@@ First /@ DownValues[s]


Util`ni::usage = 
  "Shows a number as a decimal number, and on one line.";
ni[x_Integer] := x
ni[x_] := InputForm[N[x]]


Util`abort::usage = 
  "A constant used as a possible parameter to interruptedWhile.";

Util`interruptedWhile::usage = 
  "Like a While loop except for the 1st 2 args, which say: every so many 
  seconds (and once at the end regardless) pause and execute some code, 
  presumably with the purpose of announcing some partial results or the 
  status of the computation."; 
SetAttributes[interruptedWhile, HoldRest];
interruptedWhile[abort, interruptBody_, whileTest_, whileBody_] :=
  While[whileTest, 
    CheckAbort[While[whileTest, AbortProtect[whileBody]], 
      interruptBody]]
interruptedWhile[interruptFreq_, interruptBody_, whileTest_, whileBody_] := 
  While[whileTest,
    TimeConstrained[
      While[whileTest, AbortProtect[whileBody]],
      interruptFreq];
    interruptBody;
  ]


Util`fileAbsoluteTime::usage = 
  "Give the AbsoluteTime of the file modification date.";
fileAbsoluteTime[fileName_String] := (Date/.FileInformation[fileName])


(* TODO: see setFirstCheckPoint in the perl version (util.pl) *)
Util`checkPoint::usage = 
  "Call this in a loop and it will return true the " <>
  "first time and false the subsequent times except every specified " <>
  "number of seconds thereafter.";
nextCheckPoint = 0;
checkPoint[seconds_] := With[{t = AbsoluteTime[]},
   If[t >= nextCheckPoint,
      nextCheckPoint = t + seconds; 
      Return[True];
	];
  	Return[False];
]


(* TODO combine these functions to one, but keep the separate names for 
   external use 
*)

Util`StringPadRight::usage = 
  "StringPadRight[str,n,x] pads a string on the right with 
  char x (defaults to space) such that the length is n (returns original 
  string if longer than n).  If the first arg is not a string, does 
  ToString on it.";
StringPadRight[str_String, n_Integer, x_String:" "] := 
  If[StringLength[str] >= n, str,
    StringJoin[PadRight[Characters[str], n, x]]]
StringPadRight[stuff_, n_Integer, x_String:" "] := 
  StringPadRight[ToString[stuff], n, x]

Util`StringPadLeft::usage = 
  "StringPadLeft[str,n,x] pads a string on the left with 
  char x (defaults to space) such that the length is n (returns original 
  string if longer than n).  If the first arg is not a string, does 
  ToString on it.";
StringPadLeft[str_String, n_Integer, x_String:" "] := 
  If[StringLength[str] >= n, str,
    StringJoin[PadLeft[Characters[str], n, x]]]
StringPadLeft[stuff_, n_Integer, x_String:" "] := 
  StringPadLeft[ToString[stuff], n, x]

Util`nspl::usage = 
  "nspl is number-string-pad-left.";
nspl[num_, n_] := StringPadLeft[num,n,"0"]

Util`dd::usage = 
  "dd is double-digit, for taking a number from 0-99 and returning a 
  2-character string like \"03\" or \"42\".";
dd[num_?(#<100&)] := nspl[num, 2]


daysInYear = 365.242198781;  (* lightyear / c *)
weeksInYear = daysInYear / 7;
daysInMonth = daysInYear / 12;
weeksInMonth = daysInMonth / 7; 
sS = 1;
mS = 60*sS;
hS = 60*mS;
dS = 24*hS;
wS = 7*dS;
oS = daysInMonth*dS;
yS = daysInYear*dS;

(* TODO: handle negative number of seconds *)
Util`seconds2str::usage = 
  "Convert a number of seconds to years,months,weeks,days,hrs,mins,secs.";
seconds2str[secs_] := Module[
	{y, yQ= False, o, oQ= False, w, wQ= False, 
    d, dQ= False, h, hQ= False, m, mQ= False, s= secs},

   y = Floor[s / yS];
   If[y>0, yQ = oQ = wQ = dQ = hQ = mQ = True];
   s -= y * yS;

   o = Floor[s / oS];
   If[o>0, oQ = wQ = dQ = hQ = mQ = True];
   s -= o * oS;

   w = Floor[s / wS];
   If[w>0, wQ = dQ = hQ = mQ = True];
   s -= w * wS;

   d = Floor[s / dS];
   If[d>0, dQ = hQ = mQ = True];
   s -= d * dS;

   h = Floor[s / hS];
   If[h>0, hQ = mQ = True];
   s -= h * hS;

   m = Floor[s / mS];
   If[m>0, mQ = True];
   s -= m * mS;
	
	Return[strout[
      If[yQ, strout[y," years "], ""],
      If[oQ, strout[o," months "], ""],
      If[wQ, strout[w," weeks "], ""],
		If[dQ, strout[d," days "], ""],
		If[hQ, strout[dd[h],":"], ""],
		If[mQ, strout[dd[m],":",dd[Round[s]],"s"], strout[s,"s"]]]];
]


(* TODO: seconds2str is a better version of this now. *)
Util`secondsToStr::usage = 
  "Convert a number of seconds to days,hrs,mins,secs.";
secondsToStr[0, _] := ""
secondsToStr[seconds_, {label_}] := dd[seconds] <> label
secondsToStr[seconds_, {rest__, {unit_, label_}}] := 
  secondsToStr[Quotient[seconds - Mod[seconds, unit], unit], {rest}] <> 
    ToString[Mod[seconds, unit]] <> label
secondsToStr[seconds_] := 
  secondsToStr[seconds, {"days ", {24, ":"}, {60, ":"}, {60, "s"}}]
secondsToStr[0] := "0s"


(* TODO: TimeZone[] is reported as 0 on mynah, making this give GMT time *)
Util`time2str::usage = 
  "Convert an absolute number of seconds (add absolute time of 
  1970-1-1 00:00:00 for Unix (timezone issues?)) to a date/time string.";
time2str[t_:AbsoluteTime[]] := Module[ {y,m,d,h,min,s},
  {y,m,d,h,min,s} = ToDate[t];
  strout[y,"-",dd[m],"-",dd[d]," ",
         dd[h],":",dd[min],":",dd[s]]
]


End[];   (* Private context *)
EndPackage[];

