Skip to content

Commit ac5ce66

Browse files
authored
Merge pull request #4727 from oleibman/codeunicode
CODE/UNICODE and CHAR/UNICHAR
2 parents fd923c9 + 919c6c1 commit ac5ce66

File tree

14 files changed

+334
-44
lines changed

14 files changed

+334
-44
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
2929

3030
### Fixed
3131

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

3434
## 2025-11-24 - 5.3.0
3535

docs/references/function-list-by-category.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -569,8 +569,8 @@ THAINUMSOUND | **Not yet Implemented**
569569
THAINUMSTRING | **Not yet Implemented**
570570
THAISTRINGLENGTH | **Not yet Implemented**
571571
TRIM | \PhpOffice\PhpSpreadsheet\Calculation\TextData\Trim::spaces
572-
UNICHAR | \PhpOffice\PhpSpreadsheet\Calculation\TextData\CharacterConvert::character
573-
UNICODE | \PhpOffice\PhpSpreadsheet\Calculation\TextData\CharacterConvert::code
572+
UNICHAR | \PhpOffice\PhpSpreadsheet\Calculation\TextData\CharacterConvert::characterUnicode
573+
UNICODE | \PhpOffice\PhpSpreadsheet\Calculation\TextData\CharacterConvert::codeUnicode
574574
UPPER | \PhpOffice\PhpSpreadsheet\Calculation\TextData\CaseConvert::upper
575575
VALUE | \PhpOffice\PhpSpreadsheet\Calculation\TextData\Format::VALUE
576576
VALUETOTEXT | \PhpOffice\PhpSpreadsheet\Calculation\TextData\Format::valueToText

docs/references/function-list-by-name-compact.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -599,8 +599,8 @@ TYPE | INFORMATION | Information\Value::type
599599

600600
Excel Function | Category | PhpSpreadsheet Function
601601
-------------------------|-----------------------|--------------------------------------
602-
UNICHAR | TEXT_AND_DATA | TextData\CharacterConvert::character
603-
UNICODE | TEXT_AND_DATA | TextData\CharacterConvert::code
602+
UNICHAR | TEXT_AND_DATA | TextData\CharacterConvert::characterUnicode
603+
UNICODE | TEXT_AND_DATA | TextData\CharacterConvert::codeUnicode
604604
UNIQUE | LOOKUP_AND_REFERENCE | LookupRef\Unique::unique
605605
UPPER | TEXT_AND_DATA | TextData\CaseConvert::upper
606606
USDOLLAR | FINANCIAL | Financial\Dollar::format

docs/references/function-list-by-name.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -595,8 +595,8 @@ TYPE | CATEGORY_INFORMATION | \PhpOffice\PhpSpread
595595

596596
Excel Function | Category | PhpSpreadsheet Function
597597
-------------------------|--------------------------------|--------------------------------------
598-
UNICHAR | CATEGORY_TEXT_AND_DATA | \PhpOffice\PhpSpreadsheet\Calculation\TextData\CharacterConvert::character
599-
UNICODE | CATEGORY_TEXT_AND_DATA | \PhpOffice\PhpSpreadsheet\Calculation\TextData\CharacterConvert::code
598+
UNICHAR | CATEGORY_TEXT_AND_DATA | \PhpOffice\PhpSpreadsheet\Calculation\TextData\CharacterConvert::characterUnicode
599+
UNICODE | CATEGORY_TEXT_AND_DATA | \PhpOffice\PhpSpreadsheet\Calculation\TextData\CharacterConvert::codeUnicode
600600
UNIQUE | CATEGORY_LOOKUP_AND_REFERENCE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Unique::unique
601601
UPPER | CATEGORY_TEXT_AND_DATA | \PhpOffice\PhpSpreadsheet\Calculation\TextData\CaseConvert::upper
602602
USDOLLAR | CATEGORY_FINANCIAL | \PhpOffice\PhpSpreadsheet\Calculation\Financial\Dollar::format

