Skip to content

Commit 7891fbf

Browse files
committed
Fixed a bunch of small issues and some general improvements found by Claude Code. Updated to version 0.9.3.
1 parent f0096f2 commit 7891fbf

15 files changed

Lines changed: 212 additions & 86 deletions

AGENTS.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# AGENTS.md
2+
3+
This file provides guidance to AI agents when working with code in this repository.
4+
5+
## Build Commands
6+
7+
```bash
8+
dotnet restore # Restore dependencies
9+
dotnet build --no-restore # Build solution
10+
dotnet test --no-build # Run tests
11+
dotnet test --verbosity normal # Run tests with detailed output
12+
```
13+
14+
The solution targets .NET Standard 2.0 (library) and .NET 8.0 (tests). Tests use MSTest framework.
15+
16+
## Architecture Overview
17+
18+
UnicodeHelper is a .NET library that provides Unicode property access and enhanced string handling for characters beyond the Basic Multilingual Plane (codepoints > U+FFFF). It supports Unicode 17.0.0.
19+
20+
### Core Types
21+
22+
**UCodepoint** (`UnicodeHelper/UCodepoint.cs`) - Readonly struct representing a single Unicode codepoint (0x0000-0x10FFFF). Primary API for querying Unicode properties. Implicitly converts from `char` for BMP characters.
23+
24+
**UString** (`UnicodeHelper/UString.cs`) - Sealed class representing a sequence of Unicode codepoints. Unlike .NET strings, treats upper-plane characters (emoji, etc.) as single units rather than surrogate pairs. Work-in-progress; not all methods fully implemented.
25+
26+
**UStringBuilder** (`UnicodeHelper/UStringBuilder.cs`) - Efficient builder for constructing UStrings. Uses ArrayPool for buffer management; must be disposed.
27+
28+
### Unicode Data Classes
29+
30+
**UnicodeData** (`UnicodeHelper/UnicodeData.cs`) - Core Unicode property data: categories, bidirectional classes, numeric values, case mappings, composition/decomposition mappings.
31+
32+
**UnicodeProperties** (`UnicodeHelper/UnicodeProperties.cs`) - Advanced Unicode properties from PropList.txt and DerivedCoreProperties.txt.
33+
34+
**UnicodeNames** (`UnicodeHelper/UnicodeNames.cs`) - Character names and aliases from DerivedName.txt and NameAliases.txt.
35+
36+
**UnicodeBlocks** (`UnicodeHelper/UnicodeBlocks.cs`) - Maps codepoints to Unicode blocks.
37+
38+
### Extension Methods
39+
40+
**DotNetStringExtensions** - Extends .NET strings with `Codepoints()` iterator and `DetermineDirection()` for text direction (implements Unicode TR9).
41+
42+
### Internal Components
43+
44+
Located in `UnicodeHelper/Internal/`:
45+
- **DataHelper** - Reads embedded Unicode data from Resources.zip
46+
- **NormalizationEngine** - Unicode normalization (NFC, NFD, NFKC, NFKD), includes Hangul handling
47+
- **HelperUtils** - Text direction algorithm implementation
48+
49+
### Embedded Resources
50+
51+
All Unicode data files are embedded in `UnicodeHelper/Resources/Resources.zip` for offline use. Data is loaded lazily on first access via static constructors.
52+
53+
## Testing Approach
54+
55+
Every implemented method should be extensively tested. Tests compare behavior against .NET's built-in Unicode support where applicable. The `UnicodeHelper.Tests/TestData/NormalizationTest.txt` contains normalization test cases. Data-driven tests use `[DynamicData]` attributes.

CLAUDE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
@AGENTS.md

