Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a

### Fixed

- Nothing yet.
- CODE/UNICODE and CHAR/UNICHAR. [PR #4727](https://github.com/PHPOffice/PhpSpreadsheet/pull/4727)

## 2025-11-24 - 5.3.0

Expand Down
4 changes: 2 additions & 2 deletions docs/references/function-list-by-category.md
Original file line number Diff line number Diff line change
Expand Up @@ -569,8 +569,8 @@ THAINUMSOUND | **Not yet Implemented**
THAINUMSTRING | **Not yet Implemented**
THAISTRINGLENGTH | **Not yet Implemented**
TRIM | \PhpOffice\PhpSpreadsheet\Calculation\TextData\Trim::spaces
UNICHAR | \PhpOffice\PhpSpreadsheet\Calculation\TextData\CharacterConvert::character
UNICODE | \PhpOffice\PhpSpreadsheet\Calculation\TextData\CharacterConvert::code
UNICHAR | \PhpOffice\PhpSpreadsheet\Calculation\TextData\CharacterConvert::characterUnicode
UNICODE | \PhpOffice\PhpSpreadsheet\Calculation\TextData\CharacterConvert::codeUnicode
UPPER | \PhpOffice\PhpSpreadsheet\Calculation\TextData\CaseConvert::upper
VALUE | \PhpOffice\PhpSpreadsheet\Calculation\TextData\Format::VALUE
VALUETOTEXT | \PhpOffice\PhpSpreadsheet\Calculation\TextData\Format::valueToText
Expand Down
4 changes: 2 additions & 2 deletions docs/references/function-list-by-name-compact.md
Original file line number Diff line number Diff line change
Expand Up @@ -599,8 +599,8 @@ TYPE | INFORMATION | Information\Value::type

Excel Function | Category | PhpSpreadsheet Function
-------------------------|-----------------------|--------------------------------------
UNICHAR | TEXT_AND_DATA | TextData\CharacterConvert::character
UNICODE | TEXT_AND_DATA | TextData\CharacterConvert::code
UNICHAR | TEXT_AND_DATA | TextData\CharacterConvert::characterUnicode
UNICODE | TEXT_AND_DATA | TextData\CharacterConvert::codeUnicode
UNIQUE | LOOKUP_AND_REFERENCE | LookupRef\Unique::unique
UPPER | TEXT_AND_DATA | TextData\CaseConvert::upper
USDOLLAR | FINANCIAL | Financial\Dollar::format
Expand Down
4 changes: 2 additions & 2 deletions docs/references/function-list-by-name.md
Original file line number Diff line number Diff line change
Expand Up @@ -595,8 +595,8 @@ TYPE | CATEGORY_INFORMATION | \PhpOffice\PhpSpread

Excel Function | Category | PhpSpreadsheet Function
-------------------------|--------------------------------|--------------------------------------
UNICHAR | CATEGORY_TEXT_AND_DATA | \PhpOffice\PhpSpreadsheet\Calculation\TextData\CharacterConvert::character
UNICODE | CATEGORY_TEXT_AND_DATA | \PhpOffice\PhpSpreadsheet\Calculation\TextData\CharacterConvert::code
UNICHAR | CATEGORY_TEXT_AND_DATA | \PhpOffice\PhpSpreadsheet\Calculation\TextData\CharacterConvert::characterUnicode
UNICODE | CATEGORY_TEXT_AND_DATA | \PhpOffice\PhpSpreadsheet\Calculation\TextData\CharacterConvert::codeUnicode
UNIQUE | CATEGORY_LOOKUP_AND_REFERENCE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Unique::unique
UPPER | CATEGORY_TEXT_AND_DATA | \PhpOffice\PhpSpreadsheet\Calculation\TextData\CaseConvert::upper
USDOLLAR | CATEGORY_FINANCIAL | \PhpOffice\PhpSpreadsheet\Calculation\Financial\Dollar::format
Expand Down
4 changes: 2 additions & 2 deletions src/PhpSpreadsheet/Calculation/FunctionArray.php
Original file line number Diff line number Diff line change
Expand Up @@ -2496,12 +2496,12 @@ class FunctionArray extends CalculationBase
],
'UNICHAR' => [
'category' => Category::CATEGORY_TEXT_AND_DATA,
'functionCall' => [TextData\CharacterConvert::class, 'character'],
'functionCall' => [TextData\CharacterConvert::class, 'characterUnicode'],
'argumentCount' => '1',
],
'UNICODE' => [
'category' => Category::CATEGORY_TEXT_AND_DATA,
'functionCall' => [TextData\CharacterConvert::class, 'code'],
'functionCall' => [TextData\CharacterConvert::class, 'codeUnicode'],
'argumentCount' => '1',
],
'UNIQUE' => [
Expand Down
85 changes: 70 additions & 15 deletions src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;

class CharacterConvert
{
use ArrayEnabled;

private static string $oneByteCharacterSet = 'Windows-1252';

/**
* CHAR.
*
Expand All @@ -27,19 +30,45 @@ public static function character(mixed $character): array|string
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $character);
}

return self::characterBoth($character, true);
}

/** @return array<mixed>|string */
public static function characterUnicode(mixed $character): array|string
{
if (is_array($character)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $character);
}

return self::characterBoth($character, false);
}

private static function characterBoth(mixed $character, bool $ansi = true): string
{
try {
$character = Helpers::validateInt($character, true);
} catch (CalcExp $e) {
return $e->getMessage();
}

if ($ansi && $character === 219 && self::$oneByteCharacterSet[0] === 'M') {
return '€';
}

$min = Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE ? 0 : 1;
if ($character < $min || $character > 255) {
if ($character < $min || ($ansi && $character > 255) || $character > 0x10FFFF) {
return ExcelError::VALUE();
}
$result = iconv('UCS-4LE', 'UTF-8', pack('V', $character));
if ($character > 0x10FFFD) { // last assigned
return ExcelError::NA();
}
if ($ansi) {
$result = chr($character);

return ($result === false) ? '' : $result;
return (string) iconv(self::$oneByteCharacterSet, 'UTF-8//IGNORE', $result);
}

return mb_chr($character, 'UTF-8');
}

/**
Expand All @@ -57,7 +86,28 @@ public static function code(mixed $characters): array|string|int
if (is_array($characters)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $characters);
}
if (is_bool($characters) && Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) {
$characters = $characters ? '1' : '0';
}

return self::codeBoth(StringHelper::convertToString($characters, convertBool: true), true);
}

/** @return array<mixed>|int|string */
public static function codeUnicode(mixed $characters): array|string|int
{
if (is_array($characters)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $characters);
}
if (is_bool($characters) && Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) {
$characters = $characters ? '1' : '0';
}

return self::codeBoth(StringHelper::convertToString($characters, convertBool: true), false);
}

private static function codeBoth(string $characters, bool $ansi = true): int|string
{
try {
$characters = Helpers::extractString($characters, true);
} catch (CalcExp $e) {
Expand All @@ -72,22 +122,27 @@ public static function code(mixed $characters): array|string|int
if (mb_strlen($characters, 'UTF-8') > 1) {
$character = mb_substr($characters, 0, 1, 'UTF-8');
}
if ($ansi && $character === '€' && self::$oneByteCharacterSet[0] === 'M') {
return 219;
}

$result = mb_ord($character, 'UTF-8');
if ($ansi) {
$result = iconv('UTF-8', self::$oneByteCharacterSet . '//IGNORE', $character);

return ($result !== '') ? ord("$result") : 63; // question mark
}

return self::unicodeToOrd($character);
return $result;
}

private static function unicodeToOrd(string $character): int
public static function setWindowsCharacterSet(): void
{
$retVal = 0;
$iconv = iconv('UTF-8', 'UCS-4LE', $character);
if ($iconv !== false) {
/** @var false|int[] */
$result = unpack('V', $iconv);
if (is_array($result) && isset($result[1])) {
$retVal = $result[1];
}
}
self::$oneByteCharacterSet = 'Windows-1252';
}

return $retVal;
public static function setMacCharacterSet(): void
{
self::$oneByteCharacterSet = 'MAC';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,46 @@
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\TextData;

use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\TextData\CharacterConvert as CC;
use PHPUnit\Framework\Attributes\DataProvider;

class CharTest extends AllSetupTeardown
{
protected function tearDown(): void
{
parent::tearDown();
CC::setWindowsCharacterSet();
}

#[DataProvider('providerCHAR')]
public function testCHAR(mixed $expectedResult, mixed $character = 'omitted'): void
{
// If expected is array, 1st is for CHAR, 2nd for UNICHAR,
// 3rd is for Mac CHAR if different from Windows.
if (is_array($expectedResult)) {
$expectedResult = $expectedResult[0];
}
$this->mightHaveException($expectedResult);
$sheet = $this->getSheet();
if ($character === 'omitted') {
$sheet->getCell('B1')->setValue('=CHAR()');
} else {
$this->setCell('A1', $character);
$sheet->getCell('B1')->setValue('=CHAR(A1)');
}
$result = $sheet->getCell('B1')->getCalculatedValue();
self::assertEquals($expectedResult, $result);
}

#[DataProvider('providerCHAR')]
public function testMacCHAR(mixed $expectedResult, mixed $character = 'omitted'): void
{
CC::setMacCharacterSet();
// If expected is array, 1st is for CHAR, 2nd for UNICHAR,
// 3rd is for Mac CHAR if different from Windows.
if (is_array($expectedResult)) {
$expectedResult = $expectedResult[2] ?? $expectedResult[0];
}
$this->mightHaveException($expectedResult);
$sheet = $this->getSheet();
if ($character === 'omitted') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,46 @@
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\TextData;

use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\TextData\CharacterConvert as CC;
use PHPUnit\Framework\Attributes\DataProvider;

class CodeTest extends AllSetupTeardown
{
protected function tearDown(): void
{
parent::tearDown();
CC::setWindowsCharacterSet();
}

#[DataProvider('providerCODE')]
public function testCODE(mixed $expectedResult, mixed $character = 'omitted'): void
{
// If expected is array, 1st is for CODE, 2nd for UNICODE,
// 3rd is for Mac CODE if different from Windows.
if (is_array($expectedResult)) {
$expectedResult = $expectedResult[0];
}
$this->mightHaveException($expectedResult);
$sheet = $this->getSheet();
if ($character === 'omitted') {
$sheet->getCell('B1')->setValue('=CODE()');
} else {
$this->setCell('A1', $character);
$sheet->getCell('B1')->setValue('=CODE(A1)');
}
$result = $sheet->getCell('B1')->getCalculatedValue();
self::assertEquals($expectedResult, $result);
}

#[DataProvider('providerCODE')]
public function testMacCODE(mixed $expectedResult, mixed $character = 'omitted'): void
{
CC::setMacCharacterSet();
// If expected is array, 1st is for CODE, 2nd for UNICODE,
// 3rd is for Mac CODE if different from Windows.
if (is_array($expectedResult)) {
$expectedResult = $expectedResult[2] ?? $expectedResult[0];
}
$this->mightHaveException($expectedResult);
$sheet = $this->getSheet();
if ($character === 'omitted') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public function testOpenOffice(mixed $expectedResult, string $formula): void
$sheet = $this->getSheet();
$this->setCell('A1', $formula);
$result = $sheet->getCell('A1')->getCalculatedValue();
self::assertEquals($expectedResult, $result);
self::assertSame($expectedResult, $result);
}

public static function providerOpenOffice(): array
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\TextData;

use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PHPUnit\Framework\Attributes\DataProvider;

class UnicharTest extends AllSetupTeardown
{
#[DataProvider('providerCHAR')]
public function testCHAR(mixed $expectedResult, mixed $character = 'omitted'): void
{
// If expected is array, 1st is for CHAR, 2nd for UNICHAR,
// 3rd is for Mac CHAR if different from Windows.
if (is_array($expectedResult)) {
$expectedResult = $expectedResult[1];
}
$this->mightHaveException($expectedResult);
$sheet = $this->getSheet();
if ($character === 'omitted') {
$sheet->getCell('B1')->setValue('=UNICHAR()');
} else {
$this->setCell('A1', $character);
$sheet->getCell('B1')->setValue('=UNICHAR(A1)');
}
$result = $sheet->getCell('B1')->getCalculatedValue();
self::assertEquals($expectedResult, $result);
}

public static function providerCHAR(): array
{
return require 'tests/data/Calculation/TextData/CHAR.php';
}

/** @param mixed[] $expectedResult */
#[DataProvider('providerCharArray')]
public function testCharArray(array $expectedResult, string $array): void
{
$calculation = Calculation::getInstance();

$formula = "=UNICHAR({$array})";
$result = $calculation->calculateFormula($formula);
self::assertSame($expectedResult, $result);
}

public static function providerCharArray(): array
{
return [
'row vector' => [[['P', 'H', 'P']], '{80, 72, 80}'],
'column vector' => [[['P'], ['H'], ['P']], '{80; 72; 80}'],
'matrix' => [[['Y', 'o'], ['l', 'o']], '{89, 111; 108, 111}'],
];
}
}
Loading