src/PhpSpreadsheet/Calculation/FunctionArray.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2496,12 +2496,12 @@ class FunctionArray extends CalculationBase
24962496
],
24972497
'UNICHAR' => [
24982498
'category' => Category::CATEGORY_TEXT_AND_DATA,
2499-
'functionCall' => [TextData\CharacterConvert::class, 'character'],
2499+
'functionCall' => [TextData\CharacterConvert::class, 'characterUnicode'],
25002500
'argumentCount' => '1',
25012501
],
25022502
'UNICODE' => [
25032503
'category' => Category::CATEGORY_TEXT_AND_DATA,
2504-
'functionCall' => [TextData\CharacterConvert::class, 'code'],
2504+
'functionCall' => [TextData\CharacterConvert::class, 'codeUnicode'],
25052505
'argumentCount' => '1',
25062506
],
25072507
'UNIQUE' => [

src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@
66
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp;
77
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
88
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
9+
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
910

1011
class CharacterConvert
1112
{
1213
use ArrayEnabled;
1314

15+
private static string $oneByteCharacterSet = 'Windows-1252';
16+
1417
/**
1518
* CHAR.
1619
*
@@ -27,19 +30,45 @@ public static function character(mixed $character): array|string
2730
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $character);
2831
}
2932

33+
return self::characterBoth($character, true);
34+
}
35+
36+
/** @return array<mixed>|string */
37+
public static function characterUnicode(mixed $character): array|string
38+
{
39+
if (is_array($character)) {
40+
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $character);
41+
}
42+
43+
return self::characterBoth($character, false);
44+
}
45+
46+
private static function characterBoth(mixed $character, bool $ansi = true): string
47+
{
3048
try {
3149
$character = Helpers::validateInt($character, true);
3250
} catch (CalcExp $e) {
3351
return $e->getMessage();
3452
}
3553

54+
if ($ansi && $character === 219 && self::$oneByteCharacterSet[0] === 'M') {
55+
return '';
56+
}
57+
3658
$min = Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE ? 0 : 1;
37-
if ($character < $min || $character > 255) {
59+
if ($character < $min || ($ansi && $character > 255) || $character > 0x10FFFF) {
3860
return ExcelError::VALUE();
3961
}
40-
$result = iconv('UCS-4LE', 'UTF-8', pack('V', $character));
62+
if ($character > 0x10FFFD) { // last assigned
63+
return ExcelError::NA();
64+
}
65+
if ($ansi) {
66+
$result = chr($character);
4167

42-
return ($result === false) ? '' : $result;
68+
return (string) iconv(self::$oneByteCharacterSet, 'UTF-8//IGNORE', $result);
69+
}
70+
71+
return mb_chr($character, 'UTF-8');
4372
}
4473

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

93+
return self::codeBoth(StringHelper::convertToString($characters, convertBool: true), true);
94+
}
95+
96+
/** @return array<mixed>|int|string */
97+
public static function codeUnicode(mixed $characters): array|string|int
98+
{
99+
if (is_array($characters)) {
100+
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $characters);
101+
}
102+
if (is_bool($characters) && Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) {
103+
$characters = $characters ? '1' : '0';
104+
}
105+
106+
return self::codeBoth(StringHelper::convertToString($characters, convertBool: true), false);
107+
}
108+
109+
private static function codeBoth(string $characters, bool $ansi = true): int|string
110+
{
61111
try {
62112
$characters = Helpers::extractString($characters, true);
63113
} catch (CalcExp $e) {
@@ -72,22 +122,27 @@ public static function code(mixed $characters): array|string|int
72122
if (mb_strlen($characters, 'UTF-8') > 1) {
73123
$character = mb_substr($characters, 0, 1, 'UTF-8');
74124
}
125+
if ($ansi && $character === '' && self::$oneByteCharacterSet[0] === 'M') {
126+
return 219;
127+
}
128+
129+
$result = mb_ord($character, 'UTF-8');
130+
if ($ansi) {
131+
$result = iconv('UTF-8', self::$oneByteCharacterSet . '//IGNORE', $character);
132+
133+
return ($result !== '') ? ord("$result") : 63; // question mark
134+
}
75135

76-
return self::unicodeToOrd($character);
136+
return $result;
77137
}
78138

79-
private static function unicodeToOrd(string $character): int
139+
public static function setWindowsCharacterSet(): void
80140
{
81-
$retVal = 0;
82-
$iconv = iconv('UTF-8', 'UCS-4LE', $character);
83-
if ($iconv !== false) {
84-
/** @var false|int[] */
85-
$result = unpack('V', $iconv);
86-
if (is_array($result) && isset($result[1])) {
87-
$retVal = $result[1];
88-
}
89-
}
141+
self::$oneByteCharacterSet = 'Windows-1252';
142+
}
90143

91-
return $retVal;
144+
public static function setMacCharacterSet(): void
145+
{
146+
self::$oneByteCharacterSet = 'MAC';
92147
}
93148
}