UnicodeHelper.Tests/UStringTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -309,12 +309,13 @@ public void IndexOf_UCodepoint(string testString, UCodepoint codePoint, int star
309309
#region LastIndexOf_UCodepoint tests
310310
private static IEnumerable<object[]> UCodepointLastIndexOfExceptionTestData =>
311311
[
312+
["", 0, 0, typeof(ArgumentOutOfRangeException)],
312313
["", 0, 1, typeof(ArgumentOutOfRangeException)],
313314
["", 1, 0, typeof(ArgumentOutOfRangeException)],
314-
["This", -1, 2, typeof(ArgumentException)],
315+
["This", -1, 2, typeof(ArgumentOutOfRangeException)],
315316
["This", 3, 5, typeof(ArgumentException)],
316317
["This", 3, -1, typeof(ArgumentOutOfRangeException)],
317-
["This", -1, 1, typeof(ArgumentException)],
318+
["This", -1, 1, typeof(ArgumentOutOfRangeException)],
318319
["😁🤔😮", 1, 3, typeof(ArgumentException)],
319320
["😁🤔😮", 3, 1, typeof(ArgumentOutOfRangeException)]
320321
];
@@ -330,11 +331,10 @@ public void LastIndexOf_UCodepoint_InvalidParameters(string testString, int star
330331

331332
private static IEnumerable<object[]> UCodepointLastIndexOfTestData =>
332333
[
333-
["", (UCodepoint)'A', -1, 0, -1],
334334
["This is\r\na test!", (UCodepoint)'\r', 8, 6, 7],
335335
["This is\r\na test!", (UCodepoint)'i', 15, 16, 5],
336336
["This is\r\na test!", (UCodepoint)'T', 8, 6, -1],
337-
["This", (UCodepoint)'T', -1, 0, -1],
337+
["This", (UCodepoint)'T', 0, 0, -1],
338338
["This", (UCodepoint)'s', 3, 0, -1],
339339
["This", (UCodepoint)'i', 2, 0, -1],
340340
["العربية", (UCodepoint)'ا', 6, 7, 0],

UnicodeHelper.Tests/UnicodeDataTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ public class UnicodeDataTests
66
[TestMethod]
77
public void Version()
88
{
9-
Assert.AreEqual(new Version(16, 0, 0), UnicodeData.UnicodeVersion);
9+
Assert.AreEqual(new Version(17, 0, 0), UnicodeData.UnicodeVersion);
1010
}
1111
}
1212
}

UnicodeHelper/AGENTS.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# AGENTS.md
2+
3+
This file provides guidance to AI agents when working with code in this repository.
4+
5+
## Module Purpose
6+
7+
This is the core library implementing Unicode property access and codepoint-aware string handling. It abstracts away .NET's UTF-16 surrogate pair complexity by treating all Unicode codepoints (including upper-plane characters like emoji) as single units.
8+
9+
## Architectural Decisions
10+
11+
### Data Storage Strategy
12+
Unicode property data uses large arrays indexed directly by codepoint value (0-0x10FFFF = 1,114,112 entries). This trades memory for O(1) lookup performance:
13+
- `UnicodeData.categories[]` - byte array for UnicodeCategory
14+
- `UnicodeData.bidiClasses[]` - enum array for bidirectional class
15+
- `UnicodeProperties.props[]` - UnicodeProperty flags array
16+
- Dictionary lookups only for sparse data (numeric values, case mappings, decomposition mappings)
17+
18+
### Lazy Initialization Pattern
19+
All static data classes (`UnicodeData`, `UnicodeProperties`, `UnicodeNames`, `UnicodeBlocks`) use static constructors that load from embedded resources on first access. Each provides an empty `Init()` method to allow explicit initialization timing (e.g., during splash screen).
20+
21+
### Substring Sharing
22+
`UString` instances share their backing `UCodepoint[]` array. Substrings store `_startIndex` and `Length` offsets into the parent array rather than copying. This makes substring operations O(1) but means the parent array stays in memory as long as any substring exists.
23+
24+
### Memory Pooling
25+
`UStringBuilder` uses `ArrayPool<UCodepoint>.Shared` for buffer management. **Must be disposed** to return buffers to the pool. The finalizer also returns the buffer, but relying on it is inefficient.
26+
27+
## Design Patterns
28+
29+
### Facade Pattern
30+
`UCodepoint` acts as a facade for Unicode property lookups. Static methods like `GetUnicodeCategory()`, `IsWhiteSpace()`, `ToUpper()` delegate to `UnicodeData` or `UnicodeProperties` internally.
31+
32+
### Value Type Wrapper
33+
`UCodepoint` is a readonly struct wrapping a single `int _value`. Provides implicit conversion from `char` (for BMP characters) and explicit conversions to/from `int`. Operator overloads allow direct comparison with integers and chars.
34+
35+
### Internal Namespace Separation
36+
`UnicodeHelper.Internal` contains implementation helpers not part of the public API:
37+
- `DataHelper` - Resource loading and CSV parsing configuration
38+
- `NormalizationEngine` - Unicode normalization (ported from W3C reference)
39+
- `HelperUtils` - Canonical sorting, text direction algorithm
40+
- `UnicodeConversion` - String-to-enum conversions for Unicode data files
41+
42+
## Key Implementation Details
43+
44+
### Unicode Data File Format
45+
Uses CsvHelper with semicolon delimiter (`Delimiter = ";"`) to parse Unicode Consortium data files. Comment character is `#`. Configuration in `DataHelper.CsvConfiguration`.
46+
47+
### Normalization Implementation
48+
`NormalizationEngine` is ported from the W3C reference implementation. Handles Hangul syllable decomposition/composition separately using algorithmic approach (constants `SBase`, `LBase`, `VBase`, `TBase`). Decomposition mappings are pre-expanded fully during initialization.
49+
50+
### Bidi Default Values
51+
`UnicodeData.Init()` sets default bidirectional classes by range before loading actual data. Ranges follow DerivedBidiClass.txt specification (e.g., 0x0590-0x05FF defaults to RightToLeft).
52+
53+
### Combining Key Encoding
54+
Composition/decomposition lookups use packed keys:
55+
- Decomposition: `(compatMapping << 21) | codepoint` as `int`
56+
- Composition: `(compatMapping << 42) | (base << 21) | combining` as `long`
57+
58+
## Work-in-Progress Methods
59+
60+
These `UString` methods throw `NotImplementedException`:
61+
- `IndexOf(UString, ...)` - substring search
62+
- `LastIndexOf(UString, ...)`
63+
- `StartsWith(UString, ...)`
64+
- `EndsWith(UString, ...)`
65+
- `Contains(...)`
66+
67+
## Gotchas
68+
69+
1. **UStringBuilder disposal**: Failure to dispose leaks pooled arrays. Always use `using` statement.
70+
71+
2. **CharLength vs Length**: `UString.Length` counts codepoints; `CharLength` counts UTF-16 chars (different for upper-plane characters).
72+
73+
3. **Initialization time**: First access to `UnicodeData` takes ~300ms, `UnicodeProperties` ~150ms. Consider calling `Init()` during app startup.
74+
75+
4. **Explicit cast required**: Converting `UCodepoint` to `int` or `char` requires explicit cast: `(int)uc`, `(char)uc`.

UnicodeHelper/CLAUDE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
@AGENTS.md

UnicodeHelper/Internal/UnicodeConversion.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using System.Diagnostics;
34
using System.Globalization;
45

@@ -155,28 +156,31 @@ internal static class UnicodeConversion
155156

156157
public static UnicodeCategory ConvertCategory(string categoryStr)
157158
{
158-
return strToCategoryMap[categoryStr];
159+
return strToCategoryMap.TryGetValue(categoryStr, out UnicodeCategory cat) ? cat :
160+
throw new ArgumentException("Unknown category " + categoryStr);
159161
}
160162

161163
public static UnicodeBidiClass ConvertBidiClass(string bidiClassStr)
162164
{
163-
return strToBidiClassMap[bidiClassStr];
165+
return strToBidiClassMap.TryGetValue(bidiClassStr, out UnicodeBidiClass bidiClass) ? bidiClass :
166+
throw new ArgumentException("Unknown bi-di class " + bidiClassStr);
164167
}
165168

166169
public static UnicodeProperty ConvertProperty(string propertyStr)
167170
{
168-
return strToPropertyMap[propertyStr];
171+
return strToPropertyMap.TryGetValue(propertyStr, out UnicodeProperty prop) ? prop :
172+
throw new ArgumentException("Unknown property " + propertyStr);
169173
}
170174

171175
public static double ConvertNumeric(string numericStr)
172176
{
173177
string[] numbers = numericStr.Split('/');
174-
double value = double.Parse(numbers[0]);
178+
double value = double.Parse(numbers[0], CultureInfo.InvariantCulture);
175179
if (numbers.Length == 1)
176180
return value;
177181

178182
Debug.Assert(numbers.Length == 2);
179-
double bottom = double.Parse(numbers[1]);
183+
double bottom = double.Parse(numbers[1], CultureInfo.InvariantCulture);
180184
return value / bottom;
181185
}
182186
}

