}> */ public static function scales() { return array( 'p10' => array( 'max' => 10, 'steps' => array( 0.1, 0.5, 1.0 ), ), 's10' => array( 'max' => 10, 'steps' => array( 0.1, 0.5, 1.0 ), ), 's5' => array( 'max' => 5, 'steps' => array( 0.1, 0.5, 1.0 ), ), 'pct' => array( 'max' => 100, 'steps' => array( 1.0 ), ), ); } /** * Maximum einer Skala. * * @param string $scale Skalenschlüssel. * @return int|null */ public static function max( $scale ) { $scales = self::scales(); return isset( $scales[ $scale ] ) ? $scales[ $scale ]['max'] : null; } /** * Gesamtnote einer Box. * * Reihenfolge: von Hand gesetzt (nur mit Begründung), sonst aus Kriterien, sonst die eingetragene Note. * * @param array $box Box-Daten (scale, step, criteria, score, override). * @return array{value:float|null, share:float|null, calculated:bool, overridden:bool} */ public static function total( array $box ) { $leer = array( 'value' => null, 'share' => null, 'calculated' => false, 'overridden' => false, ); $max = self::max( isset( $box['scale'] ) ? $box['scale'] : '' ); if ( null === $max ) { return $leer; } $step = self::step_t( $box ); if ( isset( $box['override']['value'], $box['override']['reason'] ) && '' !== trim( (string) $box['override']['reason'] ) && is_numeric( $box['override']['value'] ) ) { $wert = self::round_t( self::to_t( $box['override']['value'] ), 1, $step ); return self::ergebnis( $wert, $max, false, true ); } $kriterien = self::bewertete( isset( $box['criteria'] ) ? $box['criteria'] : array() ); if ( $kriterien ) { $wert = self::mittel( $kriterien, null, $step ); return self::ergebnis( $wert, $max, true, false ); } if ( isset( $box['score'] ) && is_numeric( $box['score'] ) ) { $wert = self::round_t( self::to_t( $box['score'] ), 1, $step ); return self::ergebnis( $wert, $max, false, false ); } return $leer; } /** * Rastet eine Eingabe auf den Schritt der Skala ein. * * @param mixed $value Eingabe. * @param string $scale Skala. * @param float $step Schritt. * @return float|null Null bei negativen Werten, Werten über dem Maximum oder unbekannter Skala. */ public static function snap( $value, $scale, $step ) { $max = self::max( $scale ); if ( null === $max || ! is_numeric( $value ) ) { return null; } $wert_t = self::to_t( $value ); if ( $wert_t < 0 || $wert_t > $max * 1000 ) { return null; } $step_t = self::step_t( array( 'scale' => $scale, 'step' => $step, ) ); return self::von_t( self::round_t( $wert_t, 1, $step_t ) ); } /** * Note nach den Gewichten des Lesers („Deine Gewichtung“), Schritt 0,1 (Prozent: 1). * * @param array $box Box-Daten. * @param array $weights Gewichte in der Reihenfolge der Kriterien. * @return float|null Null, wenn alle Gewichte 0 sind. */ public static function personal( array $box, array $weights ) { $kriterien = self::bewertete( isset( $box['criteria'] ) ? $box['criteria'] : array() ); $gewichte = array(); foreach ( array_keys( $kriterien ) as $i ) { $gewichte[ $i ] = isset( $weights[ $i ] ) ? max( 0, (int) $weights[ $i ] ) : 0; } if ( ! $kriterien || 0 === array_sum( $gewichte ) ) { return null; } $step = ( isset( $box['scale'] ) && 'pct' === $box['scale'] ) ? 1000 : 100; return self::von_t( self::mittel( $kriterien, $gewichte, $step ) ); } /** * Wortnote aus dem Anteil (0 bis 1). * * @param float $share Anteil der Note am Skalenmaximum. * @return string Schlüssel der Wortnote. */ public static function word( $share ) { $z = (int) round( (float) $share * 10000 ); if ( $z >= 9000 ) { return 'outstanding'; } if ( $z >= 8000 ) { return 'very_good'; } if ( $z >= 7000 ) { return 'good'; } if ( $z >= 6000 ) { return 'satisfactory'; } if ( $z >= 5000 ) { return 'sufficient'; } return 'poor'; } /** * Rechnet alle Noten einer Box verhältnisgleich in eine andere Skala um. * * @param array $box Box-Daten. * @param string $scale Neue Skala. * @param float $step Neue Schrittweite. * @return array Box mit neuer Skala, neuem Schritt und umgerechneten Noten. */ public static function convert( array $box, $scale, $step ) { $alt = self::max( isset( $box['scale'] ) ? $box['scale'] : '' ); $neu = self::max( $scale ); if ( null === $alt || null === $neu ) { return $box; } $box['scale'] = $scale; $box['step'] = (float) $step; $step_t = self::step_t( $box ); $um = function ( $wert ) use ( $alt, $neu, $step_t ) { return self::von_t( self::round_t( self::to_t( $wert ) * $neu, $alt, $step_t ) ); }; if ( ! empty( $box['criteria'] ) ) { foreach ( $box['criteria'] as $i => $k ) { if ( isset( $k['score'] ) && is_numeric( $k['score'] ) ) { $box['criteria'][ $i ]['score'] = $um( $k['score'] ); } } } if ( isset( $box['score'] ) && is_numeric( $box['score'] ) ) { $box['score'] = $um( $box['score'] ); } if ( isset( $box['override']['value'] ) && is_numeric( $box['override']['value'] ) ) { $box['override']['value'] = $um( $box['override']['value'] ); } return $box; } /** * Kriterien mit gültiger Note, Schlüssel bleiben erhalten. * * @param array $criteria Kriterien. * @return array */ private static function bewertete( array $criteria ) { $liste = array(); foreach ( array_values( $criteria ) as $i => $k ) { if ( isset( $k['score'] ) && is_numeric( $k['score'] ) ) { $liste[ $i ] = array( 'weight' => isset( $k['weight'] ) ? max( 0, (int) $k['weight'] ) : 0, 'score' => self::to_t( $k['score'] ), ); } } return $liste; } /** * Gewichteter Mittelwert in Tausendsteln, gerundet auf den Schritt. * * @param array $kriterien Kriterien mit score in Tausendsteln. * @param array|null $gewichte Eigene Gewichte oder null für die der Kriterien. * @param int $step Schritt in Tausendsteln. * @return int */ private static function mittel( array $kriterien, $gewichte, $step ) { $summe = 0; $nenner = 0; foreach ( $kriterien as $i => $k ) { $nenner += null === $gewichte ? $k['weight'] : $gewichte[ $i ]; } $gleich = 0 === $nenner; $nenner = 0; foreach ( $kriterien as $i => $k ) { $w = $gleich ? 1 : ( null === $gewichte ? $k['weight'] : $gewichte[ $i ] ); $summe += $w * $k['score']; $nenner += $w; } return self::round_t( $summe, $nenner, $step ); } /** * Rundet zaehler/nenner (Tausendstel) kaufmännisch auf ein Vielfaches des Schritts. * * @param int $zaehler Zähler in Tausendsteln. * @param int $nenner Nenner. * @param int $step Schritt in Tausendsteln. * @return int */ private static function round_t( $zaehler, $nenner, $step ) { if ( $zaehler <= 0 || $nenner <= 0 ) { return 0; } return intdiv( 2 * $zaehler + $nenner * $step, 2 * $nenner * $step ) * $step; } /** * Ergebnis-Array. * * @param int $wert_t Note in Tausendsteln. * @param int $max Skalenmaximum. * @param bool $calculated Aus Kriterien berechnet. * @param bool $overridden Von Hand gesetzt. * @return array */ private static function ergebnis( $wert_t, $max, $calculated, $overridden ) { $wert_t = min( $wert_t, $max * 1000 ); return array( 'value' => self::von_t( $wert_t ), 'share' => round( $wert_t / ( $max * 1000 ), 4 ), 'calculated' => $calculated, 'overridden' => $overridden, ); } /** * Schritt der Box in Tausendsteln, Vorgabe 0,1 (Prozent: 1). * * @param array $box Box-Daten. * @return int */ private static function step_t( array $box ) { $scales = self::scales(); $scale = isset( $box['scale'], $scales[ $box['scale'] ] ) ? $box['scale'] : 'p10'; $erlaubt = array_map( array( __CLASS__, 'to_t' ), $scales[ $scale ]['steps'] ); $step = isset( $box['step'] ) && is_numeric( $box['step'] ) ? self::to_t( $box['step'] ) : $erlaubt[0]; return in_array( $step, $erlaubt, true ) ? $step : $erlaubt[0]; } /** * Zahl in Tausendstel. * * @param mixed $wert Zahl. * @return int */ private static function to_t( $wert ) { return (int) round( (float) $wert * 1000 ); } /** * Tausendstel in Zahl. * * @param int $wert_t Tausendstel. * @return float */ private static function von_t( $wert_t ) { return round( $wert_t / 1000, 3 ); } }