CWIS Developer Documentation

- Home
- Main Page
- Namespaces
- Classes
- Files

- File List
- File Members

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 }
StdLib\CheckMyCaller
static CheckMyCaller($DesiredCaller, $ExceptionMsg=NULL)
Check the caller of the current function.
Definition: StdLib.php:47
StdLib\SortCompare
static SortCompare($A, $B)
Perform compare and return value appropriate for sort function callbacks.
Definition: StdLib.php:466
StdLib\ZipCodeDistance
static ZipCodeDistance($ZipA, $ZipB)
Compute the distance between two US ZIP codes.
Definition: StdLib.php:537
StdLib\GetMyCaller
static GetMyCaller()
Get string with file and line number for call to current function.
Definition: StdLib.php:23
StdLib\Pluralize
static Pluralize($Word)
Pluralize an English word.
Definition: StdLib.php:105
StdLib\GetLatLngForZipCode
static GetLatLngForZipCode($Zip)
Look up the GPS coordinates for a US ZIP code.
Definition: StdLib.php:490
StdLib
Standard utility library.
Definition: StdLib.php:14
StdLib\ArrayPermutations
static ArrayPermutations($Items, $Perms=array())
Return all possible permutations of a given array.
Definition: StdLib.php:615
PHP
PHP
Definition: RSSClient--Test.php:12
StdLib\ComputeGreatCircleDistance
static ComputeGreatCircleDistance($LatSrc, $LonSrc, $LatDst, $LonDst)
Computes the distance in kilometers between two points, assuming a spherical earth.
Definition: StdLib.php:564
StdLib\ComputeBearing
static ComputeBearing($LatSrc, $LonSrc, $LatDst, $LonDst)
Computes the initial angle on a course connecting two points, assuming a spherical earth...
Definition: StdLib.php:593
StdLib\SQL_DATE_FORMAT
const SQL_DATE_FORMAT
Format to feed to date() to get SQL-compatible date/time string.
Definition: StdLib.php:701
StdLib\NeatlyTruncateString
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
StdLib\Singularize
static Singularize($Word)
Singularize an English word.
Definition: StdLib.php:141
StdLib\GetUsStatesList
static GetUsStatesList()
Get an array of US state names with their two-letter abbreviations as the index.
Definition: StdLib.php:643
$Items
$Items
Definition: RSSClient--Test.php:26