PluginProbe ʕ •ᴥ•ʔ
Pods – Custom Content Types and Fields / 2.8.23.4
Pods – Custom Content Types and Fields v2.8.23.4
2.7.31.4 2.8.23.5 2.9.19.5 3.0.10.5 3.1.4.3 3.2.8.4 3.3.9.2 2.8.23.4 2.9.19.4 3.0.10.4 3.1.4.2 3.2.8.3 3.3.9.1 trunk 1.14.8 2.7.31.3 2.8.23.3 2.9.19.3 3.0.10.3 3.1.4.1 3.2.0 3.2.1 3.2.1.1 3.2.2 3.2.4 3.2.5 3.2.6 3.2.7 3.2.7.1 3.2.8 3.2.8.1 3.2.8.2 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.3.8 3.3.9
pods / tribe-common / src / Tribe / Cost_Utils.php
pods / tribe-common / src / Tribe Last commit date
Admin 2 weeks ago Ajax 2 weeks ago Asset 2 weeks ago Context 2 weeks ago Customizer 2 weeks ago Debug_Bar 2 weeks ago Dialog 2 weeks ago Documentation 2 weeks ago Duplicate 2 weeks ago Editor 2 weeks ago Image 2 weeks ago JSON_LD 2 weeks ago Languages 2 weeks ago Log 2 weeks ago Meta 2 weeks ago Models 2 weeks ago PUE 2 weeks ago Process 2 weeks ago Promoter 2 weeks ago REST 2 weeks ago Repository 2 weeks ago Service_Providers 2 weeks ago Shortcode 2 weeks ago Support 2 weeks ago Tabbed_View 2 weeks ago Tooltip 2 weeks ago Traits 2 weeks ago Utils 2 weeks ago Validator 2 weeks ago Widget 2 weeks ago Abstract_Deactivation.php 2 weeks ago Abstract_Plugin_Register.php 2 weeks ago App_Shop.php 2 weeks ago Assets.php 2 weeks ago Assets_Pipeline.php 2 weeks ago Autoloader.php 2 weeks ago Cache.php 2 weeks ago Cache_Listener.php 2 weeks ago Changelog_Reader.php 2 weeks ago Container.php 2 weeks ago Context.php 2 weeks ago Cost_Utils.php 2 weeks ago Credits.php 2 weeks ago Customizer.php 2 weeks ago DB_Lock.php 2 weeks ago Data.php 2 weeks ago Date_Utils.php 2 weeks ago Db.php 2 weeks ago Debug.php 2 weeks ago Dependency.php 2 weeks ago Deprecation.php 2 weeks ago Editor.php 2 weeks ago Error.php 2 weeks ago Exception.php 2 weeks ago Extension.php 2 weeks ago Extension_Loader.php 2 weeks ago Feature_Detection.php 2 weeks ago Field.php 2 weeks ago Field_Conditional.php 2 weeks ago Freemius.php 2 weeks ago Log.php 2 weeks ago Main.php 2 weeks ago Notices.php 2 weeks ago Plugin_Meta_Links.php 2 weeks ago Plugins.php 2 weeks ago Plugins_API.php 2 weeks ago Post_History.php 2 weeks ago Post_Transient.php 2 weeks ago Promise.php 2 weeks ago Repository.php 2 weeks ago Rewrite.php 2 weeks ago Settings.php 2 weeks ago Settings_Manager.php 2 weeks ago Settings_Tab.php 2 weeks ago Simple_Table.php 2 weeks ago Support.php 2 weeks ago Tabbed_View.php 2 weeks ago Template.php 2 weeks ago Template_Factory.php 2 weeks ago Template_Part_Cache.php 2 weeks ago Templates.php 2 weeks ago Terms.php 2 weeks ago Timezones.php 2 weeks ago Tracker.php 2 weeks ago Updater.php 2 weeks ago Validate.php 2 weeks ago View_Helpers.php 2 weeks ago
Cost_Utils.php
543 lines
1 <?php
2
3
4 /**
5 * Class Tribe__Cost_Utils
6 *
7 * Utility methods to deal with costs.
8 *
9 * @since 4.3
10 */
11 class Tribe__Cost_Utils {
12
13 /**
14 * @var string
15 */
16 protected $_current_original_cost_separator;
17
18 /**
19 * @var string
20 */
21 protected $_supported_decimal_separators = '.,';
22
23 /**
24 * Static Singleton Factory Method
25 *
26 * @return Tribe__Events__Cost_Utils
27 */
28 public static function instance() {
29 static $instance;
30
31 if ( ! $instance ) {
32 $instance = new self;
33 }
34
35 return $instance;
36 }
37
38 /**
39 * Check if a string is a valid cost.
40 *
41 * @param string $cost String to be checked.
42 * Can include decimal and thousands separator.
43 *
44 * @return boolean
45 */
46 public function is_valid_cost( $cost, $allow_negative = true ) {
47 return preg_match( $this->get_cost_regex(), trim( $cost ) );
48 }
49
50 /**
51 * Returns the regular expression that shold be used to identify a valid
52 * cost string.
53 *
54 * @return string
55 */
56 public function get_cost_regex() {
57 $separators = '[\\' . implode( '\\', $this->get_separators() ) . ']?';
58 $cost_regex = '(' . $separators . '([\d]+)' . $separators . '([\d]*))';
59
60 /**
61 * Filters the regular expression that will be used to identify a valid cost
62 * string.
63 *
64 * @param string $cost_regex
65 *
66 * @deprecated 4.3 Use `tribe_cost_regex` instead
67 */
68 $cost_regex = apply_filters(
69 'tribe_events_cost_regex', $cost_regex
70 );
71
72 /**
73 * Filters the regular expression that will be used to identify a valid cost
74 * string.
75 *
76 * @param string $cost_regex
77 */
78 $cost_regex = apply_filters( 'tribe_cost_regex', $cost_regex );
79
80 return $cost_regex;
81 }
82
83 /**
84 * Fetch the possible separators
85 *
86 * @return array
87 */
88 public function get_separators() {
89 $separators = [ ',', '.' ];
90
91 /**
92 * Filters the cost string possible separators, those must be only 1 char.
93 *
94 * @param array $separators Defaults to comma (",") and period (".")
95 */
96 return apply_filters( 'tribe_events_cost_separators', $separators );
97 }
98
99 /**
100 * If the cost is "0", call it "Free"
101 *
102 * @param int|float|string $cost Cost to analyze
103 *
104 * @return int|float|string
105 */
106 public function maybe_replace_cost_with_free( $cost ) {
107
108 $cost_with_period = $this->convert_decimal_separator( $cost );
109
110 if (
111 is_numeric( $cost_with_period )
112 && '0.00' === number_format( $cost_with_period, 2, '.', ',' )
113 ) {
114 return esc_html__( 'Free', 'the-events-calendar' );
115 }
116
117 return $cost;
118 }
119
120 /**
121 * Formats a cost with a currency symbol
122 *
123 * @param int|float|string $cost Cost to format
124 *
125 * return string
126 * @param int|WP_Post $event An event post ID or post object.
127 * @param string $currency_symbol
128 * @param string $currency_position Either "prefix" or "posfix"
129 *
130 * @return float|int|string
131 */
132 public function maybe_format_with_currency( $cost, $event = null, $currency_symbol = null, $currency_position = null ) {
133 // check if the currency symbol is desired, and it's just a number in the field
134 // be sure to account for european formats in decimals, and thousands separators
135 if ( is_numeric( str_replace( $this->get_separators(), '', $cost ) ) ) {
136 $reverse_position = null;
137 // currency_position often gets passed as null or an empty string.
138 if ( ! empty( $currency_position ) ) {
139 $reverse_position = 'prefix' === $currency_position ? false : true;
140 }
141
142 $cost = tribe_format_currency( $cost, $event, $currency_symbol, $reverse_position );
143 }
144
145 return $cost;
146 }
147
148 /**
149 * @param string $original_string_cost A string cost with or without currency symbol,
150 * e.g. `10 - 20`, `Free` or `2$ - 4$`.
151 * @param array|string $merging_cost A single string cost representation to merge or an array of
152 * string cost representations to merge, e.g. ['Free', 10, 20,
153 * 'Donation'] or `Donation`.
154 * @param bool $with_currency_symbol Whether the output should prepend the currency symbol to the
155 * numeric costs or not.
156 * @param array $sorted_mins An array of non numeric price minimums sorted smaller to larger,
157 * e.g. `['Really free', 'Somewhat free', 'Free with 3 friends']`.
158 * @param array $sorted_maxs An array of non numeric price maximums sorted smaller to larger,
159 * e.g. `['Donation min $10', 'Donation min $20', 'Donation min
160 * $100']`.
161 *
162 * @return string|array The merged cost range.
163 */
164 public function merge_cost_ranges( $original_string_cost, $merging_cost, $with_currency_symbol, $sorted_mins = [], $sorted_maxs = [] ) {
165 if ( empty( $merging_cost ) || $original_string_cost === $merging_cost ) {
166 return $original_string_cost;
167 }
168
169 $_merging_cost = array_map(
170 [ $this, 'convert_decimal_separator' ], (array) $merging_cost
171 );
172 $_merging_cost = array_map( [ $this, 'numerize_numbers' ], $_merging_cost );
173 $numeric_merging_cost_costs = array_filter( $_merging_cost, 'is_numeric' );
174
175 $matches = [];
176 preg_match_all(
177 '!\d+(?:([' . preg_quote( $this->_supported_decimal_separators ) . '])\d+)?!', $original_string_cost,
178 $matches
179 );
180 $this->_current_original_cost_separator = empty( $matches[1][0] ) ? '.' : $matches[1][0];
181 $matches[0] = empty( $matches[0] )
182 ? $matches[0]
183 : array_map(
184 [
185 $this,
186 'convert_decimal_separator',
187 ],
188 $matches[0]
189 );
190
191 $numeric_orignal_costs = empty( $matches[0] ) ? $matches[0] : array_map(
192 'floatval', $matches[0]
193 );
194
195 $all_numeric_costs = array_filter( array_merge( $numeric_merging_cost_costs, $numeric_orignal_costs ) );
196 $cost_min = $cost_max = false;
197
198 $merging_mins = array_intersect( $sorted_mins, (array) $merging_cost );
199 $merging_has_min = array_search( reset( $merging_mins ), $sorted_mins );
200 $original_has_min = array_search( $original_string_cost, $sorted_mins );
201 $merging_has_min = false === $merging_has_min ? 999 : $merging_has_min;
202 $original_has_min = false === $original_has_min ? 999 : $original_has_min;
203 $string_min_key = min( $merging_has_min, $original_has_min );
204 if ( array_key_exists( $string_min_key, $sorted_mins ) ) {
205 $cost_min = $sorted_mins[ $string_min_key ];
206 } else {
207 $cost_min = empty( $all_numeric_costs ) ? '' : min( $all_numeric_costs );
208 }
209
210 $merging_maxs = array_intersect( $sorted_maxs, (array) $merging_cost );
211 $merging_has_max = array_search( end( $merging_maxs ), $sorted_maxs );
212 $original_has_max = array_search( $original_string_cost, $sorted_maxs );
213 $merging_has_max = false === $merging_has_max ? - 1 : $merging_has_max;
214 $original_has_max = false === $original_has_max ? - 1 : $original_has_max;
215 $string_max_key = max( $merging_has_max, $original_has_max );
216 if ( array_key_exists( $string_max_key, $sorted_maxs ) ) {
217 $cost_max = $sorted_maxs[ $string_max_key ];
218 } else {
219 $cost_max = empty( $all_numeric_costs ) ? '' : max( $all_numeric_costs );
220 }
221
222 $cost = array_filter( [ $cost_min, $cost_max ] );
223
224 if ( $with_currency_symbol ) {
225 $formatted_cost = [];
226 foreach ( $cost as $c ) {
227 $formatted_cost[] = is_numeric( $c ) ? tribe_format_currency( $c ) : $c;
228 }
229 $cost = $formatted_cost;
230 }
231
232 return empty( $cost ) ? $original_string_cost : array_map(
233 [ $this, 'restore_original_decimal_separator' ],
234 $cost
235 );
236 }
237
238 /**
239 * Returns a maximum cost in a list of costs. If an array of costs is not passed in, the array of costs is fetched
240 * via query.
241 *
242 * @param $costs mixed Cost(s) to review for max value
243 *
244 * @return float
245 */
246 public function get_maximum_cost( $costs = null ) {
247 return $this->get_cost_by_func( $costs, 'max' );
248 }
249
250 /**
251 * Returns a particular cost within an array of costs
252 *
253 * @param $costs mixed Cost(s) to review for max value
254 * @param $function string Function to use to determine which cost to return from range. Valid values: max, min
255 *
256 * @return float
257 */
258 protected function get_cost_by_func( $costs = null, $function = 'max' ) {
259 if ( null === $costs ) {
260 $costs = $this->get_all_costs();
261 } else {
262 $costs = (array) $costs;
263 }
264
265 $costs = $this->parse_cost_range( $costs );
266
267 // if there's only one item, we're looking at a single event. If the cost is non-numeric, let's
268 // return the non-numeric cost so that value is preserved
269 if ( 1 === count( $costs ) && ! is_numeric( current( $costs ) ) ) {
270 return current( $costs );
271 }
272
273 // make sure we are only trying to get numeric min/max values
274 $costs = array_filter( $costs, 'is_numeric' );
275
276 if ( empty( $costs ) ) {
277 return 0;
278 }
279
280 switch ( $function ) {
281 case 'min':
282 $cost = $costs[ min( array_keys( $costs ) ) ];
283 break;
284 case 'max':
285 default:
286 $cost = $costs[ max( array_keys( $costs ) ) ];
287 break;
288 }
289
290 // If there isn't anything on the cost just return 0
291 if ( empty( $cost ) ) {
292 return 0;
293 }
294
295 return $cost;
296 }
297
298 /**
299 * Parses a cost into an array of ranges.
300 *
301 * If a range isn't provided, the resulting array will hold a single
302 * value.
303 *
304 * @param string|array $costs A cost string or an array of cost strings.
305 * @param null $max_decimals The maximum number of decimal values that should be returned in the range.
306 * @param bool $sort Whether the returned values should be sorted.
307 *
308 * @return array An associative array of parsed costs in [ <string cost> => <cost number> ] format.
309 */
310 public function parse_cost_range( $costs, $max_decimals = null, $sort = true ) {
311 if ( ! is_array( $costs ) && ! is_string( $costs ) ) {
312 return [];
313 }
314
315 // make sure costs is an array
316 $costs = (array) $costs;
317
318 // If there aren't any costs, return a blank array
319 if ( 0 === count( $costs ) ) {
320 return [];
321 }
322
323 // Build the regular expression
324 $price_regex = $this->get_cost_regex();
325 $max = 0;
326
327 foreach ( $costs as &$cost ) {
328 // Get the required parts
329 if ( preg_match_all( '/' . $price_regex . '/', $cost, $matches ) ) {
330 $cost = reset( $matches );
331 } else {
332 $cost = [ $cost ];
333 continue;
334 }
335
336 // Get the max number of decimals for the range
337 if ( count( $matches ) === 4 ) {
338 $decimals = max( array_map( 'strlen', end( $matches ) ) );
339 $max = max( $max, $decimals );
340 }
341 }
342
343 // If we passed max decimals
344 if ( ! is_null( $max_decimals ) ) {
345 $max = max( $max_decimals, $max );
346 }
347
348 $output_costs = [];
349 $costs = call_user_func_array( 'array_merge', $costs );
350
351 foreach ( $costs as $cost ) {
352 $numeric_cost = str_replace( $this->get_separators(), '.', $cost );
353
354 if ( is_numeric( $numeric_cost ) ) {
355 // Creates a Well Balanced Index that will perform good on a Key Sorting method
356 $index = str_replace( [ '.', ',' ], '', number_format( $numeric_cost, $max ) );
357 } else {
358 // Makes sure that we have "index-safe" string
359 $index = sanitize_title( $numeric_cost );
360 }
361
362 // Keep the Costs in a organizable array by keys with the "numeric" value
363 $output_costs[ $index ] = $cost;
364 }
365
366 // Filter keeping the Keys
367 if ( $sort ) {
368 ksort( $output_costs );
369 }
370
371 return (array) $output_costs;
372 }
373
374 /**
375 * Returns a minimum cost in a list of costs. If an array of costs is not passed in, the array of costs is fetched
376 * via query.
377 *
378 * @param $costs mixed Cost(s) to review for min value
379 *
380 * @return float
381 */
382 public function get_minimum_cost( $costs = null ) {
383 return $this->get_cost_by_func( $costs, 'min' );
384 }
385
386 /**
387 * Converts the original decimal separator to ".".
388 *
389 * @param string|int $value
390 *
391 * @return string
392 */
393 protected function convert_decimal_separator( $value ) {
394 return preg_replace( '/[' . preg_quote( $this->_supported_decimal_separators ) . ']/', '.', $value );
395 }
396
397 /**
398 * Restores the decimal separator to its original symbol.
399 *
400 * @param string $value
401 *
402 * @return string
403 */
404 protected function restore_original_decimal_separator( $value ) {
405 return str_replace( '.', $this->_current_original_cost_separator, $value );
406 }
407
408 /**
409 * Extracts int and floats from a numeric "dirty" string like strings that might contain other symbols.
410 *
411 * E.g. "$10" will yield "10"; "23.55$" will yield "23.55".
412 *
413 * @param string|int $value
414 *
415 * @return int|float
416 */
417 protected function numerize_numbers( $value ) {
418 $matches = [];
419
420 $pattern = '/(\\d{1,}([' . $this->_supported_decimal_separators . ']\\d{1,}))/';
421
422 return preg_match( $pattern, $value, $matches ) ? $matches[1] : $value;
423 }
424
425 /**
426 * Parses the currency symbol part of a cost string.
427 *
428 * @param string|array $cost A string cost, a comma separated array of string costs or an array of costs.
429 *
430 * @return false|string Either the inferred currency symbol or `false` if the currency symbol is missing or not consistent.
431 */
432 public function parse_currency_symbol( $cost ) {
433 if ( empty( $cost ) ) {
434 return false;
435 }
436
437 $original_costs = is_array( $cost ) ? $cost : preg_split( '/\\s*,\\s*/', $cost );
438 $costs = $this->parse_cost_range( $original_costs, null, false );
439
440 if ( empty( $costs ) ) {
441 return false;
442 }
443
444 $currency_symbols = [];
445 $i = 0;
446 foreach ( $costs as $string => $value ) {
447 if ( is_numeric( $string ) ) {
448 $currency_symbols[] = trim( str_replace( $value, '', $original_costs[ $i ] ) );
449 if ( end( $currency_symbols ) !== reset( $currency_symbols ) ) {
450 return false;
451 }
452 }
453
454 $i ++;
455 }
456
457 return ! empty( $currency_symbols ) ? reset( $currency_symbols ) : false;
458 }
459
460 /**
461 * Parses the currency symbol position part of a cost string.
462 *
463 * @param string|array $cost A string cost, a comma separated array of string costs or an array of costs.
464 *
465 * @return false|string Either the inferred currency symbol position or `false` if not present or not consistent.
466 */
467 public function parse_currency_position( $cost ) {
468 if ( empty( $cost ) ) {
469 return false;
470 }
471
472 $original_costs = is_array( $cost ) ? $cost : preg_split( '/\\s*,\\s*/', $cost );
473 $currency_symbol = $this->parse_currency_symbol( $original_costs );
474
475 if ( empty( $currency_symbol ) ) {
476 return false;
477 }
478
479 $currency_positions = [];
480 foreach ( $original_costs as $original_cost ) {
481 $currency_symbol_position = strpos( trim( $original_cost ), $currency_symbol );
482 if ( false === $currency_symbol_position ) {
483 continue;
484 }
485
486 $currency_positions[] = 0 === $currency_symbol_position ? 'prefix' : 'postfix';
487 if ( end( $currency_positions ) !== reset( $currency_positions ) ) {
488 return false;
489 }
490 }
491
492 return ! empty( $currency_positions ) ? reset( $currency_positions ) : false;
493 }
494
495 /**
496 * Parses the cost value and current locale to infer decimal and thousands separators.
497 *
498 * The cost values stored in the meta table might not use the same decimal and thousands separator as the current
499 * locale.
500 * To work around this we parse the value assuming the decimal separator will be the last non-numeric symbol,
501 * if any.
502 *
503 * @since 4.9.12
504 *
505 * @param string|int|float $value The cost value to parse.
506 *
507 * @return array An array containing the parsed decimal and thousands separator symbols.
508 */
509 public function parse_separators( $value ) {
510 global $wp_locale;
511 $locale_decimal_point = $wp_locale->number_format['decimal_point'];
512 $locale_thousands_sep = $wp_locale->number_format['thousands_sep'];
513 $decimal_sep = $locale_decimal_point;
514 $thousands_sep = $locale_thousands_sep;
515
516 preg_match_all( '/[\\.,]+/', $value, $matches );
517
518 if ( ! empty( $matches[0] ) ) {
519 $matched_separators = $matches[0];
520 if ( count( array_unique( $matched_separators ) ) > 1 ) {
521 // We have both, the decimal separator will be the last non-numeric symbol.
522 $decimal_sep = end( $matched_separators );
523 $thousands_sep = reset( $matched_separators );
524 } else {
525 /*
526 * We only have one, we can assume it's the decimal separator if it comes before a number of numeric
527 * symbols that is not exactly 3. If there are exactly 3 number after the symbols we fall back on the
528 * locale; we did our best and cannot guess any further.
529 */
530 $frags = explode( end( $matched_separators ), $value );
531 if ( strlen( end( $frags ) ) !== 3 ) {
532 $decimal_sep = end( $matched_separators );
533 $thousands_sep = $decimal_sep === $locale_decimal_point ?
534 $locale_thousands_sep
535 : $locale_decimal_point;
536 }
537 }
538 }
539
540 return [ $decimal_sep, $thousands_sep ];
541 }
542 }
543