array('aggregate' => 'count', 'output' => 'MoneyFormat'), array( 'name' => array('aggregate' => 'count', 'output' => 'MoneyFormat') ) ); * Aggregate values: 'count', 'sum', 'min', 'max', 'avg' * @param $array * @param $cols * @param bool $subtotal * @return array */ static function GroupBy( &$array, $cols, $subtotal = false ) { //**NOTE: &$array must be passed by reference so we can unset() rows as they are processed and actually free memory. global $profiler; $profiler->startTimer( 'Group()' ); $group_by_cols = []; $aggregate_cols = []; if ( is_array( $cols ) ) { foreach ( $cols as $col => $aggregate ) { if ( is_string( $aggregate ) && $aggregate != '' ) { $aggregate_cols[$col] = $aggregate; } else { $group_by_cols[$col] = $col; //Use $col as the key so we can use isset() instead of in_array() later on. //$sort_by_cols[$col] = SORT_ASC; } } } //Debug::Arr( $group_by_cols, 'Group By Columns: ', __FILE__, __LINE__, __METHOD__, 10); //Debug::Arr( $aggregate_cols, 'Aggregate Columns: ', __FILE__, __LINE__, __METHOD__, 10); //Sort the array by group_by_col first, ONLY if subtotal mode, because order matters for merging the subtotal rows back into the main array. //The array should be sorted before subtotaling is done though, so we either need to pass the final sorting array here, or don't sort at all. //if ( $subtotal == 1 AND count($sort_by_cols) > 0 ) { // $array = Sort::arrayMultiSort( $array, $sort_by_cols ); //} $retarr = []; $row_map = []; if ( is_array( $array ) ) { $i = 0; $array_keys = array_keys( $array ); foreach( $array_keys as $row_key ) { //Loop over array_keys rather than $array, as foreach() causes the array to be duplicated, and this allows freeing memory with unset() on each row. $row = $array[$row_key]; if ( !is_array( $row ) ) { continue; } $group_by_key_val = null; if ( $subtotal == 2 ) { $group_by_key_val = 0; //Total the entire array. } else { foreach ( $group_by_cols as $group_by_element ) { if ( isset( $row[$group_by_element] ) ) { //Check if the value is an array with a 'sort' column, ie: array('sort' => 12345678, 'display' => '01-Jan-10' ) if ( is_array( $row[$group_by_element] ) ) { if ( isset( $row[$group_by_element]['sort'] ) ) { $group_by_key_val .= $row[$group_by_element]['sort']; } else if ( count( $row[$group_by_element] ) == 1 && isset( $row[$group_by_element][0] ) ) { //Could be a custom field single-select dropdown box, if so try to still make use of it. $group_by_key_val .= $row[$group_by_element][0]; } } else { $group_by_key_val .= $row[$group_by_element]; } } } } //Debug::Text('Group By Key Val: '. $group_by_key_val, __FILE__, __LINE__, __METHOD__, 10); if ( !isset( $retarr[$group_by_key_val] ) ) { $retarr[$group_by_key_val] = []; } //Map the last row that each group_by_key_val was seen. Assume that the array is properly sorted first of course. //Suffix '_' to the end so we can do an array_merge and ksort( $arr, SORT_STRING). //Although for creating overall totals we will likely need to do that before we merge in any sub-totals. $row_map[$group_by_key_val] = $i . '_'; foreach ( $row as $key => $val ) { //Debug::text(' aKey: '. $key .' Value: '. $val, __FILE__, __LINE__, __METHOD__, 10); if ( isset( $group_by_cols[$key] ) ) { //Only include a single key => value pair for the grouped columns to save memory. //If we are subtotaling, ignore all static columns. //Keep all columns even when sub-totalling so we can provide more information regarding the sub-total itself. if ( !isset( $retarr[$group_by_key_val][$key] ) ) { $retarr[$group_by_key_val][$key] = $val; } } else if ( isset( $aggregate_cols[$key] ) ) { $retarr[$group_by_key_val][$key] = self::Aggregate( $group_by_key_val, $key, $val, $aggregate_cols[$key], $subtotal ); } // else { //Ignore data that isn't in grouping or aggregate. } if ( $subtotal === false ) { //Only unset the data when *not* sub-totalling, as sub-totalling just returns the sub-total rows to be injected, and not the entire array. unset( $array[$row_key] ); //Reduces memory usage } $i++; } unset( $array_keys, $row, $group_by_key_val ); } if ( $subtotal === false ) { //Only unset the data when *not* sub-totalling, as sub-totalling just returns the sub-total rows to be injected, and not the entire array. unset( $array ); //Just be doubly sure memory gets cleared. } //Substitude group_by_key_val with sparse row values so we know where to insert totals within the main array if sub-totaling. //Debug::Arr($row_map, ' Row Map: ', __FILE__, __LINE__, __METHOD__, 10); if ( $subtotal == 1 ) { if ( is_array( $retarr ) && is_array( $row_map ) ) { foreach ( $row_map as $key => $count ) { $retarr[$count] = $retarr[$key]; $retarr[$count]['_subtotal'] = true; unset( $retarr[$key] ); } } } self::$_streaming_aggregate_data = null; //Clear memory before exiting and so the next run is correct as well. $profiler->stopTimer( 'Group()' ); if ( $subtotal == 1 ) { return $retarr; } else { return array_values( $retarr ); //Use array_values() to reindex array starting at 0. } } /** * Usage: $arr2 = Group::GroupBy($arr1, array( 'name' => array('aggregate' => 'count', 'output' => 'MoneyFormat'), array( 'name' => array('aggregate' => 'count', 'output' => 'MoneyFormat') ) ); * Aggregate values: 'count', 'sum', 'min', 'max', 'avg' * @param $array * @param $cols * @return array */ static function PageBreakBy( $array, $cols ) { global $profiler; $profiler->startTimer( 'PageBreak()' ); $group_by_cols = []; if ( is_array( $cols ) ) { foreach ( $cols as $key => $col ) { $group_by_cols[$col] = $col; //Use $col as the key so we can use isset() instead of in_array() later on. } } //Debug::Arr( $group_by_cols, 'Group By Columns: ', __FILE__, __LINE__, __METHOD__, 10); //Debug::Arr( $aggregate_cols, 'Aggregate Columns: ', __FILE__, __LINE__, __METHOD__, 10); $retarr = []; $row_map = []; if ( is_array( $array ) ) { $i = 0; foreach ( $array as $row ) { if ( !is_array( $row ) ) { continue; } $group_by_key_val = null; foreach ( $group_by_cols as $group_by_element ) { if ( isset( $row[$group_by_element] ) ) { //Check if the value is an array with a 'sort' column, ie: array('sort' => 12345678, 'display' => '01-Jan-10' ) if ( is_array( $row[$group_by_element] ) && isset( $row[$group_by_element]['sort'] ) ) { $group_by_key_val .= $row[$group_by_element]['sort']; } else { $group_by_key_val .= $row[$group_by_element]; } } } //Debug::Text('Group By Key Val: '. $group_by_key_val, __FILE__, __LINE__, __METHOD__, 10); if ( !isset( $retarr[$group_by_key_val] ) ) { $retarr[$group_by_key_val] = []; } //Map the last row that each group_by_key_val was seen. Assume that the array is properly sorted first of course. $row_map[$group_by_key_val] = $i; foreach ( $row as $key => $val ) { //Debug::text(' aKey: '. $key .' Value: '. $val, __FILE__, __LINE__, __METHOD__, 10); if ( isset( $group_by_cols[$key] ) ) { //Only include a single key => value pair for the grouped columns to save memory. //If we are subtotaling, ignore all static columns. //Keep all columns even when sub-totalling so we can provide more information regarding the sub-total itself. if ( !isset( $retarr[$group_by_key_val][$key] ) ) { $retarr[$group_by_key_val][$key] = $val; } } // else { //Ignore data that isn't in grouping or aggregate. } $i++; } } //Substitude group_by_key_val with sparse row values so we know where to insert totals within the main array if sub-totaling. //Debug::Arr($row_map, ' Row Map: ', __FILE__, __LINE__, __METHOD__, 10); if ( is_array( $retarr ) && is_array( $row_map ) ) { foreach ( $row_map as $key => $count ) { //$retarr[$count] = $retarr[$key]; //Uncomment this to show the columns that are causing it to insert a page break. $retarr[$count]['_page_break'] = true; unset( $retarr[$key] ); } } $profiler->stopTimer( 'PageBreak()' ); return $retarr; } static private $_streaming_aggregate_data = null; static function Aggregate( $group_by_key, $key, $value, $type, $subtotal = false ) { switch ( $type ) { default: case 'sum': if ( !isset( self::$_streaming_aggregate_data[$type][$group_by_key][$key] ) ) { self::$_streaming_aggregate_data[$type][$group_by_key][$key] = 0; } if ( is_numeric( $value ) ) { $retarr = self::$_streaming_aggregate_data[$type][$group_by_key][$key] = bcadd( self::$_streaming_aggregate_data[$type][$group_by_key][$key], $value ); } else { $retarr = self::$_streaming_aggregate_data[$type][$group_by_key][$key]; //If $value is not numeric, ignore it and just return the sum() up to now. Otherwise this resets the sum back to null/0 } break; case 'average': case 'avg': if ( !isset( self::$_streaming_aggregate_data[$type][$group_by_key][$key] ) ) { self::$_streaming_aggregate_data[$type][$group_by_key][$key] = [ 'sum' => 0, 'count' => 0 ]; } if ( is_numeric( $value ) ) { self::$_streaming_aggregate_data[$type][$group_by_key][$key]['count']++; self::$_streaming_aggregate_data[$type][$group_by_key][$key]['sum'] = bcadd( self::$_streaming_aggregate_data[$type][$group_by_key][$key]['sum'], $value ); } else { self::$_streaming_aggregate_data[$type][$group_by_key][$key]['count']++; //Still count the number of rows, even if its not a numeric value we can sum. } $retarr = bcdiv( self::$_streaming_aggregate_data[$type][$group_by_key][$key]['sum'], self::$_streaming_aggregate_data[$type][$group_by_key][$key]['count'] ); //If $value is not numeric, ignore it and just return the sum() up to now. Otherwise this resets the sum back to null/0 //$retarr = ( array_sum($array) / count($array) ); //$retarr = bcdiv( self::arraySum( $array ), count( $array ) ); //Use bcdiv() otherwise PHP could return scientific notation which then later breaks BCMath functions. break; case 'minimum': case 'min': if ( !isset( self::$_streaming_aggregate_data[$type][$group_by_key] ) || !array_key_exists( $key, self::$_streaming_aggregate_data[$type][$group_by_key] ) ) { //isset() won't work here if $value = null $retarr = self::$_streaming_aggregate_data[$type][$group_by_key][$key] = $value; } else { if ( $value < self::$_streaming_aggregate_data[$type][$group_by_key][$key] ) { self::$_streaming_aggregate_data[$type][$group_by_key][$key] = $value; } $retarr = self::$_streaming_aggregate_data[$type][$group_by_key][$key]; } //$retarr = min( $array ); break; case 'min_not_null': if ( !isset( self::$_streaming_aggregate_data[$type][$group_by_key][$key] ) ) { self::$_streaming_aggregate_data[$type][$group_by_key][$key] = $value; } if ( is_numeric( $value ) ) { if ( $value < self::$_streaming_aggregate_data[$type][$group_by_key][$key] ) { self::$_streaming_aggregate_data[$type][$group_by_key][$key] = $value; } } $retarr = self::$_streaming_aggregate_data[$type][$group_by_key][$key]; //$retarr = self::MinNotNull( $array ); break; case 'maximum': case 'max': case 'max_not_null': if ( !isset( self::$_streaming_aggregate_data[$type][$group_by_key][$key] ) ) { self::$_streaming_aggregate_data[$type][$group_by_key][$key] = $value; } if ( is_numeric( $value ) ) { if ( $value > self::$_streaming_aggregate_data[$type][$group_by_key][$key] ) { self::$_streaming_aggregate_data[$type][$group_by_key][$key] = $value; } } $retarr = self::$_streaming_aggregate_data[$type][$group_by_key][$key]; //$retarr = max( $array ); break; case 'first': if ( !isset( self::$_streaming_aggregate_data[$type][$group_by_key] ) || !array_key_exists( $key, self::$_streaming_aggregate_data[$type][$group_by_key] ) ) { //isset() won't work here if $value = null self::$_streaming_aggregate_data[$type][$group_by_key][$key] = $value; } $retarr = self::$_streaming_aggregate_data[$type][$group_by_key][$key]; //reset( $array ); //$retarr = current( $array ); break; case 'last': $retarr = $value; //end( $array ); //$retarr = current( $array ); break; case 'count': if ( !isset( self::$_streaming_aggregate_data[$type][$group_by_key][$key] ) ) { self::$_streaming_aggregate_data[$type][$group_by_key][$key] = 1; } else { self::$_streaming_aggregate_data[$type][$group_by_key][$key]++; } $retarr = self::$_streaming_aggregate_data[$type][$group_by_key][$key]; //$retarr = count( $array ); break; case 'concat': if ( $subtotal == false ) { if ( !isset( self::$_streaming_aggregate_data[$type][$group_by_key][$key] ) ) { self::$_streaming_aggregate_data[$type][$group_by_key][$key] = $value; } else { if ( $value != '' ) { self::$_streaming_aggregate_data[$type][$group_by_key][$key] .= ' -- '. $value; } } $retarr = self::$_streaming_aggregate_data[$type][$group_by_key][$key]; //Pass $array through array_filter() without any callback, as that will remove any FALSE/NULL values, elimating strings like: ' -- -- -- -- ' //$retarr = implode( ' -- ', array_filter( $array ) ); //Using \n or
doesn't work properly on HTML/PDF reports, so just use " -- " for now. } else { //Don't concat for subtotal/total rows, as there is no point for text/string columns. $retarr = ''; } //if ( $subtotal == false ) { // //Pass $array through array_filter() without any callback, as that will remove any FALSE/NULL values, elimating strings like: ' -- -- -- -- ' // $retarr = implode( ' -- ', array_filter( $array ) ); //Using \n or
doesn't work properly on HTML/PDF reports, so just use " -- " for now. //} else { // //Don't concat for subtotal/total rows, as there is no point for text/string columns. // $retarr = ''; //} break; } //Debug::Arr($array, 'Aggregate Raw Data: ', __FILE__, __LINE__, __METHOD__, 10); //Debug::Arr($retarr, 'Aggregate Result: Aggregate: '. $type, __FILE__, __LINE__, __METHOD__, 10); return $retarr; } } ?>