potisanのプログラミングメモ

趣味のプログラマーがプログラミング関係で気になったことや調べたことをいつでも忘れられるようにメモするブログです。はてなブログ無料版なので記事の上の方はたぶん広告です。記事中にも広告挿入されるみたいです。

JavaScript 配列様オブジェクトの作成とArray.from()での利用

配列様オブジェクト(Array-like object)

MDNのArray.from()の説明には「Array-like object」(「配列様オブジェクト」)あるいは「配列のようなオブジェクト」という表現があります。具体的には次のようなオブジェクトです。

{length: 5};
{length: 5, "1", 1, "2", 2};

これらが配列様オブジェクトであることは次のコードで確認できます。なお、リテラル表記法で作成したオブジェクトなので配列でないことは自明としています。

Array.from({length: 5});
// Array(5) [ undefined, undefined, undefined, undefined, undefined ]
Array.from({length: 5, "1": 1, "2": 2});
// Array(5) [ undefined, 1, 2, undefined, undefined ]

{length: 5}は長さ5で全要素undefinedの配列、{length: 5, "1": 1, "2": 2}は長さ5で1、2番目の要素が1、2、他はundefinedの配列とみなされています。

これらの共通点はlengthプロパティを持つことです。また、lengthプロパティの値は整数とみなせれば問題ないようです。

Array.from({length: "5"})
// Array(5) [ undefined, undefined, undefined, undefined, undefined ]
Array.from({length: 5.5})
// Array(5) [ undefined, undefined, undefined, undefined, undefined ]
Array.from({length: "a"})
// Array []

逆に整数とみなせるキーは持つがlengthプロパティを持たないオブジェクトは配列様オブジェクトとはみなされません。次のコードで確かめられます。

Array.from({"0": 0, "1": 1, "2": 2});
// Array []

配列様オブジェクトのArray.from()での利用

Array.from()メソッド(MDN)を使えば余分な配列を作成せずにある長さの配列を作成できます。例えば次のコードは長さ5でbからfまでの文字の配列を作成します。

Array.from({length: 5}, (_, i) => String.fromCodePoint("b".codePointAt(0) + i));
// Array(5) [ "b", "c", "d", "e", "f" ]

JavaScript 文字列をUTF-16コードユニットの配列へ変換する

以下のコードで文字列をUTF-16コードユニットの配列に変換できます。

Array.from("🍎🍊😁", s => Array.from({length: s.length}, (_, i) => s.charCodeAt(i)))
// Array(3) [ [ 55356, 57166 ], [ 55356, 57162 ], [ 55357, 56833 ] ]

Array.from("🍎🍊😁", s => Array.from({length: s.length}, (_, i) => s.charCodeAt(i))).flat()
// Array(6) [ 55356, 57166, 55356, 57162, 55357, 56833 ]

関数にしておくと便利かもしれません。

const getUTF16CharPointsFromString = (source) => {
    if (typeof source != "string") return undefined;
    return Array.from(source, s => Array.from({length: s.length}, (_, i) => s.charCodeAt(i)))
}

getUTF16CharPointsFromString("🍎🍊😁")
// Array(3) [ [ 55356, 57166 ], [ 55356, 57162 ], [ 55357, 56833 ]]

getUTF16CharPointsFromString("🍎🍊😁").flat()
// Array(6) [ 55356, 57166, 55356, 57162, 55357, 56833 ]

JavaScript 文字列をUnicodeコードポイントへ変換する

本文

以下のコードで文字列をUnicodeコードポイントの配列に変換できます。

Array.from("🍎🍊😁", s => s.codePointAt(0));
// Array(3) [ 127822, 127818, 128513 ]

関数にしておくと便利かもしれません。

const getCodePointsFromString = (source) => {
    if (typeof source != "string") return undefined;
    return Array.from(source, s => s.codePointAt(0));
}

getCodePointsFromString("🍊🍎😁");
// Array(3) [ 127822, 127818, 128513 ]

