Browse Source

added conversions and variations

Joachim M. Giæver 4 years ago
parent
commit
7334403047

+ 108 - 28
index.php

@@ -1,41 +1,121 @@
 <?php
 
+ini_set('display_errors', 1);
+ini_set('display_startup_errors', 1);
+error_reporting(E_ALL);
+
+use App\Yr\Forecast;
+use App\Yr\Forecast\Tabular;
+use App\Yr\Forecast\Tabular\Time\Symbol;
+use App\Yr\Forecast\Tabular\Time\Temperature;
+use App\Yr\Forecast\Tabular\Time\WindDirection;
+use App\Yr\Forecast\Tabular\Time\WindSpeed;
+
 require __DIR__ . '/vendor/autoload.php';
 
-$url = 'https://www.yr.no/place/Norway/Troms/Tromsø/Tromsø/forecast_hour_by_hour.xml';
+class TextForcast {
 
-$forecast = new App\Yr\Forecast($url);
+    private $forecast;
 
-$range = $forecast->getTabular()->getBetween(
-    $forecast->getSunset(), $forecast->getSunrise()->add(new \DateInterval('P1D'))
-);
+    public function __construct(Tabular $t) {
+        $this->forecast = $t; 
+    }
 
-echo '<pre>';
-echo sprintf("Forecast from %s until %s, updated %s:\n", 
-    $range->getFrom()->format('d.m H:i'), 
-    $range->getUntil()->format('d.m H:i'),
-    $forecast->getLastUpdate()->format('H:i')
-);
-foreach ($range->getVariations()->getData() as $data) {
-    echo $data['time']->getFrom()->format("H:i");
+    /**
+     * «Tonight Southeasterly light breeze, cloudy, temperature -8 to  -9 degrees»
+     * @author Helge Tangen
+     */
+    public function start(): string {
+        $time = $this->forecast->getVariations()->getTime();
+        return sprintf(
+            "%s from %s (%s) %s",
+            $this->when($time->getFrom()), 
+            $time->getFrom()->format('H'),
+            $time->getFrom()->format('h a'),
+            (function() use ($time){
+                $str = [];
+
+                $prev = null;
+                foreach ($time as $data) {
+                    switch (get_class($data)) {
+                    case Symbol::class:
+                        $str[] = ($prev == null ? ' ' : ', ') . $data->getName();
+                        break;
+                    case WindSpeed::class:
+                        $str[] = ($prev == null || WindSpeed::class ? " " : ", ") . $data->getName();
+                        break;
+                    case WindDirection::class:
+                        $str[] = ($prev == null ? ' ' : ', ') . $data->getName();
+                        break;
+                    case Temperature::class:
+                        $str[] = ($prev == null ? ' ' : ', ') . sprintf('%d %s', $data->getValue(), $data->getDegree());
+                        break;
+                    default:
+                        //$str []= '``' .  get_class($data) . '`` missing';
+                    }
+                    $prev = $data;
+                }
+
+                $last = array_pop($str);
+
+                if (substr($last, 0, 1) == ',')
+                    $last = substr($last, 1);
+
+                if (sizeof($str) == 0)
+                    return strtolower($last);
+
+                return strtolower(sprintf(
+                    "%s and %s", 
+                    join($str), $last
+                ));
+            })()
+        );
+    }
+
+    public function when(\DateTimeInterface $d): string {
+        $diff = $d->diff(new \DateTime());
+
+        if ($diff->invert && $diff->days == 1)
+            return sprintf("yesterday %s", $this->period($d));
+        elseif (!$diff->invert && $diff->days == 1)
+            return sprintf("tomorrow %s", $this->period($d));
+        elseif ($diff->days == 0)
+            return sprintf("This %s", $this->period($d));
+
+        return sprintf("the %s at %s of %s %y ",
+            $this->period($d),
+            $d->format('dd'), $d->format('m'), $d->format('YY')
+        );
+    }
+
+    public function period(\DateTimeInterface $d): string {
+        $hour = (int)$d->format('H');
+        if ($hour >= 6 && $hour <= 11)
+            return "morning";
+        elseif ($hour == 12)
+            return "noon";
+        elseif ($hour > 12 && $hour < 18)
+            return "afternoon";
+        elseif ($hour >= 18 && $hour < 24)
+            return "evening";
+        elseif ($hour == 12)
+            return "midning";
+        else
+            return "night";
+    }
+
+    public function since(\DateTimeInterface $d): string {
+    
+    }
 }
-    //echo sprintf("%s: %s\n", $data['time']->getFrom()->format("H:i"), join(", ", $data['entities']));
 
-$f = current(iterator_to_array($range->getIterator()));
-echo $f->getWindSpeed()->convertTo('ft/s');
+$url = 'https://www.yr.no/place/Norway/Troms/Tromsø/Tromsø/forecast_hour_by_hour.xml';
 
-echo "\n";
-$stat = $range->getStatistics();
+$forecast = new Forecast($url);
 
-echo "\n";
-echo $stat->getAverageTemperature();
-echo "\n";
-echo $stat->getAverageWindSpeed();
+$range = $forecast->getTabular()->getBetween(
+    $forecast->getSunset(), $forecast->getSunrise()->add(new \DateInterval('P1D'))
+);
 
-echo "\n";
-var_dump($stat->getAverageSymbols());
-var_dump($stat->getMostCommonSymbol());
-echo "\n";
-foreach ($forecast->getCredit() as $c)
-    echo $c . "\n";
+echo (new TextForcast($range))->start();
 ?>

+ 14 - 0
src/Yr/Forecast.php

@@ -11,6 +11,20 @@ use App\Yr\Forecast\Tabular;
  * Disclaimer: To use this package you are required
  * to print the credits from the method ```getCredit()```
  *
+ * Require the package to your project by adding (or creating)
+ * the following in the `composer.json`-file:
+ * ```
+ * {
+ *      "require": {
+ *          "joachimmg/yr-forecast": "dev-master"
+ *      },
+ *      "repositories": [{
+ *          "type": "vcs",
+ *          "url": "https://git.giaever.org/joachimmg/yr-forecast.git"
+ *      }]
+ * }
+ * ```
+ *
  * @author Joachim M. Giæver (joachim[]giaever.org)
  * @version 1.0
  */

+ 23 - 2
src/Yr/Forecast/Tabular.php

@@ -21,6 +21,7 @@ class Tabular implements \IteratorAggregate {
 
     private $time = [];
     private $stats;
+    private $variations;
 
     /**
      * @param \SimpleXMLElement $xml The xml part holding the time objects, can be null
@@ -28,9 +29,13 @@ class Tabular implements \IteratorAggregate {
     public function __construct(?\SimpleXMLElement $xml) {
         $this->stats = new Statistics();
 
-        if ($xml != null)
+        if ($xml != null) {
+
             foreach ($xml->time as $time)
                 $this->addTime(new Time($time));
+            
+            $this->makeVariations($time);
+        }
     }
 
     /**
@@ -53,6 +58,11 @@ class Tabular implements \IteratorAggregate {
         return $this->stats;
     }
 
+    protected function makeVariations(): self {
+        $this->variations = new Variations($this->time);
+        return $this;
+    }
+
     /**
      * Remove superfluous weather data.
      *
@@ -62,7 +72,7 @@ class Tabular implements \IteratorAggregate {
      * @return Variations
      */
     public function getVariations(): Variations {
-        return new Variations($this->time);
+        return $this->variations;
     }
 
     /**
@@ -86,13 +96,24 @@ class Tabular implements \IteratorAggregate {
             if ($time->getFrom() >= $from && $time->getUntil() <= $until)
                 $n->addTime($time);
 
+        $n->makeVariations(); 
         return $n;
     }
 
+    /**
+     * Return the time this tabular is from
+     * 
+     * @return \DateTimeInterface The from time
+     */
     public function getFrom(): \DateTimeInterface {
         return current($this->time)->getFrom();
     }
 
+    /**
+     * Return the time this tabular is until
+     * 
+     * @return \DateTimeInterface The until time
+     */
     public function getUntil(): \DateTimeInterface {
         return current(array_reverse($this->time))->getUntil();
     }

+ 16 - 2
src/Yr/Forecast/Tabular/Time/AbstractUnit.php

@@ -20,6 +20,15 @@ abstract class AbstractUnit implements DiffInterface {
         $this->unit = $unit;
     }
 
+    /**
+     * Determines if the classes can operate on each other
+     *
+     * @param AbstractUnit $with Class to operate on
+     * @param bool $throw Set to true if function should throw exception
+     *
+     * @return bool True if they can
+     * @throws \InvalidArgumentException if $throw set to true
+     */
     private function canOperate(AbstractUnit $with, bool $throw = false): bool {
         if ($with instanceof $this || $with instanceof CustomUnit)
             return true;
@@ -36,6 +45,9 @@ abstract class AbstractUnit implements DiffInterface {
         return false;
     }
 
+    /**
+     * Transform custom unit to self
+     */
     protected function transformClass(CustomUnit $cu): self {
         return unserialize(preg_replace(
             '/^O:\d+:"[^"]++"/',
@@ -115,11 +127,13 @@ abstract class AbstractUnit implements DiffInterface {
     }
 
     /**
+     * Note! Working on floats, so operates on 100-times value.
+     *
      * {@inheritDoc}
      */
     public function diff(DiffInterface $d): int {
-        if ($d instanceof $this)
-            return $this->value - $d->getValue();
+        if ($this instanceof $d)
+            return (int)(($this->getValue() * 100) - ($d->getValue() * 100));
 
         return 0;
     }

+ 17 - 0
src/Yr/Forecast/Tabular/Time/ConvertableInterface.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\Yr\Forecast\Tabular\Time;
+
+/**
+ * Implemented on units that can be converted, 
+ * such as wind speed and temperature.
+ */
+interface ConvertableInterface {
+    /**
+     * Convert the to a different unit
+     *
+     * @param string $unit The unit to convert to, eg UNIT_FTS
+     */
+    public function convertTo(string $unit): self;
+}
+?>

+ 7 - 1
src/Yr/Forecast/Tabular/Time/CustomUnit.php

@@ -2,8 +2,14 @@
 
 namespace App\Yr\Forecast\Tabular\Time;
 
+/**
+ * Custom unit, only used to operate on units of same kind
+ *
+ * Classes should transform this (see {@AbstractUnit}) to
+ * the correct class again after operation.
+ */
 class CustomUnit extends AbstractUnit {
-
+    // Nothing extra here
 }
 
 ?>

+ 1 - 0
src/Yr/Forecast/Tabular/Time/Pressure.php

@@ -6,6 +6,7 @@ namespace App\Yr\Forecast\Tabular\Time;
  * Airpressure
  *
  * @author Joachim M. Giæver (joachim[]giaever.org)
+ * @todo Conversion
  */
 class Pressure extends AbstractUnit {
     const NORMAL_PRESSURE = 1015.0;

+ 30 - 2
src/Yr/Forecast/Tabular/Time/Temperature.php

@@ -7,8 +7,10 @@ namespace App\Yr\Forecast\Tabular\Time;
  *
  * @author Joachim M. Giæver (joachim[]giaever.org)
  */
-class Temperature extends AbstractUnit {
-    const DEFAULT_VARIANCE = 2;
+class Temperature extends AbstractUnit implements ConvertableInterface {
+    const UNIT_CELCIUS      = 'celcius';
+    const UNIT_FAHRENHEIT   = 'fahrenheit';
+    const UNIT_KELVIN       = 'kelvin';
 
     /**
      * @param \SimpleXMLElement $xml XML containing the temperature
@@ -19,5 +21,31 @@ class Temperature extends AbstractUnit {
             (string)$xml['unit']
         );
     }
+
+    /**
+     * {@inheritDoc}
+     * @todo support conversion from other types, not just celcius
+     */
+    public function convertTo(string $unit): self {
+        switch ($unit) {
+        case UNIT_FAHRENHEIT:
+            return $this->mul(new CustomUnit(
+                9/5, $unit
+            ))->add(new CustomUnit(
+                32, $unit
+            ));
+        case UNIT_KELVIN:
+            return $this->add(new CustomUnit(
+                273.15, $unit
+            ));
+        }
+    }
+
+    /**
+     * Return the ⁰X symbol
+     */
+    public function getDegree(): string {
+        return sprintf("⁰%s", strtoupper(substr($this->getUnit(), 0, 1)));
+    }
 }
 

+ 21 - 2
src/Yr/Forecast/Tabular/Time/WindDirection.php

@@ -33,12 +33,31 @@ class WindDirection extends AbstractUnit{
         return $this->name;
     }
 
+    /**
+     * Returns a direction on the compass, not
+     * in degree, but in interger between 1 - 16, 
+     * each explining which spectre.
+     *
+     * E.g 
+     *  1: North,
+     *  2: North-northeast,
+     *  3: North-east,
+     *  [...]
+     *  9: South
+     * etc..
+     *
+     * @return int compass spectre
+     */
+    public function getDirection(): int {
+        return (($this->getValue()-11.5) / 22.5);
+    }
+
     /**
      * {@inheritDoc}
      */
     public function diff(DiffInterface $d): int {
-        if ($diff = parent::diff($d))
-            return $diff > static::DEFAULT_VARIANCE ? 1 : 0;
+        if (parent::diff($d))
+            return $this->getDirection() - $d->getDirection();
         return 0;
     }
 

+ 8 - 6
src/Yr/Forecast/Tabular/Time/WindSpeed.php

@@ -7,7 +7,7 @@ namespace App\Yr\Forecast\Tabular\Time;
  *
  * @author Joachim M. Giæver (joachim[]giaever.org)
  */
-class WindSpeed extends AbstractUnit {
+class WindSpeed extends AbstractUnit implements ConvertableInterface {
     const DEFAULT_VARIANCE = (4.9 / 3.5);
 
     const UNIT_MPS = 'mp/s';
@@ -29,9 +29,8 @@ class WindSpeed extends AbstractUnit {
     }
 
     /**
-     * Convert the wind speed to a different unit
-     *
-     * @param string $unit The unit to convert to, eg UNIT_FTS
+     * {@inheritDoc}
+     * @todo Support conversion from other types, not just mps
      */
     public function convertTo(string $unit): self {
         switch ($unit) {
@@ -51,6 +50,9 @@ class WindSpeed extends AbstractUnit {
         return $this->name;
     }
 
+    /**
+     * Used on conversion
+     */
     protected function setName(string $name): self {
         $this->name = $name;
         return $this;
@@ -60,8 +62,8 @@ class WindSpeed extends AbstractUnit {
      * {@inheritDoc}
      */
     public function diff(DiffInterface $e): int {
-        if (parent::diff($e))
-            return ($this->getName() != $e->getName()) ? 1 : 0;
+        if ($diff = parent::diff($e))
+            return $this->getName() == $e->getName() ? 0 : $diff;
         return 0;
     }
 

+ 60 - 0
src/Yr/Forecast/Tabular/Variation/Variation.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace App\Yr\Forecast\Tabular\Variation;
+
+use App\Yr\Forecast\Tabular\Time;
+use App\Yr\Forecast\Tabular\Time\DiffInterface;
+
+if (!function_exists('array_key_last')) {
+    function array_key_last(array $args): ?int {
+        $keys = array_keys($args);
+        return array_pop($keys);
+    }
+}
+
+class Variation implements \IteratorAggregate {
+
+    private $time;
+    private $entities = [];
+    private $intersects = [];
+
+    public function __construct(Time $t) {
+        $this->time = $t;
+    }
+
+    public function addEntity(DiffInterface $entity, ?DiffInterface $intersects): self {
+        $this->entities[] = $entity;
+
+        if ($intersects != null)
+            $this->intersects[array_key_last($this->entities)] = $intersects;
+
+        return $this;
+    }
+
+    public function getTime(): Time {
+        return $this->time;
+    }
+
+    public function getIntersection(DiffInterface $entity): ?DiffInterface {
+        $key = (function() use ($entity) : int {
+            foreach ($this->entities as $key => $ent)
+                if ($ent == $entity)
+                    return $key;
+        })();
+
+        if (isset($this->intersects[$key]))
+            return $this->intersects[$key];
+
+        return null;
+    }
+
+    public function isEmpty(): bool {
+        return empty($this->entities);
+    }
+
+    public function getIterator(): \Generator {
+        foreach ($this->entities as $entity)
+            yield $entity;
+    }
+
+}

+ 39 - 19
src/Yr/Forecast/Tabular/Variations.php

@@ -2,6 +2,9 @@
 
 namespace App\Yr\Forecast\Tabular;
 
+use App\Yr\Forecast\Tabular\Time\DiffInterface;
+use App\Yr\Forecast\Tabular\Variation\Variation;
+
 /**
  * Removes superfluous forecast data in an Time-object
  * only storing changes.
@@ -10,43 +13,60 @@ namespace App\Yr\Forecast\Tabular;
  */
 class Variations {
 
+    private $time;
     private $data = [];
 
     /**
      * @param array $t Array of Time-objects
      */
     public function __construct(array $t) {
-        if (!current($t) instanceof Time)
+        $time = array_shift($t);
+
+        if (!$time instanceof Time)
             return;
 
-        foreach ($t as $time)
-            $this->determineVariance($time);
+        $this->time = $time;
+        $var = new Variation($time);
+
+        foreach($time as $entity)
+            $var->addEntity($entity, null);
+
+        $this->data[] = $var;
+
+        foreach ($t as $time) {
+            $var = new Variation($time);
+
+            foreach($time as $entity)
+                $this->match($var, $entity);
+
+            if (!$var->isEmpty())
+                $this->data[] = $var;
+
+        }
+
+        array_shift($this->data);
     }
 
-    private function determineVariance(Time $t) {
-        $var = [
-            'time' => $t,
-            'entities' => iterator_to_array($t->getIterator())
-        ];
-
-        if (empty($this->data))
-            $this->data[] = $var;
-        else {
-            foreach (array_reverse($this->data) as $entry) {
-                foreach ($entry['entities'] as $entity) {
-                    foreach ($var['entities'] as $key => $ventity)
-                        if ($ventity instanceof $entity && !$ventity->diff($entity))
-                            unset($var['entities'][$key]);
+    private function match(Variation $var, DiffInterface $entity): void {
+        foreach(array_reverse($this->data) as $data) {
+            foreach ($data as $dentity) {
+                if ($entity instanceof $dentity) {
+                    if ($entity->diff($dentity))
+                        $var->addEntity($entity, $dentity);
+                    return;
                 }
             }
-            if (!empty($var['entities']))
-                $this->data[] = $var;
         }
     }
 
+    public function getTime(): Time {
+        return $this->time;
+    }
+
     /**
      * Returns the changes in the forecast
      *
+     * @todo implement filter function
      * @return array 
      */
     public function getData(callable $filterFn = null): array {