123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- <?php
- /* provides a table to store interesting information on descriptions submitted by users
- *
- * to use:
- * scanDescription("it rained, 37F and 40% humid -- we had dutch letter ice cream")
- *
- * it works using a table called "events". The table stores integer values representing various
- * observations a user has made while recording radon reading samples. The table includes a column
- * "type" that encodes things like rain, or snow, making the "events" table more flexible. Each event
- * stored in the table has a start and end time, encoded as seconds since unix epoch.
- *
- * to make this code useful for end users, these functions are provided to show the users interesting
- * insights from the descriptions they provided
- *
- * getRainPeriods(...): returns array(["start"=>{start}, "end"=>{end}], ...)
- * getSnowPeriods(...): returns array(["start"=>{start}, "end"=>{end}], ...)
- * getTemperatureData(...): returns array(["time"=>{time}, "reading"=>{reading}, ...)
- * getHumidityData(...): returns array(["time"=>{time}, "reading"=>{reading}, ...)
- * getRainData(...): returns array(["time"=>{time}, "reading"=>{reading}, ...)
- * getSnowData(...): returns array(["time"=>{time}, "reading"=>{reading}, ...)
- *
- * [?] these functions all take an optional start and end time parameters eg:
- * $lastWeekTemperatures = getTemperatureData(time() - strtotime("7 days", 0), time());
- *
- * to extend this, three functions are modified and optionally a frontend oriented helper function could be
- * defined here to retrieve stored data. The functions to be modified are regexParse, which handles
- * searching the user description, and it's sister function typeLookup which encodes the type of data, like
- * "celcius", "rain", or "snow" as a number. One may want to modify the third function, saveEvents if
- * additional processing needs to occur on data as regexParse should only be finding the data of interest
- */
- /* order 2021 Nov 14 6:30pm
- * aroydee 1-515-528-8009
- * 14. vermicelli combo
- * 29. pad thai chicken
- * H4. green curry beef
- * vietnamese eggnog (2)
- * order will be ready in 25 minutes
- */
- function scanDescription($text = false, $time = false) {
- # connect to mysql database radondb
- $db = mysql_connect("localhost", "root", "secom8703");
- mysql_select_db("radondb", $db);
- # make sure events table exists
- if(false === mysql_query("SELECT 1 FROM `events` LIMIT 1", $db)) {
- mysql_query("CREATE TABLE `events` (start INTEGER, end INTEGER, type INTEGER, value INTEGER, CONSTRAINT event UNIQUE (start, end, type))", $db);
- }
- # either perform extraction on entire table, or just user text
- if(false /* entire table */ === $text) {
- fwrite(STDERR, "scanning radondb for temperature and humidity data" . PHP_EOL);
- $result = mysql_query("SELECT time, description FROM locationLog WHERE id = 'note';", $db);
- while($row = mysql_fetch_array($result, MYSQL_ASSOC)) {
- //printRow($row);
- saveEvents($db, regexParse($row["description"]), $row["time"]);
- }
- mysql_free_result($result);
- } else saveEvents($db, regexParse($text), (false === $time) ?time() :$time);
- # mysql needs clean-up
- mysql_close($db);
- }
- // returns stats from user descriptions
- function regexParse($s) {
- $stats = $t = array();
- $s = strtolower($s);
- // do simple mathing based using NNN{symbol} pattern
- $terms = array("farenheit" => "f", "celcius" => "c", "humidity" => "%");
- foreach($terms as $k=>$v) if(1 === preg_match("/\d{1,3}(?={$v})/", $s, $t)) $stats[$k] = $t[0];
- // check for rain, and other keywords (do snow separately)
- $terms = array("rain");
- foreach($terms as $v) if(false !== strpos($s, $v)) $stats[$v] = true;
- // when checking for snow, ignore if you also find melt
- if(false !== strpos($s, "snow")) {
- if(false === strpos($s, "melt")) $stats["snow"] = true;
- }
- return $stats;
- }
- // the database has a table events schema that includes types, defined in regexParse, the keys are immutable
- function typeLookup($s, $getArray = false) {
- $types = array(
- "farenheit" => 0,
- "celcius" => 1,
- "humidity" => 2,
- "rain" => 3,
- "snow" => 4
- /* ... no changes to previous types and keys, add new types below */
- );
- if(!isset($types[$s])) return false;
- return $types[$s];
- }
- // save the stats from a scanned description into the database as events
- function saveEvents($db, $stats, $time) {
- foreach($stats as $plaintextType=>$value) {
- // convert canonical integers to SQL INTEGER
- if(ctype_digit($value)) $value = intval($value);
- // convert boolean to SQL INTEGER
- if(true === $value) $value = 1;
- if(false === $value) $value = 0;
- // other values should be integers
- if(!is_int($value)) {
- fwrite(STDERR, "[!] stat {$plaintextType} has value {$value}, not an integer" . PHP_EOL);
- continue;
- }
- // stat type must be valid, also prevents unwanted SQL
- if(false === ($type = typeLookup($plaintextType))) {
- fwrite(STDERR, "[!] stat type {$plaintextType} is not defined in events schema" . PHP_EOL);
- continue;
- }
- // and also make sure nothing unsafe is coming through time
- $start = intval($time);
- $end = $start;
- // make rain or snow occur over time, 8 hours minimum
- if("rain" === $plaintextType || "snow" === $plaintextType) $end += (8/*hours*/ * 60 /*minutes*/ * 60 /*seconds*/);
- // save record
- if(false === mysql_query("INSERT INTO events (start, end, type, value) VALUES ({$start},{$end},{$type},{$value})", $db)) {
- // event previously recorded
- } else /* event sucessfully stored */;
- }
- }
- /* sometimes users input time intervals without an end time:
- * I might record that it is raining, then go take a six hour nap, upon waking I
- * might notice that it is still raining and wait until it stops and then record
- * that it stops. This will probably never happen. More likely I will record that
- * it is raining and then never say anything again. There is a possibility that
- * when I wake up I might record again that it is raining. We just assume that rain
- * always lasts at least 8-hours and that whenever there are overlapping mentions
- * of rain the intervals are merged, eg: not summed:
- *
- * |----------- rain 8-hours ------------|
- * |----------- rain 8-hours ------------|
- *
- * |-------------------------- merged --------------------------|
- */
- function eightHourIntervalMerge($type, $start = false, $end = false) {
- if(false === ($rows = helperFunctionTemplate($type, $start, $end))) return false;
- $intervals = array();
- $start = false;
- // rain/snow are added in 8-hour intervals, merge overlapping intervals
- foreach($rows as $k=>$row) {
- // record the begining of the interval
- if(false === $start) $start = $row["start"];
- // goto next interval if current interval overlaps
- if(isset($rows[$k + 1]) and ($rows[$k + 1]["start"] <= $row["end"])) continue;
- // store the interval when there are no more overlaps
- $intervals[] = array("start"=>$start, "end"=>$row["end"]);
- $start = false;
- }
- return $intervals;
- }
- // returns array of ["time"=>{time}, "reading"=>{reading}] from events by type
- function getReadings($type, $start = false, $end = false) {
- if(false === ($rows = helperFunctionTemplate($type, $start, $end))) return false;
- $arr = array( /* [time => ..., reading => ...] */ );
- foreach($rows as $row) $arr[] = array("time"=>$row["start"], "reading"=>$row["value"]);
- return $arr;
- }
- // returns merged time intervals of rain, with optional time range (unix epoch)
- function getRainPeriods($start = false, $end = false) {
- return eightHourIntervalMerge("rain", $start = false, $end = false);
- }
- // returns merged time intervals of snow, with optional time range (unix epoch)
- function getSnowPeriods($start = false, $end = false) {
- return eightHourIntervalMerge("snow", $start = false, $end = false);
- }
- // returns farenheit readings, with optional time range (unix epoch)
- function getTemperatureData($start = false, $end = false) {
- return getReadings("farenheit", $start, $end);
- }
- // returns humidity percentage readings, with optional time range (unix epoch)
- function getHumidityData($start = false, $end = false) {
- return getReadings("humidity", $start, $end);
- }
- // merged time intervals of snow, sampled and filtered
- function getSnowData($start = false, $end = false) {
- return intervalsToSamples($start, $end, "snow");
- }
- // merged time intervals of rain, sampled and filtered
- function getRainData($start = false, $end = false) {
- return intervalsToSamples($start, $end, "rain");
- }
- // merged time intervals of rain/snow, sampled and filtered
- function intervalsToSamples($start = false, $end = false, $type = "rain") {
- // make a set of numbers, between zero and one to use as start and stop data
- $points = /* arbitrary number, consider HighCharts - more points, more load */ 8;
- $startStopLookup = generateSoftStart($points);
- // make up the amount of time for a "soft start"
- $softTime = /* fraction of "length of rain" in time */ strtotime("8 hours", 0) / (2 * $points);
- // make up the first data point where there is no rain (selected from database manually)
- $arr = array(array("time"=> /* SELECT MIN(time) FROM radonLog */ 1631631600, "reading" => 0));
- // remaining data points come from the "events" table
- $periods = eightHourIntervalMerge($type, $start = false, $end = false);
- // generate the graph, use soft start/stop data at beginging/end of intervals
- foreach($periods as $a) {
- // [?] $a["reading"] value is always one, soft starts are between zero and one
- // generate soft start values, separated by a made up amount of time
- $start = $a["start"];
- for($i = 0;$i<$points; $i++) {
- $arr[] = array("time"=>$start, "reading" => $startStopLookup[$i]);
- $start += $softTime;
- }
- // generate soft stop values, separated by a made up amount of time
- $stop = $a["end"] - ($points * $softTime);
- for($i = $points; $i--;) {
- $arr[] = array("time"=>$stop, "reading" => $startStopLookup[$i]);
- $stop += $softTime;
- }
- }
- return $arr;
- }
- /* generate a soft curve to use instead of a harsh edge when displaying data
- * that would normally have a jarring on/off transition, user optionally
- * provides the number of points used to represent the wave
- *
- * [?] user should use the returned values in reverse for a "soft-stop"
- *
- * | .-'``
- * | .`
- * | ,`
- * | * 180 degrees of a sinewave, offset by one
- * | ` and a phase shift of PI/2 gives a "soft-
- * | .` start" -- this opposes a "sharp" edge
- * | * of digital on/off, or a square wave
- * | ' (to make values between zero and one, scaled by half)
- * | .`
- * | ,'
- * |,.-'_____________________
- */
- function generateSoftStart($n = 6) {
- $arr = array();
- for($i = 0; $i < $n; $i++) $arr[] = (1+sin(($i*2*M_PI_2)/($n-1) - M_PI_2))/2;
- return $arr;
- }
- function helperFunctionTemplate($type, $start = false, $end = false) {
- # connect to mysql database radondb
- $db = mysql_connect("localhost", "root", "secom8703");
- mysql_select_db("radondb", $db);
- // prevent injection in time interval input
- $start = intval($start);
- $end = intval($end);
- $and = (/* php zero is false */ $start and $end) ?"start >= {$start} AND end <= {$end} AND" :"";
- // add optional time interval to query
- $sql = "SELECT start, end, value FROM `events` WHERE {$and}";
- // lookup type, make sure query sorts results oldest event to newest
- $sql .= "type = " . typeLookup($type) . " ORDER BY start ASC";
- if(false === ($result = mysql_query($sql, $db))) {
- fwrite(STDERR, "{$type} query failed:" . PHP_EOL . mysql_error($db));
- return false;
- }
- $rows = array();
- while($row = mysql_fetch_array($result, MYSQL_ASSOC)) {
- $rows[] = $row;
- //printRow($row);
- }
- mysql_free_result($result);
- # mysql needs clean-up
- mysql_close($db);
-
- return $rows;
- }
- //scanDescription();
- //var_dump(getRainPeriods());
- //var_dump(getSnowPeriods());
- //var_dump(getHumidityData());
- //var_dump(getTemperatureData());
- //var_dump(getRainData());
- // calling this without parameters will try to extract all data from all records in locationLog
- //scanDescription();
- /* ------ stuff for debugging ------ */
- // row contains ["time" => "...", "description" => "..."]
- function printRow($mySQL_assocRow) {
- $temp = "";
- foreach($mySQL_assocRow as $k=>$v) {
- echo "{$v} ";
- if("description" !== $k) continue;
- $temp = implode(" ", regexParse($v));
- }
- if("" !== $temp) echo "---- @{$mySQL_assocRow["time"]} {$temp}";
- echo PHP_EOL;
- }
|