PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.7
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.7
4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 4.1.7.3.1 4.1.7.3.2 4.2.0 4.2.1 All 138 releases
learnpress / inc / Statistics / PeriodHelper.php

PeriodHelper.php in LearnPress – WordPress LMS Plugin for Create and Sell Online Courses 4.4.7, at inc/Statistics/PeriodHelper.php

179 lines 5.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class PeriodHelper
4 *
5 * @package LearnPress/Classes/Statistics
6 * @since 4.4.2
7 */
8
9 namespace LearnPress\Statistics;
10
11 use DateTimeImmutable;
12 use Exception;
13
14 defined( 'ABSPATH' ) || exit();
15
16 /**
17 * Maps a controller statistics filter ( filter_type + time ) to the equivalent
18 * previous window and builds KPI payloads with previous-period comparison.
19 *
20 * Fail-soft contract: unmappable or invalid input returns null so a KPI card
21 * simply renders without a delta — never a wrong one.
22 *
23 * @since 4.4.2
24 */
25 class PeriodHelper {
26 /**
27 * Map a statistics filter to the window immediately before it, same length.
28 *
29 * date → the previous day
30 * previous_days N → the N + 1 days before the current [ today - N … today ] window, as custom
31 * month → the previous calendar month
32 * previous_months N → the N + 1 months before the current window, as custom
33 * year → the previous calendar year
34 * custom A+B → the same-length range ending the day before A
35 *
36 * @param array $filter [ 'filter_type' => string, 'time' => string|int ] as built by get_statistics_filter().
37 * @param string $today 'Y-m-d' anchor for the relative types; defaults to WP current_time (injectable for tests).
38 * @return array|null Same shape as $filter, or null when unmappable.
39 */
40 public static function get_previous_filter( array $filter, string $today = '' ): ?array {
41 $type = $filter['filter_type'] ?? '';
42 $time = $filter['time'] ?? '';
43
44 if ( '' === $time || ( ! is_string( $time ) && ! is_int( $time ) ) ) {
45 return null;
46 }
47
48 try {
49 switch ( $type ) {
50 case 'date':
51 $date = new DateTimeImmutable( (string) $time );
52
53 return [
54 'filter_type' => 'date',
55 'time' => $date->modify( '-1 day' )->format( 'Y-m-d' ),
56 ];
57
58 case 'previous_days':
59 $days = (int) $time;
60 if ( $days < 2 ) {
61 // Mirrors the previous_days_filter() constraint — fail soft here.
62 return null;
63 }
64 // Current window is [ today - N … today ] inclusive → N + 1 days.
65 $anchor = new DateTimeImmutable( self::today( $today ) );
66 $end = $anchor->modify( sprintf( '-%d days', $days + 1 ) );
67 $start = $end->modify( sprintf( '-%d days', $days ) );
68
69 return [
70 'filter_type' => 'custom',
71 'time' => $start->format( 'Y-m-d' ) . '+' . $end->format( 'Y-m-d' ),
72 ];
73
74 case 'month':
75 $date = new DateTimeImmutable( (string) $time );
76 // Anchor to day 1 first: "Jan 31 - 1 month" would overflow past February.
77 $first = $date->modify( 'first day of this month' );
78
79 return [
80 'filter_type' => 'month',
81 'time' => $first->modify( '-1 month' )->format( 'Y-m-d' ),
82 ];
83
84 case 'previous_months':
85 $months = (int) $time;
86 if ( $months < 2 ) {
87 // Mirrors the previous_months_filter() constraint — fail soft here.
88 return null;
89 }
90 // Current window is [ month( today ) - N … month( today ) ] → N + 1 months.
91 $anchor = ( new DateTimeImmutable( self::today( $today ) ) )->modify( 'first day of this month' );
92 $end = $anchor->modify( sprintf( '-%d months', $months + 1 ) )->modify( 'last day of this month' );
93 $start = $anchor->modify( sprintf( '-%d months', ( 2 * $months ) + 1 ) );
94
95 return [
96 'filter_type' => 'custom',
97 'time' => $start->format( 'Y-m-d' ) . '+' . $end->format( 'Y-m-d' ),
98 ];
99
100 case 'year':
101 $date = new DateTimeImmutable( (string) $time );
102
103 return [
104 'filter_type' => 'year',
105 'time' => ( (int) $date->format( 'Y' ) - 1 ) . '-01-01',
106 ];
107
108 case 'custom':
109 $dates = explode( '+', (string) $time );
110 if ( 2 !== count( $dates ) ) {
111 return null;
112 }
113 sort( $dates );
114 $start = new DateTimeImmutable( $dates[0] );
115 $end = new DateTimeImmutable( $dates[1] );
116 $length = (int) $start->diff( $end )->format( '%a' ) + 1;
117 $prev_end = $start->modify( '-1 day' );
118 $prev_start = $prev_end->modify( sprintf( '-%d days', $length - 1 ) );
119
120 return [
121 'filter_type' => 'custom',
122 'time' => $prev_start->format( 'Y-m-d' ) . '+' . $prev_end->format( 'Y-m-d' ),
123 ];
124 }
125 } catch ( Exception $e ) {
126 return null;
127 }
128
129 return null;
130 }
131
132 /**
133 * Build the KPI payload comparing a current value against the previous period.
134 *
135 * change_pct is null when there is no previous value or it is zero — the UI
136 * hides the delta instead of showing a division-by-zero artifact.
137 *
138 * @param int|float|string|null $value Current-period value.
139 * @param int|float|string|null $prev Previous-period value; null when no previous window.
140 * @return array [ 'value' => number, 'prev_value' => number|null, 'change_pct' => float|null ]
141 */
142 public static function kpi_payload( $value, $prev ): array {
143 $value = is_numeric( $value ) ? $value + 0 : 0;
144
145 $payload = [
146 'value' => $value,
147 'prev_value' => null,
148 'change_pct' => null,
149 ];
150
151 if ( ! is_numeric( $prev ) ) {
152 return $payload;
153 }
154
155 $prev = $prev + 0;
156 $payload['prev_value'] = $prev;
157
158 if ( 0 == $prev ) {
159 return $payload;
160 }
161
162 $payload['change_pct'] = round( ( $value - $prev ) / $prev * 100, 1 );
163
164 return $payload;
165 }
166
167 /**
168 * @param string $today Injected 'Y-m-d' anchor or '' for WP current time.
169 * @return string
170 */
171 private static function today( string $today ): string {
172 if ( '' !== $today ) {
173 return $today;
174 }
175
176 return current_time( 'Y-m-d' );
177 }
178 }
179