tests/PhpSpreadsheetTests/Calculation/Functions/TextData/CharTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,46 @@
55
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\TextData;
66

77
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
8+
use PhpOffice\PhpSpreadsheet\Calculation\TextData\CharacterConvert as CC;
89
use PHPUnit\Framework\Attributes\DataProvider;
910

1011
class CharTest extends AllSetupTeardown
1112
{
13+
protected function tearDown(): void
14+
{
15+
parent::tearDown();
16+
CC::setWindowsCharacterSet();
17+
}
18+
1219
#[DataProvider('providerCHAR')]
1320
public function testCHAR(mixed $expectedResult, mixed $character = 'omitted'): void
1421
{
22+
// If expected is array, 1st is for CHAR, 2nd for UNICHAR,
23+
// 3rd is for Mac CHAR if different from Windows.
24+
if (is_array($expectedResult)) {
25+
$expectedResult = $expectedResult[0];
26+
}
27+
$this->mightHaveException($expectedResult);
28+
$sheet = $this->getSheet();
29+
if ($character === 'omitted') {
30+
$sheet->getCell('B1')->setValue('=CHAR()');
31+
} else {
32+
$this->setCell('A1', $character);
33+
$sheet->getCell('B1')->setValue('=CHAR(A1)');
34+
}
35+
$result = $sheet->getCell('B1')->getCalculatedValue();
36+
self::assertEquals($expectedResult, $result);
37+
}
38+
39+
#[DataProvider('providerCHAR')]
40+
public function testMacCHAR(mixed $expectedResult, mixed $character = 'omitted'): void
41+
{
42+
CC::setMacCharacterSet();
43+
// If expected is array, 1st is for CHAR, 2nd for UNICHAR,
44+
// 3rd is for Mac CHAR if different from Windows.
45+
if (is_array($expectedResult)) {
46+
$expectedResult = $expectedResult[2] ?? $expectedResult[0];
47+
}
1548
$this->mightHaveException($expectedResult);
1649
$sheet = $this->getSheet();
1750
if ($character === 'omitted') {

tests/PhpSpreadsheetTests/Calculation/Functions/TextData/CodeTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,46 @@
55
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\TextData;
66

77
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
8+
use PhpOffice\PhpSpreadsheet\Calculation\TextData\CharacterConvert as CC;
89
use PHPUnit\Framework\Attributes\DataProvider;
910

1011
class CodeTest extends AllSetupTeardown
1112
{
13+
protected function tearDown(): void
14+
{
15+
parent::tearDown();
16+
CC::setWindowsCharacterSet();
17+
}
18+
1219
#[DataProvider('providerCODE')]
1320
public function testCODE(mixed $expectedResult, mixed $character = 'omitted'): void
1421
{
22+
// If expected is array, 1st is for CODE, 2nd for UNICODE,
23+
// 3rd is for Mac CODE if different from Windows.
24+
if (is_array($expectedResult)) {
25+
$expectedResult = $expectedResult[0];
26+
}
27+
$this->mightHaveException($expectedResult);
28+
$sheet = $this->getSheet();
29+
if ($character === 'omitted') {
30+
$sheet->getCell('B1')->setValue('=CODE()');
31+
} else {
32+
$this->setCell('A1', $character);
33+
$sheet->getCell('B1')->setValue('=CODE(A1)');
34+
}
35+
$result = $sheet->getCell('B1')->getCalculatedValue();
36+
self::assertEquals($expectedResult, $result);
37+
}
38+
39+
#[DataProvider('providerCODE')]
40+
public function testMacCODE(mixed $expectedResult, mixed $character = 'omitted'): void
41+
{
42+
CC::setMacCharacterSet();
43+
// If expected is array, 1st is for CODE, 2nd for UNICODE,
44+
// 3rd is for Mac CODE if different from Windows.
45+
if (is_array($expectedResult)) {
46+
$expectedResult = $expectedResult[2] ?? $expectedResult[0];
47+
}
1548
$this->mightHaveException($expectedResult);
1649
$sheet = $this->getSheet();
1750
if ($character === 'omitted') {

tests/PhpSpreadsheetTests/Calculation/Functions/TextData/OpenOfficeTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public function testOpenOffice(mixed $expectedResult, string $formula): void
1414
$sheet = $this->getSheet();
1515
$this->setCell('A1', $formula);
1616
$result = $sheet->getCell('A1')->getCalculatedValue();
17-
self::assertEquals($expectedResult, $result);
17+
self::assertSame($expectedResult, $result);
1818
}
1919

2020
public static function providerOpenOffice(): array
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\TextData;
6+
7+
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
10+
class UnicharTest extends AllSetupTeardown
11+
{
12+
#[DataProvider('providerCHAR')]
13+
public function testCHAR(mixed $expectedResult, mixed $character = 'omitted'): void
14+
{
15+
// If expected is array, 1st is for CHAR, 2nd for UNICHAR,
16+
// 3rd is for Mac CHAR if different from Windows.
17+
if (is_array($expectedResult)) {
18+
$expectedResult = $expectedResult[1];
19+
}
20+
$this->mightHaveException($expectedResult);
21+
$sheet = $this->getSheet();
22+
if ($character === 'omitted') {
23+
$sheet->getCell('B1')->setValue('=UNICHAR()');
24+
} else {
25+
$this->setCell('A1', $character);
26+
$sheet->getCell('B1')->setValue('=UNICHAR(A1)');
27+
}
28+
$result = $sheet->getCell('B1')->getCalculatedValue();
29+
self::assertEquals($expectedResult, $result);
30+
}
31+
32+
public static function providerCHAR(): array
33+
{
34+
return require 'tests/data/Calculation/TextData/CHAR.php';
35+
}
36+
37+
/** @param mixed[] $expectedResult */
38+
#[DataProvider('providerCharArray')]
39+
public function testCharArray(array $expectedResult, string $array): void
40+
{
41+
$calculation = Calculation::getInstance();
42+
43+
$formula = "=UNICHAR({$array})";
44+
$result = $calculation->calculateFormula($formula);
45+
self::assertSame($expectedResult, $result);
46+
}
47+
48+
public static function providerCharArray(): array
49+
{
50+
return [
51+
'row vector' => [[['P', 'H', 'P']], '{80, 72, 80}'],
52+
'column vector' => [[['P'], ['H'], ['P']], '{80; 72; 80}'],
53+
'matrix' => [[['Y', 'o'], ['l', 'o']], '{89, 111; 108, 111}'],
54+
];
55+
}
56+
}

0 commit comments

Comments
 (0)