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;
}
}
?>