File Editor
Directories:
.. (Back)
Admin
Ajax
Asset
Context
Customizer
Debug_Bar
Dialog
Documentation
Duplicate
Editor
Image
JSON_LD
Languages
Log
Meta
Models
Onboarding
PUE
Process
Promoter
REST
Repository
Service_Providers
Shortcode
Support
Tabbed_View
Tooltip
Traits
Utils
Validator
Values
Widget
Files:
Abstract_Deactivation.php
Abstract_Plugin_Register.php
App_Shop.php
Assets.php
Assets_Pipeline.php
Autoloader.php
Cache.php
Cache_Listener.php
Changelog_Reader.php
Container.php
Context.php
Cost_Utils.php
Credits.php
Customizer.php
DB_Lock.php
Data.php
Date_Utils.php
Db.php
Debug.php
Dependency.php
Deprecation.php
Editor.php
Error.php
Exception.php
Extension.php
Extension_Loader.php
Feature_Detection.php
Field.php
Field_Conditional.php
Log.php
Main.php
Notices.php
Plugin_Meta_Links.php
Plugins.php
Plugins_API.php
Post_History.php
Post_Transient.php
Promise.php
Repository.php
Rewrite.php
Settings.php
Settings_Manager.php
Settings_Tab.php
Simple_Table.php
Support.php
Tabbed_View.php
Template.php
Template_Part_Cache.php
Templates.php
Terms.php
Timezones.php
Tracker.php
Updater.php
Validate.php
View_Helpers.php
Create New File
Create
Edit File: Cost_Utils.php
<?php /** * Class Tribe__Cost_Utils * * Utility methods to deal with costs. * * @since 4.3 */ class Tribe__Cost_Utils { /** * @var string */ protected $_current_original_cost_separator; /** * @var string */ protected $_supported_decimal_separators = '.,'; /** * Static Singleton Factory Method * * @return Tribe__Events__Cost_Utils */ public static function instance() { static $instance; if ( ! $instance ) { $instance = new self; } return $instance; } /** * Check if a string is a valid cost. * * @param string $cost String to be checked. * Can include decimal and thousands separator. * * @return boolean */ public function is_valid_cost( $cost, $allow_negative = true ) { return preg_match( $this->get_cost_regex(), trim( $cost ) ); } /** * Returns the regular expression that shold be used to identify a valid * cost string. * * @return string */ public function get_cost_regex() { $separators = '[\\' . implode( '\\', $this->get_separators() ) . ']?'; $cost_regex = '(' . $separators . '([\d]+)' . $separators . '([\d]*))'; /** * Filters the regular expression that will be used to identify a valid cost * string. * * @param string $cost_regex * * @deprecated 4.3 Use `tribe_cost_regex` instead */ $cost_regex = apply_filters( 'tribe_events_cost_regex', $cost_regex ); /** * Filters the regular expression that will be used to identify a valid cost * string. * * @param string $cost_regex */ $cost_regex = apply_filters( 'tribe_cost_regex', $cost_regex ); return $cost_regex; } /** * Fetch the possible separators * * @return array */ public function get_separators() { $separators = [ ',', '.' ]; /** * Filters the cost string possible separators, those must be only 1 char. * * @param array $separators Defaults to comma (",") and period (".") */ return apply_filters( 'tribe_events_cost_separators', $separators ); } /** * If the cost is "0", call it "Free" * * @param int|float|string $cost Cost to analyze * * @return int|float|string */ public function maybe_replace_cost_with_free( $cost ) { $cost_with_period = $this->convert_decimal_separator( $cost ); if ( is_numeric( $cost_with_period ) && '0.00' === number_format( (float) $cost_with_period, 2, '.', ',' ) ) { return esc_html__( 'Free', 'tribe-common' ); } return $cost; } /** * Formats a cost with a currency symbol * * @param int|float|string $cost Cost to format * * return string * @param int|WP_Post $event An event post ID or post object. * @param string $currency_symbol * @param string $currency_position Either "prefix" or "posfix" * * @return float|int|string */ public function maybe_format_with_currency( $cost, $event = null, $currency_symbol = null, $currency_position = null ) { // check if the currency symbol is desired, and it's just a number in the field // be sure to account for european formats in decimals, and thousands separators if ( is_numeric( str_replace( $this->get_separators(), '', $cost ) ) ) { $reverse_position = null; // currency_position often gets passed as null or an empty string. if ( ! empty( $currency_position ) ) { $reverse_position = 'prefix' === $currency_position ? false : true; } $cost = tribe_format_currency( $cost, $event, $currency_symbol, $reverse_position ); } return $cost; } /** * @param string $original_string_cost A string cost with or without currency symbol, * e.g. `10 - 20`, `Free` or `2$ - 4$`. * @param array|string $merging_cost A single string cost representation to merge or an array of * string cost representations to merge, e.g. ['Free', 10, 20, * 'Donation'] or `Donation`. * @param bool $with_currency_symbol Whether the output should prepend the currency symbol to the * numeric costs or not. * @param array $sorted_mins An array of non numeric price minimums sorted smaller to larger, * e.g. `['Really free', 'Somewhat free', 'Free with 3 friends']`. * @param array $sorted_maxs An array of non numeric price maximums sorted smaller to larger, * e.g. `['Donation min $10', 'Donation min $20', 'Donation min * $100']`. * * @return string|array The merged cost range. */ public function merge_cost_ranges( $original_string_cost, $merging_cost, $with_currency_symbol, $sorted_mins = [], $sorted_maxs = [] ) { if ( empty( $merging_cost ) || $original_string_cost === $merging_cost ) { return $original_string_cost; } $_merging_cost = array_map( [ $this, 'convert_decimal_separator' ], (array) $merging_cost ); $_merging_cost = array_map( [ $this, 'numerize_numbers' ], $_merging_cost ); $numeric_merging_cost_costs = array_filter( $_merging_cost, 'is_numeric' ); $matches = []; preg_match_all( '!\d+(?:([' . preg_quote( $this->_supported_decimal_separators ) . '])\d+)?!', $original_string_cost, $matches ); $this->_current_original_cost_separator = empty( $matches[1][0] ) ? '.' : $matches[1][0]; $matches[0] = empty( $matches[0] ) ? $matches[0] : array_map( [ $this, 'convert_decimal_separator', ], $matches[0] ); $numeric_orignal_costs = empty( $matches[0] ) ? $matches[0] : array_map( 'floatval', $matches[0] ); $all_numeric_costs = array_filter( array_merge( $numeric_merging_cost_costs, $numeric_orignal_costs ) ); $cost_min = $cost_max = false; $merging_mins = array_intersect( $sorted_mins, (array) $merging_cost ); $merging_has_min = array_search( reset( $merging_mins ), $sorted_mins ); $original_has_min = array_search( $original_string_cost, $sorted_mins ); $merging_has_min = false === $merging_has_min ? 999 : $merging_has_min; $original_has_min = false === $original_has_min ? 999 : $original_has_min; $string_min_key = min( $merging_has_min, $original_has_min ); if ( array_key_exists( $string_min_key, $sorted_mins ) ) { $cost_min = $sorted_mins[ $string_min_key ]; } else { $cost_min = empty( $all_numeric_costs ) ? '' : min( $all_numeric_costs ); } $merging_maxs = array_intersect( $sorted_maxs, (array) $merging_cost ); $merging_has_max = array_search( end( $merging_maxs ), $sorted_maxs ); $original_has_max = array_search( $original_string_cost, $sorted_maxs ); $merging_has_max = false === $merging_has_max ? - 1 : $merging_has_max; $original_has_max = false === $original_has_max ? - 1 : $original_has_max; $string_max_key = max( $merging_has_max, $original_has_max ); if ( array_key_exists( $string_max_key, $sorted_maxs ) ) { $cost_max = $sorted_maxs[ $string_max_key ]; } else { $cost_max = empty( $all_numeric_costs ) ? '' : max( $all_numeric_costs ); } $cost = array_filter( [ $cost_min, $cost_max ] ); if ( $with_currency_symbol ) { $formatted_cost = []; foreach ( $cost as $c ) { $formatted_cost[] = is_numeric( $c ) ? tribe_format_currency( $c ) : $c; } $cost = $formatted_cost; } return empty( $cost ) ? $original_string_cost : array_map( [ $this, 'restore_original_decimal_separator' ], $cost ); } /** * Returns a maximum cost in a list of costs. If an array of costs is not passed in, the array of costs is fetched * via query. * * @param $costs mixed Cost(s) to review for max value * * @return float */ public function get_maximum_cost( $costs = null ) { return $this->get_cost_by_func( $costs, 'max' ); } /** * Returns a particular cost within an array of costs * * @param $costs mixed Cost(s) to review for max value * @param $function string Function to use to determine which cost to return from range. Valid values: max, min * * @return float */ protected function get_cost_by_func( $costs = null, $function = 'max' ) { if ( null === $costs ) { $costs = $this->get_all_costs(); } else { $costs = (array) $costs; } $costs = $this->parse_cost_range( $costs ); // if there's only one item, we're looking at a single event. If the cost is non-numeric, let's // return the non-numeric cost so that value is preserved if ( 1 === count( $costs ) && ! is_numeric( current( $costs ) ) ) { return current( $costs ); } // make sure we are only trying to get numeric min/max values $costs = array_filter( $costs, 'is_numeric' ); if ( empty( $costs ) ) { return 0; } switch ( $function ) { case 'min': $cost = $costs[ min( array_keys( $costs ) ) ]; break; case 'max': default: $cost = $costs[ max( array_keys( $costs ) ) ]; break; } // If there isn't anything on the cost just return 0 if ( empty( $cost ) ) { return 0; } return $cost; } /** * Parses a cost into an array of ranges. * * If a range isn't provided, the resulting array will hold a single * value. * * @param string|array $costs A cost string or an array of cost strings. * @param null $max_decimals The maximum number of decimal values that should be returned in the range. * @param bool $sort Whether the returned values should be sorted. * * @return array An associative array of parsed costs in [ <string cost> => <cost number> ] format. */ public function parse_cost_range( $costs, $max_decimals = null, $sort = true ) { if ( ! is_array( $costs ) && ! is_string( $costs ) ) { return []; } // make sure costs is an array $costs = (array) $costs; // If there aren't any costs, return a blank array if ( 0 === count( $costs ) ) { return []; } // Build the regular expression $price_regex = $this->get_cost_regex(); $max = 0; foreach ( $costs as &$cost ) { // Get the required parts if ( preg_match_all( '/' . $price_regex . '/', $cost, $matches ) ) { $cost = reset( $matches ); } else { $cost = [ $cost ]; continue; } // Get the max number of decimals for the range if ( count( $matches ) === 4 ) { $decimals = max( array_map( 'strlen', end( $matches ) ) ); $max = max( $max, $decimals ); } } // If we passed max decimals if ( ! is_null( $max_decimals ) ) { $max = max( $max_decimals, $max ); } $output_costs = []; $costs = call_user_func_array( 'array_merge', array_values( $costs ) ); foreach ( $costs as $cost ) { $numeric_cost = str_replace( $this->get_separators(), '.', $cost ); if ( is_numeric( $numeric_cost ) ) { // Creates a Well Balanced Index that will perform good on a Key Sorting method $index = str_replace( [ '.', ',' ], '', number_format( (float) $numeric_cost, $max ) ); } else { // Makes sure that we have "index-safe" string $index = sanitize_title( $numeric_cost ); } // Keep the Costs in a organizable array by keys with the "numeric" value $output_costs[ $index ] = $cost; } // Filter keeping the Keys if ( $sort ) { ksort( $output_costs ); } return (array) $output_costs; } /** * Returns a minimum cost in a list of costs. If an array of costs is not passed in, the array of costs is fetched * via query. * * @param $costs mixed Cost(s) to review for min value * * @return float */ public function get_minimum_cost( $costs = null ) { return $this->get_cost_by_func( $costs, 'min' ); } /** * Converts the original decimal separator to ".". * * @param string|int $value * * @return string */ protected function convert_decimal_separator( $value ) { return preg_replace( '/[' . preg_quote( $this->_supported_decimal_separators ) . ']/', '.', $value ); } /** * Restores the decimal separator to its original symbol. * * @param string $value * * @return string */ protected function restore_original_decimal_separator( $value ) { return str_replace( '.', $this->_current_original_cost_separator, $value ); } /** * Extracts int and floats from a numeric "dirty" string like strings that might contain other symbols. * * E.g. "$10" will yield "10"; "23.55$" will yield "23.55". * * @param string|int $value * * @return int|float */ protected function numerize_numbers( $value ) { $matches = []; $pattern = '/(\\d{1,}([' . $this->_supported_decimal_separators . ']\\d{1,}))/'; return preg_match( $pattern, $value, $matches ) ? $matches[1] : $value; } /** * Parses the currency symbol part of a cost string. * * @param string|array $cost A string cost, a comma separated array of string costs or an array of costs. * * @return false|string Either the inferred currency symbol or `false` if the currency symbol is missing or not consistent. */ public function parse_currency_symbol( $cost ) { if ( empty( $cost ) ) { return false; } $original_costs = is_array( $cost ) ? $cost : preg_split( '/\\s*,\\s*/', $cost ); $costs = $this->parse_cost_range( $original_costs, null, false ); if ( empty( $costs ) ) { return false; } $currency_symbols = []; $i = 0; foreach ( $costs as $string => $value ) { if ( is_numeric( $string ) ) { $currency_symbols[] = trim( str_replace( $value, '', $original_costs[ $i ] ) ); if ( end( $currency_symbols ) !== reset( $currency_symbols ) ) { return false; } } $i ++; } return ! empty( $currency_symbols ) ? reset( $currency_symbols ) : false; } /** * Parses the currency symbol position part of a cost string. * * @param string|array $cost A string cost, a comma separated array of string costs or an array of costs. * * @return false|string Either the inferred currency symbol position or `false` if not present or not consistent. */ public function parse_currency_position( $cost ) { if ( empty( $cost ) ) { return false; } $original_costs = is_array( $cost ) ? $cost : preg_split( '/\\s*,\\s*/', $cost ); $currency_symbol = $this->parse_currency_symbol( $original_costs ); if ( empty( $currency_symbol ) ) { return false; } $currency_positions = []; foreach ( $original_costs as $original_cost ) { $currency_symbol_position = strpos( trim( $original_cost ), $currency_symbol ); if ( false === $currency_symbol_position ) { continue; } $currency_positions[] = 0 === $currency_symbol_position ? 'prefix' : 'postfix'; if ( end( $currency_positions ) !== reset( $currency_positions ) ) { return false; } } return ! empty( $currency_positions ) ? reset( $currency_positions ) : false; } /** * Parses the cost value and current locale to infer decimal and thousands separators. * * The cost values stored in the meta table might not use the same decimal and thousands separator as the current * locale. * To work around this we parse the value assuming the decimal separator will be the last non-numeric symbol, * if any. * * @since 4.9.12 * * @param string|int|float $value The cost value to parse. * * @return array An array containing the parsed decimal and thousands separator symbols. */ public function parse_separators( $value ) { global $wp_locale; $locale_decimal_point = $wp_locale->number_format['decimal_point']; $locale_thousands_sep = $wp_locale->number_format['thousands_sep']; $decimal_sep = $locale_decimal_point; $thousands_sep = $locale_thousands_sep; preg_match_all( '/[\\.,]+/', $value, $matches ); if ( ! empty( $matches[0] ) ) { $matched_separators = $matches[0]; if ( count( array_unique( $matched_separators ) ) > 1 ) { // We have both, the decimal separator will be the last non-numeric symbol. $decimal_sep = end( $matched_separators ); $thousands_sep = reset( $matched_separators ); } else { /* * We only have one, we can assume it's the decimal separator if it comes before a number of numeric * symbols that is not exactly 3. If there are exactly 3 number after the symbols we fall back on the * locale; we did our best and cannot guess any further. */ $frags = explode( end( $matched_separators ), $value ); if ( strlen( end( $frags ) ) !== 3 ) { $decimal_sep = end( $matched_separators ); $thousands_sep = $decimal_sep === $locale_decimal_point ? $locale_thousands_sep : $locale_decimal_point; } } } return [ $decimal_sep, $thousands_sep ]; } }
Save Changes
Rename File
Rename