Page MenuHomePhabricator

D14624.id.diff
No OneTemporary

D14624.id.diff

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -38,6 +38,8 @@
'AphrontScopedUnguardedWriteCapability' => 'aphront/writeguard/AphrontScopedUnguardedWriteCapability.php',
'AphrontWriteGuard' => 'aphront/writeguard/AphrontWriteGuard.php',
'BaseHTTPFuture' => 'future/http/BaseHTTPFuture.php',
+ 'CaseInsensitiveArray' => 'utils/CaseInsensitiveArray.php',
+ 'CaseInsensitiveArrayTestCase' => 'utils/__tests__/CaseInsensitiveArrayTestCase.php',
'CommandException' => 'future/exec/CommandException.php',
'ConduitClient' => 'conduit/ConduitClient.php',
'ConduitClientException' => 'conduit/ConduitClientException.php',
@@ -545,6 +547,8 @@
'AphrontScopedUnguardedWriteCapability' => 'Phobject',
'AphrontWriteGuard' => 'Phobject',
'BaseHTTPFuture' => 'Future',
+ 'CaseInsensitiveArray' => 'PhutilArray',
+ 'CaseInsensitiveArrayTestCase' => 'PhutilTestCase',
'CommandException' => 'Exception',
'ConduitClient' => 'Phobject',
'ConduitClientException' => 'Exception',
diff --git a/src/utils/CaseInsensitiveArray.php b/src/utils/CaseInsensitiveArray.php
new file mode 100644
--- /dev/null
+++ b/src/utils/CaseInsensitiveArray.php
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * A case-insensitive associative array.
+ *
+ * This class represents an associative array in which the keys are considered
+ * to be case insensitive. This means that `$array['key']` and `$array['KEY']`
+ * will refer to the same array element.
+ *
+ * ```lang=php
+ * $array = new CaseInsensitiveArray();
+ *
+ * $array['key'] = 'value';
+ * echo $array['KEY']; // 'value'
+ *
+ * $array['kEy'] = 'foobar';
+ * var_dump($array->toArray()); // array('key' => 'foobar')
+ * ```
+ *
+ * Note that it is not possible to reuse case variants of a key. That is, if
+ * the array contains key `xyz` then it is not possible to use any of the
+ * following case variants as an array key: `xyZ`, `xYz`, `xYZ`, `Xyz`, `XyZ`,
+ * `XYz`, `XYZ`. In order to use a case variant as a key, it is necessary to
+ * first unset the original case variant.
+ *
+ * ```lang=php
+ * $array = new CaseInsensitiveArray(array('key' => 'foo', 'KEY' => 'bar'));
+ * var_dump($array->toArray()); // array('key' => 'bar')
+ *
+ * $array['KEY'] = 'baz';
+ * var_dump($array->toArray()); // array('key' => 'baz')
+ *
+ * unset($array['key']);
+ * $array['KEY'] = 'baz';
+ * var_dump($array->toArray()); // array('KEY' => 'baz')
+ * ```
+ */
+final class CaseInsensitiveArray extends PhutilArray {
+
+ /**
+ * Mapping between original and case-invariant keys.
+ *
+ * All keys in the parent `PhutilArray` are indexed using the case-invariant
+ * key rather than the original key.
+ *
+ * @var map<string, string>
+ */
+ private $keys = array();
+
+ /**
+ * Construct a new array object.
+ *
+ * @param array The input array.
+ */
+ public function __construct(array $data = array()) {
+ foreach ($data as $key => $value) {
+ $this->offsetSet($key, $value);
+ }
+ }
+
+ public function getKeys() {
+ return array_values($this->keys);
+ }
+
+ public function offsetExists($key) {
+ $key = $this->transformKey($key);
+ return array_key_exists($key, $this->keys);
+ }
+
+ public function offsetGet($key) {
+ $key = $this->transformKey($key);
+ return parent::offsetGet($this->keys[$key]);
+ }
+
+ public function offsetSet($key, $value) {
+ $transformed_key = $this->transformKey($key);
+
+ if (isset($this->keys[$transformed_key])) {
+ // If the key already exists, reuse it and override the
+ // existing value.
+ $key = $this->keys[$transformed_key];
+ } else {
+ // If the key doesn't yet, create a new array element.
+ $this->keys[$transformed_key] = $key;
+ }
+
+ parent::offsetSet($key, $value);
+ }
+
+ public function offsetUnset($key) {
+ $key = $this->transformKey($key);
+
+ parent::offsetUnset($this->keys[$key]);
+ unset($this->keys[$key]);
+ }
+
+ /**
+ * Transform an array key.
+ *
+ * This method transforms an array key to be case-invariant. We //could//
+ * just call [[http://php.net/manual/en/function.strtolower.php |
+ * `strtolower`]] directly, but this method allows us to contain the key
+ * transformation logic within a single method, should it ever change.
+ *
+ * Theoretically, we should be able to use any of the following functions
+ * for the purpose of key transformations:
+ *
+ * - [[http://php.net/manual/en/function.strtolower.php | `strtolower`]]
+ * - [[http://php.net/manual/en/function.strtoupper.php | `strtoupper`]]
+ * - Some creative use of other
+ * [[http://php.net/manual/en/book.strings.php | string transformation]]
+ * functions.
+ *
+ * @param string The input key.
+ * @return string The transformed key.
+ */
+ private function transformKey($key) {
+ return phutil_utf8_strtolower($key);
+ }
+
+}
diff --git a/src/utils/__tests__/CaseInsensitiveArrayTestCase.php b/src/utils/__tests__/CaseInsensitiveArrayTestCase.php
new file mode 100644
--- /dev/null
+++ b/src/utils/__tests__/CaseInsensitiveArrayTestCase.php
@@ -0,0 +1,109 @@
+<?php
+
+final class CaseInsensitiveArrayTestCase extends PhutilTestCase {
+
+ public function testCount() {
+ $array = new CaseInsensitiveArray();
+ $this->assertEqual(0, count($array));
+
+ $array['key'] = 'foo';
+ $this->assertEqual(1, count($array));
+
+ $array['KEY'] = 'bar';
+ $this->assertEqual(1, count($array));
+ }
+
+ public function testGetKeys() {
+ $input = array(
+ 'key' => true,
+ 'KEY' => true,
+ 'kEy' => true,
+
+ 'foo' => false,
+ 'value' => false,
+ );
+ $expected = array(
+ 'key',
+ 'foo',
+ 'value',
+ );
+
+ $array = new CaseInsensitiveArray($input);
+ $this->assertEqual($expected, $array->getKeys());
+ }
+
+ public function testOffsetExists() {
+ $input = array('key' => 'value');
+ $expectations = array(
+ 'key' => true,
+ 'KEY' => true,
+ 'kEy' => true,
+
+ 'foo' => false,
+ 'value' => false,
+ );
+
+ $array = new CaseInsensitiveArray($input);
+
+ foreach ($expectations as $key => $expectation) {
+ if ($expectation) {
+ $this->assertTrue(isset($array[$key]));
+ } else {
+ $this->assertFalse(isset($array[$key]));
+ }
+ }
+ }
+
+ public function testOffsetGet() {
+ $input = array('key' => 'value');
+ $expectations = array(
+ 'key' => 'value',
+ 'KEY' => 'value',
+ 'kEy' => 'value',
+
+ 'foo' => null,
+ 'value' => null,
+ );
+
+ $array = new CaseInsensitiveArray($input);
+
+ foreach ($expectations as $key => $expectation) {
+ $this->assertEqual($expectation, @$array[$key]);
+ }
+ }
+
+ public function testOffsetSet() {
+ $input = array();
+ $data = array(
+ 'key' => 'foo',
+ 'KEY' => 'bar',
+ 'kEy' => 'baz',
+ );
+ $expected = array('key' => 'baz');
+
+ $array = new CaseInsensitiveArray($input);
+
+ foreach ($data as $key => $value) {
+ $array[$key] = $value;
+ }
+
+ $this->assertEqual($expected, $array->toArray());
+ }
+
+ public function testOffsetUnset() {
+ $input = array('key' => 'value');
+ $data = array(
+ 'KEY',
+ );
+ $expected = array();
+
+ $array = new CaseInsensitiveArray($input);
+
+ foreach ($data as $key) {
+ unset($array[$key]);
+ }
+
+ $this->assertEqual($expected, $array->toArray());
+ }
+
+}

File Metadata

Mime Type
text/plain
Expires
Sun, May 12, 9:02 PM (2 w, 5 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6290650
Default Alt Text
D14624.id.diff (7 KB)

Event Timeline