5 # Part of the ScoutLib application support library 6 # Copyright 2016 Edward Almasy and Internet Scout Research Group 7 # http://scout.wisc.edu 17 # ---- PUBLIC INTERFACE -------------------------------------------------- 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"];
47 public static function CheckMyCaller($DesiredCaller, $ExceptionMsg = NULL)
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"] :
"";
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))
65 # if exception message supplied 66 if ($ExceptionMsg !== NULL)
68 # make any needed substitutions in exception message 83 $Class.
"::".$Function),
87 throw new Exception($Msg);
91 # report to our caller that their caller was not the desired one 96 # report to our caller that their caller was not the desired one 107 # return word unchanged if singular and plural are the same 108 if (in_array(strtolower($Word), self::$UncountableWords))
113 # check for irregular singular forms 114 foreach (self::$IrregularWords as $Pattern => $Result)
116 $Pattern =
'/'.$Pattern.
'$/i';
117 if (preg_match($Pattern, $Word))
119 return preg_replace($Pattern, $Result, $Word);
123 # check for matches using regular expressions 124 foreach (self::$PluralizePatterns as $Pattern => $Result)
126 if (preg_match($Pattern, $Word))
128 return preg_replace($Pattern, $Result, $Word);
132 # return word unchanged if we could not process it 143 # return word unchanged if singular and plural are the same 144 if (in_array(strtolower($Word), self::$UncountableWords))
149 # check for irregular plural forms 150 foreach (self::$IrregularWords as $Result => $Pattern)
152 $Pattern =
'/'.$Pattern.
'$/i';
153 if (preg_match($Pattern, $Word))
155 return preg_replace($Pattern, $Result, $Word);
159 # check for matches using regular expressions 160 foreach (self::$SingularizePatterns as $Pattern => $Result)
162 if (preg_match($Pattern, $Word))
164 return preg_replace($Pattern, $Result, $Word);
168 # return word unchanged if we could not process it 182 $TagStrippedString = strip_tags(html_entity_decode($String));
184 # if the string contained no HTML tags, we can just treat it as text 185 if ($String == $TagStrippedString)
187 $Length = mb_strlen($String);
189 # if string was short enough, we need not do anything 190 if ($Length <= $MaxLength)
195 # if BreakAnywhere is set, just chop at the max length 198 $BreakPos = $MaxLength;
200 # otherwise look for an acceptable breakpoint 203 $BreakPos = mb_strrpos($String,
" ", -($Length - $MaxLength));
205 # if we couldn't find a breakpoint, just chop at max length 206 if ($BreakPos === FALSE)
208 $BreakPos = $MaxLength;
212 $Result = mb_substr($String, 0, $BreakPos);
214 # tack on the ellipsis 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 222 # if there aren't enough non-whitespace displayed characters to 223 # exceed the max length, bail because we don't need to do 225 if (mb_strlen(trim($TagStrippedString)) <= $MaxLength)
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 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);
247 # max length of an HTML Entity 248 $MaxEntityLength = 8;
250 # track how much we have displayed 251 $DisplayedLength = 0;
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
258 # start in the 'text state' 261 # iterate over all our tokens 262 foreach ($Tokens as $Token)
266 # text state handles words that will be displayed 270 # look for characters that can end a word 274 # if we've buffered up a word 277 # and if displaying that word exceeds 278 # our length, then we're done 279 if ($DisplayedLength + $BufLen > $MaxLength)
284 # otherwise, add the buffered word to our display 286 $DisplayedLength += $BufLen;
289 # buffer this character 293 # if it could have been the start of a tag or an entity, 294 # change state appropriately 297 $State = ($Token ==
"<") ? $S_MaybeTag :
302 # for characters that can't break a word, just buffer them 310 # MaybeTag state checks if a < began a tag or not 312 # tags start with alpha characters (like <b>) 313 # or a slash (like </b>) 314 if (ctype_alpha($Token) || $Token ==
"/")
316 # if this was a tag, output it, output it, 317 # clear our buffer, and move to the Tag state 318 $Result .= $Buffer.$Token;
325 # otherwise, check if displaying this character would 326 # exceed our length. bail if so 327 if ($DisplayedLength + 1 > $MaxLength)
331 # if not, output the characters, clear our buffer, 332 # move to the Text state 333 $Result .= $Buffer.$Token;
341 # Tag state processes the contents of a tag 343 # always output tag contents 346 # check if this is the beginning of a quoted string, 347 # changing state appropriately if so 348 if ($Token ==
"\"" || $Token ==
"'")
353 # if this is the end of the tag, go back to Text state 354 elseif ($Token ==
">")
360 # Quote state processes quoted attributes 362 # always output quote contents 365 # if we've found the matching quote character, 366 # return to the Tag state 367 if ($Token == $QuoteChar)
373 # MaybeEntity decides if we're enjoying an HTML entity 374 # or just an ampersand 380 # if it was a space, then we're not in an entity 381 # as they cannot contain spaces 384 # check if we should be fone 385 if ($DisplayedLength + $BufLen > $MaxLength)
389 # if not, output the buffer, clear it, and return to Text 391 $DisplayedLength += $BufLen;
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 ==
";")
406 # if this has been too many characters without a ; 407 # for it to be an entity, return to text 417 # tack on the ellipsis 420 # if our string contained HTML tags that we may need to close 421 if (preg_match_all(
"%<(/?[a-z]+)[^>]*>%", $Result, $Matches))
423 # pull out matches for the names of tags 424 $Matches = $Matches[1];
426 # build up an array of open tags 428 while ( ($Tag = array_shift($Matches)) !== NULL )
430 # if this was not a close tag, prepend it to our array 431 if (mb_substr($Tag, 0, 1) !=
"/")
433 array_unshift($Tags, $Tag);
435 # if this tag is not self-closing, append it to our list of open tags 436 elseif (mb_substr($Tag, -1) !=
"/")
438 # if this was a close tag, look to see if this tag was open 439 $Tgt = array_search(mb_substr($Tag, 1), $Tags);
442 # if so, remove this tag from our list 448 # iterate over open tags, closing them as we go 449 while ( ($Tag = array_shift($Tags)) !== NULL)
451 $Result .=
"</".$Tag.
">";
474 return ($A < $B) ? -1 : 1;
492 static $ZipCache = array();
494 # if we don't have a cached value for this zip, look one up 495 if (!isset($ZipCache[$Zip]))
497 # try to open our zip code database 498 $FHandle = fopen(dirname(__FILE__).
"/StdLib--ZipCodeCoords.txt",
"r");
500 # if we couldn't open the file, we can't look up a value 501 if ($FHandle === FALSE)
503 throw new Exception(
"Unable to open zip code coordinates file");
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)
510 if ($Line[0] == $Zip)
512 $ZipCache[$Zip] = array(
513 "Lat" => $Line[1],
"Lng" => $Line[2]);
518 # if we've scanned the entire file and have no coords for 519 # this zip, cache a failure 520 if (!isset($ZipCache[$Zip]))
522 $ZipCache[$Zip] = FALSE;
526 # hand back cached value 527 return $ZipCache[$Zip];
540 $FirstPoint = self::GetLatLngForZipCode($ZipA);
541 $SecondPoint = self::GetLatLngForZipCode($ZipB);
543 # if we scanned the whole file and lack data for either of our 544 # points, return NULL 545 if ($FirstPoint === FALSE || $SecondPoint === FALSE)
550 return self::ComputeGreatCircleDistance(
551 $FirstPoint[
"Lat"], $FirstPoint[
"Lng"],
552 $SecondPoint[
"Lat"], $SecondPoint[
"Lng"]);
567 # See http://en.wikipedia.org/wiki/Great-circle_distance 569 # Convert it all to Radians 570 $Ps = deg2rad($LatSrc);
571 $Ls = deg2rad($LonSrc);
572 $Pf = deg2rad($LatDst);
573 $Lf = deg2rad($LonDst);
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));
596 # See http://mathforum.org/library/drmath/view/55417.html 598 # Convert angles to radians 599 $Ps = deg2rad($LatSrc);
600 $Ls = deg2rad($LonSrc);
601 $Pf = deg2rad($LatDst);
602 $Lf = deg2rad($LonDst);
604 return rad2deg(atan2(sin($Lf-$Ls)*cos($Pf),
605 cos($Ps)*sin($Pf)-sin($Ps)*cos($Pf)*cos($Lf-$Ls)));
619 $Result = array($Perms);
624 for ($Index = count(
$Items) - 1; $Index >= 0; --$Index)
628 list($Segment) = array_splice($NewItems, $Index, 1);
629 array_unshift($NewPerms, $Segment);
630 $Result = array_merge($Result,
631 self::ArrayPermutations($NewItems, $NewPerms));
650 "CA" =>
"California",
652 "CT" =>
"Connecticut",
654 "DC" =>
"District of Columbia",
667 "MA" =>
"Massachusetts",
670 "MS" =>
"Mississippi",
675 "NH" =>
"New Hampshire",
676 "NJ" =>
"New Jersey",
677 "NM" =>
"New Mexico",
679 "NC" =>
"North Carolina",
680 "ND" =>
"North Dakota",
684 "PA" =>
"Pennsylvania",
685 "RI" =>
"Rhode Island",
686 "SC" =>
"South Carolina",
687 "SD" =>
"South Dakota",
693 "WA" =>
"Washington",
694 "WV" =>
"West Virginia",
704 # ---- PRIVATE INTERFACE ------------------------------------------------- 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",
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",
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",
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",
757 private static $IrregularWords = array(
762 'child' =>
'children',
767 private static $UncountableWords = array(
static CheckMyCaller($DesiredCaller, $ExceptionMsg=NULL)
Check the caller of the current function.
static SortCompare($A, $B)
Perform compare and return value appropriate for sort function callbacks.
static ZipCodeDistance($ZipA, $ZipB)
Compute the distance between two US ZIP codes.
static GetMyCaller()
Get string with file and line number for call to current function.
static Pluralize($Word)
Pluralize an English word.
static GetLatLngForZipCode($Zip)
Look up the GPS coordinates for a US ZIP code.
Standard utility library.
static ArrayPermutations($Items, $Perms=array())
Return all possible permutations of a given array.
static ComputeGreatCircleDistance($LatSrc, $LonSrc, $LatDst, $LonDst)
Computes the distance in kilometers between two points, assuming a spherical earth.
static ComputeBearing($LatSrc, $LonSrc, $LatDst, $LonDst)
Computes the initial angle on a course connecting two points, assuming a spherical earth...
const SQL_DATE_FORMAT
Format to feed to date() to get SQL-compatible date/time string.
static NeatlyTruncateString($String, $MaxLength, $BreakAnywhere=FALSE)
Attempt to truncate a string as neatly as possible with respect to word breaks, punctuation, and HTML tags.
static Singularize($Word)
Singularize an English word.
static GetUsStatesList()
Get an array of US state names with their two-letter abbreviations as the index.