UnicodeHelper/NameInfo.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace UnicodeHelper
66
/// Contains name information about a character
77
/// </summary>
88
[PublicAPI]
9-
public sealed class NameInfo
9+
public readonly struct NameInfo
1010
{
1111
internal NameInfo(string name, NameType nameType)
1212
{

UnicodeHelper/UCodepoint.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,12 @@ public static UCodepoint FromChar(char c)
6868
return new UCodepoint(c);
6969
}
7070

71-
internal UCodepoint(char value)
71+
private UCodepoint(char value)
7272
{
7373
_value = value;
7474
}
7575

76-
internal UCodepoint(int value)
76+
private UCodepoint(int value)
7777
{
7878
_value = value;
7979
}

UnicodeHelper/UString.cs

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System;
33
using System.Collections;
44
using System.Collections.Generic;
5+
using System.Diagnostics;
56
using System.Diagnostics.CodeAnalysis;
67
using System.Text;
78
using UnicodeHelper.Internal;
@@ -14,7 +15,7 @@ namespace UnicodeHelper
1415
[PublicAPI]
1516
public sealed class UString :
1617
IEquatable<UString>,
17-
IEnumerable<UCodepoint>,
18+
IReadOnlyList<UCodepoint>,
1819
ICloneable,
1920
IComparable,
2021
IComparable<UString>
@@ -29,7 +30,7 @@ public sealed class UString :
2930
/// <summary>Index of the codepoint in the array where this string starts (for a substring)</summary>
3031
private readonly int _startIndex;
3132

32-
private int _cachedHash;
33+
private int? _cachedHash;
3334
#endregion
3435

3536
#region Constructors
@@ -87,20 +88,36 @@ public UString(string dotNetString, int startIndex, int count)
8788
if (startIndex + count > dotNetString.Length)
8889
throw new ArgumentException("startIndex and count must reside in the string");
8990

90-
UCodepoint[] codepoints = new UCodepoint[count];
91-
int index = 0;
91+
// Memory allocation and copying is slow enough that it's slightly faster to calculate the correct size
92+
// than it is to guess and resize when finished.
9293
int end = startIndex + count;
94+
int totalCodePointCount = 0;
95+
for (int i = startIndex; i < end; i++)
96+
{
97+
totalCodePointCount++;
98+
if (char.IsHighSurrogate(dotNetString[i]))
99+
{
100+
int nextIndex = i + 1;
101+
if (nextIndex < end && char.IsLowSurrogate(dotNetString[nextIndex]))
102+
i++; // Step over low surrogate
103+
}
104+
}
105+
106+
UCodepoint[] codepoints = new UCodepoint[totalCodePointCount];
107+
int codePointIndex = 0;
93108
for (int i = startIndex; i < end; i++)
94109
{
95110
UCodepoint uc = UCodepoint.ReadFromStr(dotNetString, i);
96-
codepoints[index++] = uc;
111+
codepoints[codePointIndex++] = uc;
97112
if (uc > 0xFFFF)
98113
i++; // Step over low surrogate
99114
}
100115

116+
Debug.Assert(totalCodePointCount == codePointIndex, "Precalculation of size was incorrect");
117+
101118
_codepoints = codepoints;
102119
_startIndex = 0;
103-
Length = index;
120+
Length = totalCodePointCount;
104121
}
105122

