CWIS Developer Documentation
StdLib.php
Go to the documentation of this file.
1 <?PHP
2 #
3 # FILE: StdLib.php
4 #
5 # Part of the ScoutLib application support library
6 # Copyright 2016 Edward Almasy and Internet Scout Research Group
7 # http://scout.wisc.edu
8 #
9 
14 class StdLib
15 {
16 
17  # ---- PUBLIC INTERFACE --------------------------------------------------
18 
23  public static function GetMyCaller()
24  {
25  $Trace = version_compare(PHP_VERSION, "5.4.0", ">=")
26  ? debug_backtrace(FALSE, 2) : debug_backtrace(FALSE);
27  $Caller = basename($Trace[1]["file"]).":".$Trace[1]["line"];
28  return $Caller;
29  }
30 
47  public static function CheckMyCaller($DesiredCaller, $ExceptionMsg = NULL)
48  {
49  # retrieve caller info
50  $Trace = version_compare(PHP_VERSION, "5.4.0", ">=")
51  ? debug_backtrace(FALSE, 3) : debug_backtrace(FALSE);
52  $FullFile = $Trace[1]["file"];
53  $File = basename($FullFile);
54  $Line = $Trace[1]["line"];
55  $Class = isset($Trace[2]["class"]) ? $Trace[2]["class"] : "";
56  $Function = isset($Trace[2]["function"]) ? $Trace[2]["function"] : "";
57 
58  # if caller does not match desired caller
59  if (($DesiredCaller != $Class)
60  && ($DesiredCaller != $Class."::".$Function)
61  && ($DesiredCaller != $Class.$Function)
62  && ($DesiredCaller != $File)
63  && ($DesiredCaller != $File.":".$Line))
64  {
65  # if exception message supplied
66  if ($ExceptionMsg !== NULL)
67  {
68  # make any needed substitutions in exception message
69  $Msg = str_replace(
70  array(
71  "%FILE%",
72  "%LINE%",
73  "%FULLFILE%",
74  "%CLASS%",
75  "%FUNCTION%",
76  "%METHOD%"),
77  array(
78  $File,
79  $Line,
80  $FullFile,
81  $Class,
82  $Function,
83  $Class."::".$Function),
84  $ExceptionMsg);
85 
86  # throw exception
87  throw new Exception($Msg);
88  }
89  else
90  {
91  # report to our caller that their caller was not the desired one
92  return FALSE;
93  }
94  }
95 
96  # report to our caller that their caller was not the desired one
97  return TRUE;
98  }
99 
105  public static function Pluralize($Word)
106  {
107  # return word unchanged if singular and plural are the same
108  if (in_array(strtolower($Word), self::$UncountableWords))
109  {
110  return $Word;
111  }
112 
113  # check for irregular singular forms
114  foreach (self::$IrregularWords as $Pattern => $Result)
115  {
116  $Pattern = '/'.$Pattern.'$/i';
117  if (preg_match($Pattern, $Word))
118  {
119  return preg_replace($Pattern, $Result, $Word);
120  }
121  }
122 
123  # check for matches using regular expressions
124  foreach (self::$PluralizePatterns as $Pattern => $Result)
125  {
126  if (preg_match($Pattern, $Word))
127  {
128  return preg_replace($Pattern, $Result, $Word);
129  }
130  }
131 
132  # return word unchanged if we could not process it
133  return $Word;
134  }
135 
141  public static function Singularize($Word)
142  {
143  # return word unchanged if singular and plural are the same
144  if (in_array(strtolower($Word), self::$UncountableWords))
145  {
146  return $Word;
147  }
148 
149  # check for irregular plural forms
150  foreach (self::$IrregularWords as $Result => $Pattern)
151  {
152  $Pattern = '/'.$Pattern.'$/i';
153  if (preg_match($Pattern, $Word))
154  {
155  return preg_replace($Pattern, $Result, $Word);
156  }
157  }
158 
159  # check for matches using regular expressions
160  foreach (self::$SingularizePatterns as $Pattern => $Result)
161  {
162  if (preg_match($Pattern, $Word))
163  {
164  return preg_replace($Pattern, $Result, $Word);
165  }
166  }
167 
168  # return word unchanged if we could not process it
169  return $Word;
170  }
171 
180  public static function NeatlyTruncateString($String, $MaxLength, $BreakAnywhere=FALSE)
181  {
182  $TagStrippedString = strip_tags(html_entity_decode($String));
183 
184  # if the string contained no HTML tags, we can just treat it as text
185  if ($String == $TagStrippedString)
186  {
187  $Length = mb_strlen($String);
188 
189  # if string was short enough, we need not do anything
190  if ($Length <= $MaxLength)
191  {
192  return $String;
193  }
194 
195  # if BreakAnywhere is set, just chop at the max length
196  if ($BreakAnywhere)
197  {
198  $BreakPos = $MaxLength;
199  }
200  # otherwise look for an acceptable breakpoint
201  else
202  {
203  $BreakPos = mb_strrpos($String, " ", -($Length - $MaxLength));
204 
205  # if we couldn't find a breakpoint, just chop at max length
206  if ($BreakPos === FALSE)
207  {
208  $BreakPos = $MaxLength;
209  }
210  }
211 
212  $Result = mb_substr($String, 0, $BreakPos);
213 
214  # tack on the ellipsis
215  $Result .= "...";
216  }
217  # otherwise, we're in an HTML string and we have to account for
218  # how many characters are actually displayed when the string will be
219  # rendered
220  else
221  {
222  # if there aren't enough non-whitespace displayed characters to
223  # exceed the max length, bail because we don't need to do
224  # anything
225  if (mb_strlen(trim($TagStrippedString)) <= $MaxLength)
226  {
227  return $String;
228  }
229 
230  # okay, the hard way -- we have to do some limited parsing
231  # of html and attempt to count the number of printing characters
232  # as we're doing that. to accomplish this, we'll build a
233  # little state machine and iterate over the characters one at a
234  # time
235 
236  # split the string into characters (annoyingly, mb_split
237  # cannot do this, so we have to use preg_split in unicode mode)
238  $Tokens = preg_split('%%u', $String, -1, PREG_SPLIT_NO_EMPTY);
239 
240  # define our states
241  $S_Text = 0;
242  $S_MaybeTag = 1;
243  $S_MaybeEntity = 2;
244  $S_Tag = 3;
245  $S_Quote = 4;
246 
247  # max length of an HTML Entity
248  $MaxEntityLength = 8;
249 
250  # track how much we have displayed
251  $DisplayedLength = 0;
252 
253  $Buffer = ""; # for characters we're not sure about
254  $BufLen = 0; # count of buffered characters
255  $Result = ""; # for characters we've included
256  $QuoteChar =""; # quote character in use
257 
258  # start in the 'text state'
259  $State = $S_Text;
260 
261  # iterate over all our tokens
262  foreach ($Tokens as $Token)
263  {
264  switch ($State)
265  {
266  # text state handles words that will be displayed
267  case $S_Text:
268  switch($Token)
269  {
270  # look for characters that can end a word
271  case "<":
272  case "&":
273  case " ":
274  # if we've buffered up a word
275  if ($BufLen > 0)
276  {
277  # and if displaying that word exceeds
278  # our length, then we're done
279  if ($DisplayedLength + $BufLen > $MaxLength)
280  {
281  break 3;
282  }
283 
284  # otherwise, add the buffered word to our display
285  $Result .= $Buffer;
286  $DisplayedLength += $BufLen;
287  }
288 
289  # buffer this character
290  $Buffer = $Token;
291  $BufLen = 1;
292 
293  # if it could have been the start of a tag or an entity,
294  # change state appropriately
295  if ($Token != " ")
296  {
297  $State = ($Token == "<") ? $S_MaybeTag :
298  $S_MaybeEntity;
299  }
300  break;
301 
302  # for characters that can't break a word, just buffer them
303  default:
304  $Buffer .= $Token;
305  $BufLen++;
306  break;
307  }
308  break;
309 
310  # MaybeTag state checks if a < began a tag or not
311  case $S_MaybeTag:
312  # tags start with alpha characters (like <b>)
313  # or a slash (like </b>)
314  if (ctype_alpha($Token) || $Token == "/")
315  {
316  # if this was a tag, output it, output it,
317  # clear our buffer, and move to the Tag state
318  $Result .= $Buffer.$Token;
319  $Buffer = "";
320  $BufLen = 0;
321  $State = $S_Tag;
322  }
323  else
324  {
325  # otherwise, check if displaying this character would
326  # exceed our length. bail if so
327  if ($DisplayedLength + 1 > $MaxLength)
328  {
329  break 2;
330  }
331  # if not, output the characters, clear our buffer,
332  # move to the Text state
333  $Result .= $Buffer.$Token;
334  $DisplayedLength++;
335  $Buffer = "";
336  $BufLen = 0;
337  $State = $S_Text;
338  }
339  break;
340 
341  # Tag state processes the contents of a tag
342  case $S_Tag:
343  # always output tag contents
344  $Result .= $Token;
345 
346  # check if this is the beginning of a quoted string,
347  # changing state appropriately if so
348  if ($Token == "\"" || $Token == "'")
349  {
350  $QuoteChar = $Token;
351  $State = $S_Quote;
352  }
353  # if this is the end of the tag, go back to Text state
354  elseif ($Token == ">")
355  {
356  $State = $S_Text;
357  }
358  break;
359 
360  # Quote state processes quoted attributes
361  case $S_Quote:
362  # always output quote contents
363  $Result .= $Token;
364 
365  # if we've found the matching quote character,
366  # return to the Tag state
367  if ($Token == $QuoteChar)
368  {
369  $State = $S_Tag;
370  }
371  break;
372 
373  # MaybeEntity decides if we're enjoying an HTML entity
374  # or just an ampersand
375  case $S_MaybeEntity:
376  # buffer this token
377  $Buffer.= $Token;
378  $BufLen++;
379 
380  # if it was a space, then we're not in an entity
381  # as they cannot contain spaces
382  if ($Token == " ")
383  {
384  # check if we should be fone
385  if ($DisplayedLength + $BufLen > $MaxLength)
386  {
387  break 2;
388  }
389  # if not, output the buffer, clear it, and return to Text
390  $Result .= $Buffer;
391  $DisplayedLength += $BufLen;
392  $Buffer = "";
393  $BufLen = 0;
394  $State = $S_Text;
395  }
396  # if we have &xxxx; then count that as a single character entity,
397  # output it, clear the buffer, and return to Text
398  elseif ($Token == ";")
399  {
400  $Result .= $Buffer;
401  $DisplayedLength++;
402  $Buffer = "";
403  $BufLen = 0;
404  $State = $S_Text;
405  }
406  # if this has been too many characters without a ;
407  # for it to be an entity, return to text
408  elseif ($BufLen > 8)
409  {
410  $State = $S_Text;
411  }
412 
413  break;
414  }
415  }
416 
417  # tack on the ellipsis
418  $Result .= "...";
419 
420  # if our string contained HTML tags that we may need to close
421  if (preg_match_all("%<(/?[a-z]+)[^>]*>%", $Result, $Matches))
422  {
423  # pull out matches for the names of tags
424  $Matches = $Matches[1];
425 
426  # build up an array of open tags
427  $Tags = array();
428  while ( ($Tag = array_shift($Matches)) !== NULL )
429  {
430  # if this was not a close tag, prepend it to our array
431  if (mb_substr($Tag, 0, 1) != "/")
432  {
433  array_unshift($Tags, $Tag);
434  }
435  # if this tag is not self-closing, append it to our list of open tags
436  elseif (mb_substr($Tag, -1) != "/")
437  {
438  # if this was a close tag, look to see if this tag was open
439  $Tgt = array_search(mb_substr($Tag, 1), $Tags);
440  if ($Tgt !== FALSE)
441  {
442  # if so, remove this tag from our list
443  unset($Tags[$Tgt]);
444  }
445  }
446  }
447 
448  # iterate over open tags, closing them as we go
449  while ( ($Tag = array_shift($Tags)) !== NULL)
450  {
451  $Result .= "</".$Tag.">";
452  }
453  }
454  }
455 
456  return $Result;
457  }
458 
466  public static function SortCompare($A, $B)
467  {
468  if ($A == $B)
469  {
470  return 0;
471  }
472  else
473  {
474  return ($A < $B) ? -1 : 1;
475  }
476  }
477 
490  public static function GetLatLngForZipCode($Zip)
491  {
492  static $ZipCache = array();
493 
494  # if we don't have a cached value for this zip, look one up
495  if (!isset($ZipCache[$Zip]))
496  {
497  # try to open our zip code database
498  $FHandle = fopen(dirname(__FILE__)."/StdLib--ZipCodeCoords.txt", "r");
499 
500  # if we couldn't open the file, we can't look up a value
501  if ($FHandle === FALSE)
502  {
503  throw new Exception("Unable to open zip code coordinates file");
504  }
505 
506  # iterate over our database until we find the desired zip
507  # or run out of database
508  while (($Line = fgetcsv($FHandle, 0, "\t")) !== FALSE)
509  {
510  if ($Line[0] == $Zip)
511  {
512  $ZipCache[$Zip] = array(
513  "Lat" => $Line[1], "Lng" => $Line[2]);
514  break;
515  }
516  }
517 
518  # if we've scanned the entire file and have no coords for
519  # this zip, cache a failure
520  if (!isset($ZipCache[$Zip]))
521  {
522  $ZipCache[$Zip] = FALSE;
523  }
524  }
525 
526  # hand back cached value
527  return $ZipCache[$Zip];
528  }
529 
537  public static function ZipCodeDistance($ZipA, $ZipB)
538  {
539 
540  $FirstPoint = self::GetLatLngForZipCode($ZipA);
541  $SecondPoint = self::GetLatLngForZipCode($ZipB);
542 
543  # if we scanned the whole file and lack data for either of our
544  # points, return NULL
545  if ($FirstPoint === FALSE || $SecondPoint === FALSE)
546  {
547  return FALSE;
548  }
549 
550  return self::ComputeGreatCircleDistance(
551  $FirstPoint["Lat"], $FirstPoint["Lng"],
552  $SecondPoint["Lat"], $SecondPoint["Lng"]);
553  }
554 
564  public static function ComputeGreatCircleDistance($LatSrc, $LonSrc,
565  $LatDst, $LonDst)
566  {
567  # See http://en.wikipedia.org/wiki/Great-circle_distance
568 
569  # Convert it all to Radians
570  $Ps = deg2rad($LatSrc);
571  $Ls = deg2rad($LonSrc);
572  $Pf = deg2rad($LatDst);
573  $Lf = deg2rad($LonDst);
574 
575  # Compute the central angle
576  return 3958.756 * atan2(
577  sqrt( pow(cos($Pf)*sin($Lf-$Ls), 2) +
578  pow(cos($Ps)*sin($Pf) -
579  sin($Ps)*cos($Pf)*cos($Lf-$Ls), 2)),
580  sin($Ps)*sin($Pf)+cos($Ps)*cos($Pf)*cos($Lf-$Ls));
581 
582  }
583 
593  public static function ComputeBearing($LatSrc, $LonSrc,
594  $LatDst, $LonDst)
595  {
596  # See http://mathforum.org/library/drmath/view/55417.html
597 
598  # Convert angles to radians
599  $Ps = deg2rad($LatSrc);
600  $Ls = deg2rad($LonSrc);
601  $Pf = deg2rad($LatDst);
602  $Lf = deg2rad($LonDst);
603 
604  return rad2deg(atan2(sin($Lf-$Ls)*cos($Pf),
605  cos($Ps)*sin($Pf)-sin($Ps)*cos($Pf)*cos($Lf-$Ls)));
606  }
607 
615  public static function ArrayPermutations($Items, $Perms = array())
616  {
617  if (empty($Items))
618  {
619  $Result = array($Perms);
620  }
621  else
622  {
623  $Result = array();
624  for ($Index = count($Items) - 1; $Index >= 0; --$Index)
625  {
626  $NewItems = $Items;
627  $NewPerms = $Perms;
628  list($Segment) = array_splice($NewItems, $Index, 1);
629  array_unshift($NewPerms, $Segment);
630  $Result = array_merge($Result,
631  self::ArrayPermutations($NewItems, $NewPerms));
632  }
633  }
634  return $Result;
635  }
636 
643  public static function GetUsStatesList()
644  {
645  return array(
646  "AL" => "Alabama",
647  "AK" => "Alaska",
648  "AZ" => "Arizona",
649  "AR" => "Arkansas",
650  "CA" => "California",
651  "CO" => "Colorado",
652  "CT" => "Connecticut",
653  "DE" => "Delaware",
654  "DC" => "District of Columbia",
655  "FL" => "Florida",
656  "GA" => "Georgia",
657  "HI" => "Hawaii",
658  "ID" => "Idaho",
659  "IL" => "Illinois",
660  "IN" => "Indiana",
661  "IA" => "Iowa",
662  "KS" => "Kansas",
663  "KY" => "Kentucky",
664  "LA" => "Louisiana",
665  "ME" => "Maine",
666  "MD" => "Maryland",
667  "MA" => "Massachusetts",
668  "MI" => "Michigan",
669  "MN" => "Minnesota",
670  "MS" => "Mississippi",
671  "MO" => "Missouri",
672  "MT" => "Montana",
673  "NE" => "Nebraska",
674  "NV" => "Nevada",
675  "NH" => "New Hampshire",
676  "NJ" => "New Jersey",
677  "NM" => "New Mexico",
678  "NY" => "New York",
679  "NC" => "North Carolina",
680  "ND" => "North Dakota",
681  "OH" => "Ohio",
682  "OK" => "Oklahoma",
683  "OR" => "Oregon",
684  "PA" => "Pennsylvania",
685  "RI" => "Rhode Island",
686  "SC" => "South Carolina",
687  "SD" => "South Dakota",
688  "TN" => "Tennessee",
689  "TX" => "Texas",
690  "UT" => "Utah",
691  "VT" => "Vermont",
692  "VA" => "Virginia",
693  "WA" => "Washington",
694  "WV" => "West Virginia",
695  "WI" => "Wisconsin",
696  "WY" => "Wyoming",
697  );
698  }
699 
701  const SQL_DATE_FORMAT = "Y-m-d H:i:s";
702 
703 
704  # ---- PRIVATE INTERFACE -------------------------------------------------
705 
706  private static $PluralizePatterns = array(
707  '/(quiz)$/i' => "$1zes",
708  '/^(ox)$/i' => "$1en",
709  '/([m|l])ouse$/i' => "$1ice",
710  '/(matr|vert|ind)ix|ex$/i' => "$1ices",
711  '/(x|ch|ss|sh)$/i' => "$1es",
712  '/([^aeiouy]|qu)y$/i' => "$1ies",
713  '/(hive)$/i' => "$1s",
714  '/(?:([^f])fe|([lr])f)$/i' => "$1$2ves",
715  '/(shea|lea|loa|thie)f$/i' => "$1ves",
716  '/sis$/i' => "ses",
717  '/([ti])um$/i' => "$1a",
718  '/(tomat|potat|ech|her|vet)o$/i'=> "$1oes",
719  '/(bu)s$/i' => "$1ses",
720  '/(alias)$/i' => "$1es",
721  '/(octop)us$/i' => "$1i",
722  '/(ax|test)is$/i' => "$1es",
723  '/(us)$/i' => "$1es",
724  '/s$/i' => "s",
725  '/$/' => "s"
726  );
727  private static $SingularizePatterns = array(
728  '/(quiz)zes$/i' => "$1",
729  '/(matr)ices$/i' => "$1ix",
730  '/(vert|ind)ices$/i' => "$1ex",
731  '/^(ox)en$/i' => "$1",
732  '/(alias)es$/i' => "$1",
733  '/(octop|vir)i$/i' => "$1us",
734  '/(cris|ax|test)es$/i' => "$1is",
735  '/(shoe)s$/i' => "$1",
736  '/(o)es$/i' => "$1",
737  '/(bus)es$/i' => "$1",
738  '/([m|l])ice$/i' => "$1ouse",
739  '/(x|ch|ss|sh)es$/i' => "$1",
740  '/(m)ovies$/i' => "$1ovie",
741  '/(s)eries$/i' => "$1eries",
742  '/([^aeiouy]|qu)ies$/i' => "$1y",
743  '/([lr])ves$/i' => "$1f",
744  '/(tive)s$/i' => "$1",
745  '/(hive)s$/i' => "$1",
746  '/(li|wi|kni)ves$/i' => "$1fe",
747  '/(shea|loa|lea|thie)ves$/i'=> "$1f",
748  '/(^analy)ses$/i' => "$1sis",
749  '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => "$1$2sis",
750  '/([ti])a$/i' => "$1um",
751  '/(n)ews$/i' => "$1ews",
752  '/(h|bl)ouses$/i' => "$1ouse",
753  '/(corpse)s$/i' => "$1",
754  '/(us)es$/i' => "$1",
755  '/s$/i' => ""
756  );
757  private static $IrregularWords = array(
758  'move' => 'moves',
759  'foot' => 'feet',
760  'goose' => 'geese',
761  'sex' => 'sexes',
762  'child' => 'children',
763  'man' => 'men',
764  'tooth' => 'teeth',
765  'person' => 'people'
766  );
767  private static $UncountableWords = array(
768  'sheep',
769  'fish',
770  'deer',
771  'series',
772  'species',
773  'money',
774  'rice',
775  'information',
776  'equipment'
777  );
778 }
static CheckMyCaller($DesiredCaller, $ExceptionMsg=NULL)
Check the caller of the current function.
Definition: StdLib.php:47
static SortCompare($A, $B)
Perform compare and return value appropriate for sort function callbacks.
Definition: StdLib.php:466
static ZipCodeDistance($ZipA, $ZipB)
Compute the distance between two US ZIP codes.
Definition: StdLib.php:537
static GetMyCaller()
Get string with file and line number for call to current function.
Definition: StdLib.php:23
static Pluralize($Word)
Pluralize an English word.
Definition: StdLib.php:105
static GetLatLngForZipCode($Zip)
Look up the GPS coordinates for a US ZIP code.
Definition: StdLib.php:490
Standard utility library.
Definition: StdLib.php:14
static ArrayPermutations($Items, $Perms=array())
Return all possible permutations of a given array.
Definition: StdLib.php:615
static ComputeGreatCircleDistance($LatSrc, $LonSrc, $LatDst, $LonDst)
Computes the distance in kilometers between two points, assuming a spherical earth.
Definition: StdLib.php:564
static ComputeBearing($LatSrc, $LonSrc, $LatDst, $LonDst)
Computes the initial angle on a course connecting two points, assuming a spherical earth...
Definition: StdLib.php:593
const SQL_DATE_FORMAT
Format to feed to date() to get SQL-compatible date/time string.
Definition: StdLib.php:701
static NeatlyTruncateString($String, $MaxLength, $BreakAnywhere=FALSE)
Attempt to truncate a string as neatly as possible with respect to word breaks, punctuation, and HTML tags.
Definition: StdLib.php:180
static Singularize($Word)
Singularize an English word.
Definition: StdLib.php:141
static GetUsStatesList()
Get an array of US state names with their two-letter abbreviations as the index.
Definition: StdLib.php:643