Trying to locate the first blank cell in a column. The idea is to pick a column that I know has to have a value (in this case, JOB_NUMBER) and scan through it until a blank cell is found. The code below, in my mind, should do that. However, it never stops. I imagine it is stuck in the while loop, but I don't understand why.
Code:
<?php
require('./Classes/PHPExcel/IOFactory.php');
ini_set('max_execution_time', 800);
ini_set('memory_limit', 2000000000);
$inputFileType = 'Excel2007';
$inputFileName = $_FILES['file']['tmp_name'];
class MyReadFilter implements PHPExcel_Reader_IReadFilter {
public function __construct($fromColumn, $toColumn) {
$this->columns = array();
$toColumn++;
while ($fromColumn !== $toColumn) {
$this->columns[] = $fromColumn++;
}
}
public function readCell($column, $row, $worksheetName = '') {
// Read columns from 'A' to 'AF'
if (in_array($column, $this->columns)) {
return true;
}
return false;
}
}
$filterSubset = new MyReadFilter('A', 'AF');
$objReader = PHPExcel_IOFactory::createReader($inputFileType);
$objReader->setReadFilter($filterSubset);
$objReader->setLoadSheetsOnly( array("NORTH") );
$objPHPExcelReader = $objReader->load($inputFileName);
$r = 3500;
while(isset($maxrow_north) != 1){
$cellvalue = $objPHPExcelReader->getSheetByName('NORTH')->getCellByColumnAndRow(2, $r);
if(isset($cellvalue) != 1){
$maxrow_north = $r;
} elseif($r > 4000) {
echo "It's over 4000!";
} else {
$r = $r++;
}
}
echo $maxrow_north;
?>
Some more background
I am having admins upload .xlsx .xls or .csv files into an html form. The code, above, is the handler. I have limited the number of columns seen because the original creator of the .xlsx file thought it would be a great idea to have the columns go all the way out to XCF.
The rows also go all the way out to somewhere around 10,000. So, I want to find the first blank row and stop there.
TIA!
Don't use
if(isset($cellvalue) != 1){
A cell value always exists even if it's an empty string or a null: and you're not testing the actual cell value, but the existence of a cell.... simply get() ting a cell will create a new empty cell object if one didn't already exist
You need to test the actual value stored in the cell
if($cellvalue->getValue() === NULL || $cellvalue->getValue() === '') {
$maxrow_north = $r;
And if you're trying to find the first blank cell in the column, then break once you've found it rather than carry on iterating till you reach your max
(Note, doesn't check for rich text in cells)
EDIT
Example, that also allows for merged cells
function testInMergeRangeNotParent($objWorksheet, $cell)
{
$inMergeRange = false;
foreach($objWorksheet->getMergeCells() as $mergeRange) {
if ($cell->isInRange($mergeRange)) {
$range = PHPExcel_Cell::splitRange($mergeRange);
list($startCell) = $range[0];
if ($cell->getCoordinate() !== $startCell) {
$inMergeRange = true;
}
break;
}
}
return $inMergeRange;
}
$column = 2; // Column to check
$max = 4000;
echo 'Get First blank row in column ', $column, PHP_EOL;
$r = 3500; // Starting row
while(true){
$cell = $objPHPExcelReader->getSheetByName('NORTH')->getCellByColumnAndRow($column, $r);
if ($cell->getValue() === NULL &&
!testInMergeRangeNotParent($objPHPExcelReader->getSheetByName('NORTH'), $cell)) {
break;
}elseif($r > $max) {
echo "It's over $max !";
break;
}
$r++;
}
echo 'First blank row in column ', $column, ' is ', $r, PHP_EOL;
Related
just developed a simple CLI php code that calculates the multiplication table of your choosen size but when I check the files to make sure they stick to PSR coding standards, it gives me four errors/violations. I don't know where in the files the errors are after several attempts and days of work on the files.
there are two files:
cliVersion.php and generateCLITable.php
The first file gives me 1 PSR error and the second one gives me 3 PSR errors.
this is how I generate the multiplication table of size 12 on command line :
php cliVersion.php 12
can anyone help me to find out the PSR errors in the files.
here's the files and the error report:
cliVersion.php
<?php
declare(strict_types=1);
require_once 'generateCLITable.php';
require_once '../model/validateInput.php';
?>
<?php
// Assign the user's input argument value to $input variable
$inputString = $argv[1];
$errorMessage = "Please enter a valid argument (a whole number greater than 1)";
// Check if the user's input argument is not null or empty
if ($inputString == null || $inputString == "") {
echo $errorMessage;
} else {
// Create an object of ValidateInput Class
$inputData = new ValidateInput();
/*
Validate the $input variable received from the user as an argument.
The code will be safe to be processed after this line.
*/
$validatedInput = $inputData->validateInputData($inputString);
$validatedInputInt = (int)$validatedInput;
/*
Check if the validated input is an Integer and if it is,
generates the table else returns the error message
*/
$isInputValidInt = $inputData->isInputInt($validatedInputInt);
if ($isInputValidInt && $validatedInputInt > 1) {
$multTable = new MultTable();
$multTable->generateTable($validatedInputInt);
} else {
echo $errorMessage;
}
}
echo PHP_EOL;
GenerateCLITable.php
<?php
declare(strict_types=1);
class MultTable
{
/**
* The public generateTable function generates the multiplication table
*
* #param int $inputValue
*
* #return void
*/
public function generateTable(int $inputValue)
{
// Create first row of table headers - green colour
for ($col=1; $col <= $inputValue; $col++) {
echo "\033[35m \t$col \033[0m";
}
// Create remaining rows
for ($row=1, $col=1; $row <= $inputValue; $row++) {
echo "\n";
// First cell is a table header - green colour
if ($col == 1) {
echo "\033[35m \n$row \033[0m";
}
while ($col <= $inputValue) {
echo "\t" . $row * $col++ ;
}
// Reset $col at the end of the row
$col = 1;
}
}
}
Error report:
cliVersion.php
----------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
generateCLITable.php
----------------------------------------------------------------------
FOUND 3 ERRORS AFFECTING 3 LINES
I strongly recommend to use https://github.com/FriendsOfPHP/PHP-CS-Fixer in such cases.
I guess you are trying to unit test the code. I would probably focus on trying to be as thoughtful with the code first . Take cliVersion, I think you can simplify it greatly by creating a single method to handle the input. Also I find a lot of issue with your comments and variables and overdesign.
Revised Version:
<?php
//declare(strict_types=1); // I'm using php 5 so I don't have this (but ok for you)
require_once 'generateCLITable.php';
require_once '../model/validateInput.php';
$must_be_gtr_than = 1; // set floor val for validation
$inputData = new ValidateInput($must_be_gtr_than);
$multTable = new MultTable();
if ($inputData->isValidIntegerData($argv[1])) {
$tableSize = (int) $argv[1];
$multTable->generateTable($tableSize);
} else {
echo "Error: Please enter a valid argument (a whole number greater than ".$must_be_gtr_than.")";
die();
}
In this example I have one method alone that validates that I have an integer > 0, if isValidIntegerData than proceed . Think about ease of code and names that make sense generateTable doesn't tell me much, is there a better name ? Things like that. Take "$validatedInputInt", that doesn't speak to what you are doing as well as what that int is, "tableSize" makes more sense to me. Also we many times save camel back for Classes etc.. I would look up PSR2.
For example we use
CONST_SOMETHING = xx; //constants
var_x or varx // lowercase for vars
methodX //camel back
ClassName // ucwords for classes
etc..
UPDATE:
This is how I would probably go about building something like this:
<?php
//declare(strict_types=1); // PHP5.x Example
class GenerateCLITable
{
const DEFAULT_BOARD_SIZE = 12;
public $game_board, $table_size;
public function __construct($input = self::DEFAULT_BOARD_SIZE)
{
$this->game_board = $this->generateTable($input);
}
public function generateTable($inputValue = self::DEFAULT_BOARD_SIZE)
{
$table = "";
// Create first row of table headers - green colour
for ($col=1; $col <= $inputValue; $col++) {
$table .= "\033[35m \t$col \033[0m";
}
// Create remaining rows
for ($row=1, $col=1; $row <= $inputValue; $row++) {
$table .= "\n";
// First cell is a table header - green colour
if ($col == 1) {
$table .= "\033[35m \n$row \033[0m";
}
while ($col <= $inputValue) {
$table .= "\t" . $row * $col++ ;
}
// Reset $col at the end of the row
$col = 1;
}
$this->game_board = $table;
$this->table_size = $inputValue;
return $table;
}
public function isValidInputValue($input = '', $size = 0)
{
return (!empty($input) && $input > $size) ? (is_int((int) $input)) : false;
}
}
//require_once 'generateCLITable.php';
$multTable = new GenerateCLITable();
$must_be_gtr_than = 1;
if (!$multTable->isValidInputValue($argv[1], $must_be_gtr_than)) {
echo "Error: Please enter a valid argument (a whole number greater than ".$must_be_gtr_than.")";
die();
}
$table = $multTable->generateTable((int) $argv[1]);
echo $table . "\n";
I have an export of Customer Records that needed to be split up over several chunks of 500 records. I grab each chunk through a REST request, save it to my server:
public function createImportFile($json)
{
$filePath = storage_path().'/import/'.$this->getImportFileName($this->import->chunkNumber);
$importFile = fopen($filePath, 'w');
$array = json_decode($json);
fwrite($importFile, $json);
fclose($importFile);
return $filePath;
}
Then after grabbing all of the chunks, I import all of the records. I'm wondering what the best way would be to find the Nth record among all the chunks?
Currently, I divide the record number that I'm looking for by the total number of chunks to find out which chunk the record will be in. Then, I get the total records for the previous chunks and subtract this number from the record number to get the record's position in the chunk.
while ($this->recordNumber <= $this->totalRecords) {
$item = $this->getRecord($this->recordNumber);
if (empty($item)) {
$this->recordNumber++;
continue;
}
$results = $this->translateItem($item);
$this->recordNumber++;
}
public function getRecord($recordNumber)
{
if ($this->import->isChunkedImport()) {
$chunkNumber = (integer) $this->returnChunkFromRecordNumber($recordNumber);
$countInPrevChunks = intval($this->returnRecordCountForPrevChunks($chunkNumber));
$chunkPosition = intval($this->getChunkPosition($recordNumber, $countInPrevChunks));
$jsonObj = $this->getJsonObjectForChunkNumer($chunkNumber);
return $jsonObj[$chunkPosition];
}
else {
$chunkPosition = $this->getChunkPosition($recordNumber, 0);
$filePath = storage_path().'/import/'.$this->getImportFileName();
return (array) json_decode(file_get_contents($filePath))[$chunkPosition];
}
}
private function &getJsonObjectForChunkNumer($chunkNumber)
{
if ($this->currentFileArray == null || ($chunkNumber != $this->lastChunkNumber)) {
$filePath = storage_path().'/import/'.$this->getImportFileName($chunkNumber);
$this->currentFileArray = json_decode(file_get_contents($filePath), true);
$this->lastChunkNumber = $chunkNumber;
}
return $this->currentFileArray;
}
public function getChunkCount()
{
$filePath = storage_path().'/import/'.$this->getImportFileName();
return count(json_decode(file_get_contents($filePath)));
}
public function returnChunkFromRecordNumber($recordNumber)
{
if ($recordNumber >= $this->getChunkCount()) {
if (is_int($recordNumber/$this->getChunkCount())) {
if (($recordNumber/$this->getChunkCount()) == 1) {
return intval(1);
}
return intval(($recordNumber/$this->getChunkCount())-1);
}
else {
return intval($recordNumber/$this->getChunkCount());
}
}
else {
return intval(0);
}
}
public function getChunkPosition($recordNumber, $countInPrevChunks)
{
$positionInChunk = $recordNumber - $countInPrevChunks;
if ($positionInChunk == 0) {
return $positionInChunk;
}
return $positionInChunk - 1;
}
public function returnRecordCountForPrevChunks($chunkNumber)
{
if ($chunkNumber == 0) {
return 0;
}
else {
return $this->getChunkCount() * $chunkNumber;
I try to account for the first key for both Chunks and Records in the Chunks being 0, but I'm still missing the last record of the import. It also seems like I might be making this more complicated than it needs to be. I was wondering if anyone had advice or a more simple way to grab the Nth record. I thought about possibly just numbering the records as I bring them in with the REST request, then I could find the Chunk containing the record number as an array key and then return that record:
public function createImportFile($json)
{
$filePath = storage_path().'/import/'.$this->getImportFileName($this->import->chunkNumber);
$importFile = fopen($filePath, 'w');
if ($this->import->chunkNumber == 0 && $this->recordNumber == 0) $this->recordNumber = 1;
$array = json_decode($json);
$ordered_array = [];
foreach ($array as $record) {
$ordered_array[$this->recordNumber] = $record;
$this->recordNumber++;
}
fwrite($importFile, json_encode($ordered_array));
fclose($importFile);
return $filePath;
}
But I wasn't sure if that was the best approach.
With a lot of records, you could use a database table. MySQL would easily handle tens of thousands of records. You wouldn't even need to store the whole records. Perhaps just:
record_no | chunk_no | position_in_chunk
record_no: Primary key. Unique identifier for this record
chunk_no: Which chunk contains the record
position_in_chunk: Where within the chunk is the record located
Put a UNIQUE(chunk_no, position_in_chunk) index on the table.
Then as you pull records, assign them a number, build up the DB table, and save the table as you write records to disk. In the future, to get a specific record, all you'll need is its number.
If you don't want to use a database, you can also store this data as a JSON file, though retrieval performance will suffer from having to open and parse a big JSON file each time.
I have a script that imports records to multiple database tables, all relation to one parent entity (address).
If the address already exist, extra columns should just be updated according to the csv. The same counts for all other entities. A counter ($exist++) should increase or an array ($existingRecords) should be filled with already existing records.
If mandatory fields are empty in the record, that record should be added to another array ($failedRecords) or another counter ($failed++) should increase.
If the address doesn't yet exist and should be created with all fields just a counter ($successful++) should increase.
In the end I have an array $result that give the number of failed, successful, and already existing (but updated) records, for user feedback.
How can I implement this in a nice clean manner without messing my current script too much up? Because what is happening right now, if a record already exist the $exist counter increase but the $successful counter as well, and I only want the $exist counter to increase if a record already already exists, and only the $successful counter should increase if a record still has to be added and was added successfully. Same goes for the $failed counter.
Here is my script (with what I tried):
public function import(CsvFile $csv) {
$root = __DIR__.'/../../../../../';
$file = $root.'var/data/'.$csv->getCsvName();
$fp = fopen($file, "r");
$batchSize = 25;
$header = null;
$successful = 0;
$failed = 0;
$exist = 0;
$results = [];
while ($row = fgetcsv($fp, null, ";")) {
if ($header === null) {
$header = $row;
continue;
}
$record = array_combine($header, $row);
// cast all values to correct data types
foreach ($record as $key => &$value) {
if (strpos($key, 'datum') !== false ||
strpos($key, 'tijdstip') !== false &&
strlen($value) == 8 &&
is_numeric($value)
) {
$value = \DateTime::createFromFormat('Ymd', $value);
}
if ($value === "") {
$value = null;
}
if (is_numeric($value)) {
intval($value) == $value ? $value = (int)$value : $value = (float)$value;
}
}
// required fields
if (!$record['name'] ||
!$record['surname'] ||
!$record['email'] ||
!$record['phone'] ||
!$record['street'] ||
!$record['houseNo'] ||
!$record['town'] ||
!$record['postcode'] ||
!$record['location'] ||
!$record['lecture'] ||
!$record['session'] ||
) {
$failed++;
continue;
}
$student = $this->em->getRepository(Student::class)->findStudent(
$record['name'], $record['surname'],
$record['email'], $record['phone']
);
if (!$student) {
$student = new Student();
$student->setName($record['name']);
$student->setSurname($record['surname']);
$student->setEmail($record['email']);
$student->setPhone($record['phone']);
} else {
$exist++;
}
$student->setAge($record['age']);
$student->setLength($record['length']);
$address = $this->em->getRepository(Address::class)->findOneBy([
'street' => $record['street'],
'houseNo' => $record['houseNo'],
'town' => $record['town'],
'postcode' => $record['postcode'],
);
if (!$address) {
$address = new Address();
$address->setStreet($record['street']);
$address->setHouseNo($record['houseNo']);
$address->setPostcode($record['postcode']);
$address->setTown($record['town']);
}
$student->setAddress($address);
$lecture = $this->em->getRepository(Lecture::class)->findOneBy([
'location' => $record['location'],
'lecture' => $record['lecture'],
'session' => $record['session'],
]);
if (!$lecture) {
$lecture = new Lecture();
$lecture->setLocation($record['location']);
$lecture->setLecture($record['lecture']);
$lecture->setSession($record['session']);
}
$lecture->setTime($record['time']);
$lecture->setSubject($record['subject']);
$student->setLecture($lecture);
$validationErrors = $this->validator->validate($student);
if (!count($validationErrors)) {
$this->em->persist($student);
$successful++;
}
if (($successful % $batchSize) == 0) {
$this->em->flush();
}
}
fclose($fp);
$csv->setImported(true);
$this->em->persist($csv);
$this->em->flush(); // Also persist objects that did not make up an entire batch
$results['successful'] = $successful;
$results['failed'] = $failed;
$results['exist'] = $exist;
return $results;
}
You can replace the existing records counter with a flag that is set to TRUE only if the current record is an existing one, and use it at the end before persisting the record.
You can set the flag to FALSE on the beginning of each loop:
while ($row = fgetcsv($fp, null, ";")) {
$existingRecordFlag = FALSE; // initialize the flag for the current record
then update it instead of the counter :
if (!$student) {
$student = new Student();
$student->setName($record['name']);
$student->setSurname($record['surname']);
$student->setEmail($record['email']);
$student->setPhone($record['phone']);
} else {
$existingRecordFlag = TRUE; //update the flag
}
then before persisting user check the value for the flag and according to it update the counter:
$validationErrors = $this->validator->validate($student);
if (!count($validationErrors)) {
$this->em->persist($student);
if ( !$existingRecordFlag){
$successful++; //new record
}else{
$exist++; //existing user
}
}else{
$failed++; //failed as the validations has errors
continue;
}
also you can add the existing records to the batch counter as they might be updated, they can also be persisted with the successful ones
if ((($successful+$exist) % $batchSize) == 0) {
$this->em->flush();
}
In your block handling the addresses you could an existing/failed counter quite easily:
$exist++;
if (!$address) {
$exist--;
$successful++;
//...
}
This will always increase the exist-counter, but jump back if address is empty and increase the new address-counter instead. Alternatively you could work with array_push($record) and array_pop($record) to add/remove the current record from a list. This might cause memory issues, since you keep your records in multiple arrays and you might run into out of memory errors.
For missing mandatory fields you'd have to do a check on the $address afterwards to see if it's not empty/containing invalid data and then increase the counter/update the field.
If you want
Let me change my question completely to explain myself better;
I have an order;
An order have multiple order rows. Each order row has two fields; Quantity ordered, and quantity delivered.
If all order rows' quantities delivered are the same as the quantity ordered, the entire order should get a status of '100% delivered'.
If multiple or even one order row's quantities delivered does not match the quantities ordered the entire order should get a status of 'partly delivered'.
If no order row have any deliveries (if all deliveries stands on 0) the status should be '0% delivered'.
What I have so far looks only at the last order row of the entire order because all the previous rows gets overridden by the latest check. This is my code;
public function deliveryAction(Request $request, $id) {
$em = $this->getDoctrine()->getManager();
$order = $em->getRepository('QiBssBaseBundle:PmodOrder')->find($id);
$orderRowsDelivered = $request->request->all();
$delivered = "0%";
foreach ($orderRowsDelivered['order_row_id'] as $orderRowId => $quantityDelivered) {
if($quantityDelivered != '' || $quantityDelivered != null) {
$orderRow = $em->getRepository("QiBssBaseBundle:PmodOrderRow")->find($orderRowId);
$orderDelivered = new PmodDelivery();
$orderDelivered->setOrderRow($orderRow);
$orderDelivered->setQuantity($quantityDelivered);
$orderDelivered->setTimeArrived(new \DateTime());
$em->persist($orderDelivered);
$em->flush();
if($orderRow->getQuantityDelivered() > 0 && $orderRow->getQuantityDelivered() < $orderRow->getQuantity()) {
$delivered = "partly";
} elseif ($orderRow->getQuantityDelivered() == $orderRow->getQuantity()) {
$delivered = "100%";
}
}
}
var_dump($delivered);exit;
return new RedirectResponse ... ;
}
Because as of this moment he looks at the last one with 10 and 8 in the example image, and give a status of 'partly', as soon as the 'quantity delivered' amounts is entered. But he should take all rows together.
I hope this makes more sense.
Based on what Cerad in the comments of his own answer said, this is my answer; (I'll make use of OP's scenario where he uses order rows per order.)
I've added an extra property to my OrderRow entity called $rowStatus.
After that I created a getter function for the $rowStatus called getRowStatus() that gives each row a status individually;
public function getRowStatus()
{
if ($this->getQuantityDelivered() == $this->getQuantity()) {
return $this->rowStatus = 100;
} elseif ($this->getQuantityDelivered() == 0) {
return $this->rowStatus = 0;
} else {
return $this->rowStatus = 50;
}
}
After that in my Order entity I've added a $deliveryStatus property, with a corresponding getter function called getDeliveryStatus() that looks like this;
public function getDeliveryStatus()
{
if (count($this->getOrderRows()) > 0) { //this check is to make sure there are orderRows, because you can't devide by zero if it might happen that there are no order rows. If not the delivery status will just be set on 0.
$sum = 0;
foreach ($this->getOrderRows() as $row) {
$sum += $row->getRowStatus();
}
$average = $sum / count($this->getOrderRows());
if ($average == 100) {
return $this->deliveryStatus = 100;
} elseif ($average == 0) {
return $this->deliveryStatus = 0;
} else {
return $this->deliveryStatus = 50;
}
} else {
return $this->deliveryStatus = 0;
}
}
That's it! After this I just use an enum function to display the 100 as "100% delivered", the 50 as "partly delivered", and the 0 as "0% delivered". I know this isn't really necessary, and you can instead change the status number directly to a string or whatever you want to display.
Just off the top of my head I might do:
$deliveredNone = true;
$deliveredAll = true;
$deliveredSome = false;
foreach ($orderRowsDelivered['order_row_id'] as $orderRowId => $quantityDelivered) {
if ($quantityDelivered) {
$deliveredNone = false; // Know that something has been delivered
}
...
if ($orderRow->getQuantityDelivered() != $orderRow->getQuantity()) {
$deliveredSome = true;
$deliveredAll = false;
}
}
$delivered = null;
if ($deliveredNone) $delivered = '0%';
if ($deliveredAll) $delivered = '100%';
if ($deliveredSome) $delivered = 'partly';
Though I would probably just update the order with the quantities delivered then use a different function to calculate the percentage delivered. As you can see, mixing the two processes can result in confusion.
I want to insert this array formula:
{=SUM(IF(FREQUENCY(IF(T9:T977=1,MATCH(U9:U977,U9:U977,0)),ROW(U9:U977)-ROW(U9)+1),1))}
but when I'm using:
$sheet->getCell("C1")->setValue("{=SUM(IF(FREQUENCY(IF(T9:T977=1,MATCH(U9:U977,U9:U977,0)),ROW(U9:U977)-ROW(U9)+1),1))}");
It doesn't work, I've checked the documentation, but still haven't found anything.
I couldn't find any answer so I went through PHPExcel and made myself a solution:
in /PHPExcel/Cell.php at row 251 in the switch($pDataType) add this:
case PHPExcel_Cell_DataType::TYPE_FORMULA_ARRAY:
$this->_value = (string)$pValue;
break;
in /PHPExcel/Cell/DataType.php add this constant:
const TYPE_FORMULA_ARRAY = 't';
At last in /PHPExcel/Writer/Excel2007/Worksheet.phpI've added this in the switch beginning at row 1095:
case 't': // Array Formulae
$objWriter->startElement('f');
$objWriter->writeAttribute('t', 'array');
$objWriter->writeAttribute('ref', $pCellAddress);
$objWriter->writeAttribute('aca', '1');
$objWriter->writeAttribute('ca', '1');
$objWriter->text($cellValue);
$objWriter->endElement();
if ($this->getParentWriter()->getOffice2003Compatibility() === false) {
if ($this->getParentWriter()->getPreCalculateFormulas()) {
$calculatedValue = $pCell->getCalculatedValue();
if (!is_array($calculatedValue) && substr($calculatedValue, 0, 1) != '#') {
$objWriter->writeElement('v', PHPExcel_Shared_String::FormatNumber($calculatedValue));
} else {
$objWriter->writeElement('v', '0');
}
} else {
$objWriter->writeElement('v', '0');
}
}
break;
Then I used the function like this:
$sheet->getCell("C1")->setValueExplicit("=SUM(IF(FREQUENCY(IF(T9:T977=1,MATCH(U9:U977,U9:U977,0)),ROW(U9:U977)-ROW(U9)+1),1))", PHPExcel_Cell_DataType::TYPE_FORMULA_ARRAY);
And it works all good when I'm creating a excel file!
Since this question/answer still comes up when searching for PHPExcel's successor, PHPSpreadsheet, I'd like to add that the latter has support for array formulas build in. Just assign the formula as usual and set the type-attribute to array:
$attrs = $sheet->getCell("C1")->getFormulaAttributes();
$attrs['t'] = 'array';
$sheet->getCell("C1")->setFormulaAttributes($attrs);
Here is my fix for inserting array formula using PhpSpreadsheet.
I have made few changes in function 'writeCellFormula'
(PhpSpreadsheet\Writer\Xlsx\Worksheet.php)
Usage:
$sheet = $spreadsheet->getActiveSheet();
$attrs = $sheet->getCell("C1")->getFormulaAttributes();
$attrs['t'] = 'array';
$sheet->getCell("C1")->setFormulaAttributes($attrs);
$sheet->setCellValue("C1",'=SUM(A1:A3*B1:B3)');
Expected output: {=SUM(Q1:Q3*R1:R3) }
private function writeCellFormula(XMLWriter $objWriter, string $cellValue, Cell $pCell): void
{
$attributes = $pCell->getFormulaAttributes();
if ($attributes['t']=='array')
{
$pCellAddress = $pCell->getCoordinate();
$objWriter->startElement('f');
$objWriter->writeAttribute('t', 'array');
$objWriter->writeAttribute('ref', $pCellAddress);
//$objWriter->writeAttribute('aca', '1');
//$objWriter->writeAttribute('ca', '1');
$objWriter->text(substr($cellValue, 1));
$objWriter->endElement();
return;
}
$calculatedValue = $this->getParentWriter()->getPreCalculateFormulas() ? $pCell->getCalculatedValue() : $cellValue;
if (is_string($calculatedValue)) {
if (\PhpOffice\PhpSpreadsheet\Calculation\Functions::isError($calculatedValue)) {
$this->writeCellError($objWriter, 'e', $cellValue, $calculatedValue);
return;
}
$objWriter->writeAttribute('t', 'str');
}
elseif (is_bool($calculatedValue) ) {
$objWriter->writeAttribute('t', 'b');
$calculatedValue = (int) $calculatedValue;
}
$objWriter->writeElement('f', Xlfn::addXlfnStripEquals($cellValue));
self::writeElementIf(
$objWriter,
$this->getParentWriter()->getOffice2003Compatibility() === false,
'v',
($this->getParentWriter()->getPreCalculateFormulas() && !is_array($calculatedValue) && substr($calculatedValue, 0, 1) !== '#')
? StringHelper::formatNumber($calculatedValue) : '0'
);
}