106123
internal UString(int start, int length, UCodepoint[] codepoints)
@@ -138,6 +155,13 @@ public int CharLength
138155
return charCount;
139156
}
140157
}
158+
#endregion
159+
160+
#region Implementation of IReadOnlyList
161+
/// <summary>
162+
/// Gets the number of Unicode codepoints that make up this Unicode string
163+
/// </summary>
164+
public int Count => Length;
141165

142166
/// <summary>
143167
/// Gets the Unicode codepoint at the specified index in this Unicode string
@@ -440,7 +464,7 @@ public int IndexOf(UCodepoint value, int startIndex, int count)
440464
/// </summary>
441465
public int LastIndexOf(UCodepoint value)
442466
{
443-
return LastIndexOf(value, Length - 1, Length);
467+
return Length == 0 ? -1 : LastIndexOf(value, Length - 1, Length);
444468
}
445469

446470
/// <summary>
@@ -464,6 +488,8 @@ public int LastIndexOf(UCodepoint value, int startIndex, int count)
464488
throw new ArgumentOutOfRangeException(nameof(count), "count is less than zero");
465489
if (startIndex >= Length)
466490
throw new ArgumentOutOfRangeException(nameof(startIndex), "startIndex must be less than the length of the string");
491+
if (startIndex < 0)
492+
throw new ArgumentOutOfRangeException(nameof(startIndex), "startIndex must be greater than zero");
467493
if (startIndex - count + 1 < 0)
468494
throw new ArgumentException("StartIndex and count must reside in the string");
469495

@@ -782,15 +808,17 @@ public TextDirection DetermineDirection()
782808
public override int GetHashCode()
783809
{
784810
// TODO: Write tests for this method
785-
if (_cachedHash != 0)
786-
return _cachedHash;
787-
788-
HashCode hc = new HashCode();
789-
int end = _startIndex + Length;
790-
for (int i = _startIndex; i < end; i++)
791-
hc.Add(_codepoints[i]);
811+
if (_cachedHash == null)
812+
{
813+
HashCode hc = new HashCode();
814+
int end = _startIndex + Length;
815+
for (int i = _startIndex; i < end; i++)
816+
hc.Add(_codepoints[i]);
817+
818+
_cachedHash = hc.ToHashCode();
819+
}
792820

793-
return _cachedHash = hc.ToHashCode();
821+
return _cachedHash.Value;
794822
}
795823

796824
/// <inheritdoc />

0 commit comments

Comments
 (0)