このコードでは以下を利用しています。

  • 文字列のイテレーターはコードポイント(単位の文字列)を返す(参考:MDN
  • 文字列からコードポイント(整数)への変換はString.prototype.codePointAt()メソッド(参考:MDN)。
  • Array.from()メソッドは第1引数に配列や反復可能オブジェクト、第2引数に変換関数を受け取る(参考:MDN)。

文字列のイテレーターが反復するオブジェクトがコードポイント単位の文字列であることは次のコードで確認できます。

console.log(Array.from("🍎🍊😁", s => [s, typeof(s)]));
// (3) […]
// 0: Array [ "🍎", "string" ]
// 1: Array [ "🍊", "string" ]
// 2: Array [ "😁", "string" ]

備考1:文字列のlengthプロパティはUTF-16コードユニット数

なお、文字列のlengthプロパティはUTF-16コードユニット数を返すことに注意してください。次のようにサロゲートペアが含まれる場合に結果が異なります。

"🍎🍊😁".length // 6
Array.from("🍎🍊😁", s => s.codePointAt(0)).length // 3

備考2:スプレッド構文は新しい配列を作る

スプレッド構文...iteratorにより文字列のイテレーターを配列に変換することもできます。ただし、この場合はコードポイントに対応する文字を要素とした新しい配列が作成されます。

[..."🍎🍊😁"].map(s => s.codePointAt(0))
// Array(3) [ 127822, 127818, 128513 ]

C# 9 バイト配列からメモリ上の読み枠を広げた整数配列を作成する

#nullable enable

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static uint[] CreateUInt32ArrayRough(ReadOnlySpan<byte> value)
    => MemoryMarshal.Cast<byte, uint>(value).ToArray();

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static uint[] CreateUInt32ArrayStrict(ReadOnlySpan<byte> value)
{
    if (value.Length % 4 != 0)
        throw new ArgumentException("領域の長さが不正です。");
    return CreateUInt32ArrayRough(value);
}

SpanReadOnlySpan)とMemoryMarshal.Castを使用してバイト配列を整数配列へ変換できます。MemoryMarshal.Castは元のメモリ領域の長さが不足しても余分を無視して成功してしまうため、余分の無視を許容するRough版と例外を発生するStrict版を用意しています。

また現在のC#ではジェネリックsizeof(T)が使えないため、具体的なuintUInt32)に対してCreateUInt32ArrayRough/Strictを定義しています。

使用例

#nullable enable

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

var b = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 };

// uint[2] {0x04030201, 0x08070605}
var ui1 = CreateUInt32ArrayRough(b);
// uint[1] {0x04030201]
var ui2 = CreateUInt32ArrayRough(b[0..5]);

// uint[2] {0x04030201, 0x08070605}
var ui3 = CreateUInt32ArrayStrict(b);
// <ArgumentException>
var ui4 = CreateUInt32ArrayStrict(b[0..5]);

Console.WriteLine();

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static uint[] CreateUInt32ArrayRough(ReadOnlySpan<byte> value)
    => MemoryMarshal.Cast<byte, uint>(value).ToArray();

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static uint[] CreateUInt32ArrayStrict(ReadOnlySpan<byte> value)
{
    if (value.Length % 4 != 0)
        throw new ArgumentException("領域の長さが不正です。");
    return CreateUInt32ArrayRough(value);
}

C++ Windows環境で絵文字のstd::iswgraphがfalseな理由の考察

VC++ではワイド文字列(L"...")に絵文字を含めることができます。この文字列に対してstd::iswgraphを適用しても戻り値はfalseです。

#include <string>
#include <cwctype>
#include <iostream>
#include <iomanip>

int main()
{
    const std::wstring s{ L"🍊🍎😁" };
    for (auto i = s.cbegin(); i != s.cend(); i++)
    {
        auto c = *i;
        std::wcout
            << L"0x" << std::hex << std::setfill(L'0') << std::setw(4) << static_cast<size_t>(c)
            << L" : " << std::boolalpha << !!std::iswgraph(c) << std::endl;
    }
}

出力結果

0xd83c : false
0xdf4a : false
0xd83c : false
0xdf4e : false
0xd83d : false
0xde01 : false

この背景にはWindowsではワイド文字が16ビットであることとUTF-16サロゲートペアを含むことが関与しています。

  1. Windows環境ではワイド文字が16ビット

ワイド文字(wchar_t)のサイズは環境依存で、Windowsでは16ビット、LinuxmacOS環境では32ビットだそうです。したがって、Windows開発ではワイド文字/文字列はUTF-16を指します。

  1. UTF-16サロゲートペアを含む

Unicode 11.0.0ではコードポイント(文字などに対応する番号)として0x0~0x10ffffffが予約されています。一方、UTF-16は16ビットなので0x0~0xffffしか表現できません。UTF-16で0x10000以上のコードポイントを表すため、サロゲートペアと呼ばれる方法が導入されています。

サロゲートペアはUTF-16で使われていなかった2つの領域を上位サロゲート、下位サロゲートとして予約し、上位サロゲートと下位サロゲートのペアで1つのコードポイントを表す方法です。

UTF-16はよく使う文字は16ビット、たまに使われる文字は32ビット(サロゲートペア)で表せるのでUTF-32よりもデータサイズに優れるメリットを持ちますが、扱いの複雑さをデメリットに持ち、Windows環境で絵文字のstd::iswgraphfalseになる理由もこれに関連しています。

std::iswgraphwchar_t型の引数を取り、wchar_tWindows環境では16ビット整数です。std::iswgraphは与えられた引数だけで文字のタイプを判断するため、サロゲートペアの上位・下位を別々に与えられると指すものが分からずfalseを返していると考えられます。

参考:Unicode - Wikipedia