Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UnicodeUtilityにIsEmptyOrWhiteSpaceとGetLengthを追加 #152

Merged
merged 4 commits into from
Sep 4, 2023
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
26 changes: 18 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,25 @@ using Utf8Utility.Text;
// stringまたはUTF-8のバイト配列、ReadOnlySpan{char|byte}を指定できます。
var array = new Utf8Array("abc");

var span = array.AsSpan();

// バイト数
var byteCount = array.ByteCount;

// 文字数
var length = array.GetLength();
var length2 = UnicodeUtility.GetLength(span);

// 空かどうか
var isEmpty = array.IsEmpty;

// 空か空白文字列かどうか
var isEmptyOrWhiteSpace = array.IsEmptyOrWhiteSpace();
var isEmptyOrWhiteSpace2 = UnicodeUtility.IsEmptyOrWhiteSpace(span);

// Ascii文字列かどうか
var isAscii = array.IsAscii();
var isAscii2 = UnicodeUtility.IsAscii(span);

// 内部配列への参照
ref var start = ref array.DangerousGetReference();
Expand All @@ -61,26 +66,30 @@ var empty = Utf8Array.Empty;
var equals = array.Equals(array);
var hash = array.GetHashCode();
var utf16 = array.ToString();
var span = array.AsSpan();
array.TryFormat(stackalloc char[256], out var charsWritten);

_ = array.TryFormat(stackalloc char[256], out var charsWritten);
_ = array.TryFormat(stackalloc byte[256], out var bytesWritten);

array.CopyTo(stackalloc byte[256]);
_ = array.TryCopyTo(stackalloc byte[256]);

_ = array.GetChars(stackalloc char[256]);
_ = array.TryGetChars(stackalloc char[256], out var charsWritten);

// Utf8ArrayをキーとしたDictionaryです。
var dict = new Utf8ArrayDictionary<int>();

// キー指定にはUtf8Arrayの他にReadOnlySpan{char|byte}を指定できます。
dict.TryGetValue(array, out var result);
_ = dict.TryGetValue(array, out var result);
ref var dictStart = ref dict.GetValueRefOrNullRef(array);

dict.TryAdd(array, 1);
_ = dict.TryAdd(array, 1);
dict.Clear();

// Ascii文字列かどうか
var span = array.AsSpan();
var isAscii = UnicodeUtility.IsAscii(span);
```

## サポートフレームワーク

- .NET 8
- .NET 7
- .NET 6
- .NET Standard 2.1
Expand All @@ -99,6 +108,7 @@ MIT

### ライブラリ

- [CommunityToolkit.Diagnostics](https://github.com/CommunityToolkit/dotnet)
- [CommunityToolkit.HighPerformance](https://github.com/CommunityToolkit/dotnet)

### テスト
Expand Down
10 changes: 0 additions & 10 deletions Source/Utf8Utility/Helpers/BitOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,4 @@ public static int RoundUpToPowerOf2(int value)
return ++value;
}
#endif

#if NET6_0_OR_GREATER
/// <summary>
/// 立っているビット数を取得します。
/// </summary>
/// <param name="value">数値</param>
/// <returns>立っているビット数を返します。</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int PopCount(ulong value) => System.Numerics.BitOperations.PopCount(value);
#endif
}
115 changes: 115 additions & 0 deletions Source/Utf8Utility/Text/UnicodeUtility.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using CommunityToolkit.HighPerformance;

#if NET6_0_OR_GREATER
using System.Numerics;
using System.Text;
#endif

namespace Utf8Utility.Text;

Expand Down Expand Up @@ -50,4 +56,113 @@ public static int GetUtf8SequenceLength(byte value)
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsAsciiCodePoint(byte value) => value <= 0x7Fu;

/// <summary>
/// UTF-8文字数を取得します。
/// </summary>
/// <param name="value">UTF-8文字列</param>
/// <returns>UTF-8文字数を返します。</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetLength(ReadOnlySpan<byte> value)
{
var count = 0;
nuint index = 0;

#if NET6_0_OR_GREATER
const ulong Mask = 0x8080808080808080 >> 7;
var length = value.Length - sizeof(ulong);

// 8バイト単位でカウントする。
while ((int)index <= length)
{
var number = Unsafe.As<byte, ulong>(ref Unsafe.AddByteOffset(ref value.DangerousGetReference(), index));

var x = ((number >> 6) | (~number >> 7)) & Mask;
count += BitOperations.PopCount(x);
index += sizeof(ulong);
}
#endif

// 1バイト単位でカウントする。
while ((int)index < value.Length)
{
var number = Unsafe.AddByteOffset(ref value.DangerousGetReference(), index);

if ((number & 0xC0) != 0x80)
{
count++;
}

index++;
}

return count;
}

#if NET6_0_OR_GREATER
/// <summary>
/// UTF-8文字列が空または空白かどうかを判定します。
/// </summary>
/// <param name="value">UTF-8文字列</param>
/// <returns>
/// UTF-8配列が空または空白の場合は<see langword="true"/>、
/// それ以外は<see langword="false"/>を返します。
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsEmptyOrWhiteSpace(ReadOnlySpan<byte> value)
{
// Utf8Spanを参考に実装
// https://github.com/dotnet/runtimelab/blob/84564a0e033114a1b2316c7bfb9953e4e3255cd3/src/libraries/System.Private.CoreLib/src/System/Text/Utf8Span.cs#L124
// https://github.com/dotnet/runtimelab/blob/84564a0e033114a1b2316c7bfb9953e4e3255cd3/src/libraries/System.Private.CoreLib/src/System/Text/Unicode/Utf8Utility.WhiteSpace.CoreLib.cs#L11-L68
nuint index = 0;
var length = value.Length;

while ((int)index < length)
{
ref var valueStart = ref Unsafe.AddByteOffset(ref value.DangerousGetReference(), index);

// 文字コードが[0x21..0x7F]の範囲にあるか。
if ((sbyte)valueStart > (sbyte)' ')
{
break;
}

// Ascii文字の場合のみ、処理を最適化する。
// 空白確認には、{Rune|char}.IsWhiteSpaceを利用できる。
// Rune.DecodeFromUtf8は引数チェックなどがあるので遅く、
// 回避するためにUnsafe.Asでvalueを直接書き換える必要があるが、Ascii文字限定。
// charにキャストして比較する方法もAscii文字限定。
// したがって、最適化を行う場合はAscii文字かどうかでの分岐は必須。
if (IsAsciiCodePoint(valueStart))
{
// 直前の処理でAscii文字であることは確定しているため、
// {Rune|char}.IsWhiteSpaceを使用せず、自前実装で比較を行う。
// 上記メソッドではAscii文字かどうかで判定が入ってしまうため。
// https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs#L1350-L1366
// https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Char.cs#L274-L287
if (AsciiUtility.IsWhiteSpace(valueStart))
{
index++;
continue;
}
}
else
{
var span = MemoryMarshal.CreateReadOnlySpan(ref valueStart, length - (int)index);
Rune.DecodeFromUtf8(span, out var rune, out var bytesConsumed);

if (Rune.IsWhiteSpace(rune))
{
index += (uint)bytesConsumed;
continue;
}
}

// ここに到達した場合、空白以外の文字のはず。
break;
}

return (int)index == length;
}
#endif
}
108 changes: 7 additions & 101 deletions Source/Utf8Utility/Utf8Array.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@
using CommunityToolkit.HighPerformance.Helpers;
using Utf8Utility.Text;

#if NET6_0_OR_GREATER
using Utf8Utility.Helpers;
#endif

namespace Utf8Utility;

/// <summary>
Expand Down Expand Up @@ -91,7 +87,7 @@ public Utf8Array(ReadOnlySpan<char> chars)
/// 空文字列を表す<see cref="Utf8Array"/>インスタンスを取得します。
/// </summary>
/// <value>
/// 空文字列を表す<see cref="Utf8Array"/>インスタンス
/// 空文字列を表す<see cref="Utf8Array"/>インスタンスを返します。
/// </value>
public static Utf8Array Empty
{
Expand All @@ -103,17 +99,17 @@ public static Utf8Array Empty
/// バイト数を取得します。
/// </summary>
/// <value>
/// バイト数
/// 内部配列のバイト数を返します。
/// </value>
public int ByteCount => _value.Length;

/// <summary>
/// UTF-8配列が空かどうかを判定します
/// UTF-8配列が空かどうかを取得します
/// </summary>
/// <returns>
/// <value>
/// UTF-8配列が空の場合は<see langword="true"/>、
/// それ以外は<see langword="false"/>を返します。
/// </returns>
/// </value>
public bool IsEmpty
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down Expand Up @@ -283,43 +279,7 @@ public ReadOnlySpan<byte> AsSpan()
/// </summary>
/// <returns>UTF-8文字数を返します。</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetLength()
{
var count = 0;
nuint index = 0;

#if NET6_0_OR_GREATER
const ulong Mask = 0x8080808080808080 >> 7;
var length = ByteCount - sizeof(ulong);

// 8バイト単位でカウントする。
while ((int)index <= length)
{
// 最適化の関係でrefローカル変数にしてはいけない。
var value = Unsafe.As<byte, ulong>(ref Unsafe.AddByteOffset(ref DangerousGetReference(), index));

var x = ((value >> 6) | (~value >> 7)) & Mask;
count += BitOperations.PopCount(x);
index += sizeof(ulong);
}
#endif

// 1バイト単位でカウントする。
while ((int)index < ByteCount)
{
// 最適化の関係でrefローカル変数にしてはいけない。
var value = Unsafe.AddByteOffset(ref DangerousGetReference(), index);

if ((value & 0xC0) != 0x80)
{
count++;
}

index++;
}

return count;
}
public int GetLength() => UnicodeUtility.GetLength(AsSpan());

/// <summary>
/// UTF-8配列をUTF-16配列に変換します。
Expand Down Expand Up @@ -365,61 +325,7 @@ public bool TryGetChars(Span<char> destination, out int charsWritten)
/// それ以外は<see langword="false"/>を返します。
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsEmptyOrWhiteSpace()
{
// Utf8Spanを参考に実装
// https://github.com/dotnet/runtimelab/blob/84564a0e033114a1b2316c7bfb9953e4e3255cd3/src/libraries/System.Private.CoreLib/src/System/Text/Utf8Span.cs#L124
// https://github.com/dotnet/runtimelab/blob/84564a0e033114a1b2316c7bfb9953e4e3255cd3/src/libraries/System.Private.CoreLib/src/System/Text/Unicode/Utf8Utility.WhiteSpace.CoreLib.cs#L11-L68
nuint index = 0;

while ((int)index < _value.Length)
{
ref var valueStart = ref DangerousGetReference();
ref var value = ref Unsafe.AddByteOffset(ref valueStart, index);

// 文字コードが[0x21..0x7F]の範囲にあるか。
if ((sbyte)value > (sbyte)' ')
{
break;
}

// Ascii文字の場合のみ、処理を最適化する。
// 空白確認には、{Rune|char}.IsWhiteSpaceを利用できる。
// Rune.DecodeFromUtf8は引数チェックなどがあるので遅く、
// 回避するためにUnsafe.Asでvalueを直接書き換える必要があるが、Ascii文字限定。
// charにキャストして比較する方法もAscii文字限定。
// したがって、最適化を行う場合はAscii文字かどうかでの分岐は必須。
if (UnicodeUtility.IsAsciiCodePoint(value))
{
// 直前の処理でAscii文字であることは確定しているため、
// {Rune|char}.IsWhiteSpaceを使用せず、自前実装で比較を行う。
// 上記メソッドではAscii文字かどうかで判定が入ってしまうため。
// https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs#L1350-L1366
// https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Char.cs#L274-L287
if (AsciiUtility.IsWhiteSpace(value))
{
index++;
continue;
}
}
else
{
var span = MemoryMarshal.CreateReadOnlySpan(ref value, _value.Length - (int)index);
Rune.DecodeFromUtf8(span, out var rune, out var bytesConsumed);

if (Rune.IsWhiteSpace(rune))
{
index += (uint)bytesConsumed;
continue;
}
}

// ここに到達した場合、空白以外の文字のはず。
break;
}

return (int)index == _value.Length;
}
public bool IsEmptyOrWhiteSpace() => UnicodeUtility.IsEmptyOrWhiteSpace(AsSpan());
#endif

/// <summary>
Expand Down
Loading