File: src\runtime\src\libraries\System.Private.CoreLib\src\System\Text\RunePosition.cs
Web Access
Project: System.Private.CoreLib.csproj (System.Private.CoreLib)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;
using System.Buffers;
using System.Collections.Generic;
using System.Collections;

namespace System.Text;

/// <summary>
/// Represents a position in Unicode data, allowing for deeper data inspection.
/// </summary>
/// <remarks>
/// Invalid Unicode symbols will be represented by the <see cref="System.Text.Rune.ReplacementChar"/> value.
/// </remarks>
public readonly struct RunePosition : IEquatable<RunePosition>
{
    /// <summary>
    /// Returns an enumeration of <see cref="RunePosition"/> from the provided span that allows deeper data inspection.
    /// </summary>
    /// <param name="span">The <see cref="ReadOnlySpan{T}"/> with Unicode data.</param>
    /// <returns>
    /// <see cref="Utf16Enumerator"/> to enumerate <see cref="RunePosition"/> from the provided span with UTF-16
    /// Unicode data.
    /// </returns>
    /// <remarks>
    /// Invalid Unicode symbols will be represented by <see cref="System.Text.Rune.ReplacementChar"/>
    /// value.
    /// </remarks>
    public static Utf16Enumerator EnumerateUtf16(ReadOnlySpan<char> span) => new(span);

    /// <summary>
    /// Returns an enumeration of <see cref="RunePosition"/> from the provided span that allows deeper data inspection.
    /// </summary>
    /// <param name="span">The <see cref="ReadOnlySpan{T}"/> with Unicode data.</param>
    /// <returns>
    /// <see cref="Utf8Enumerator"/> to enumerate <see cref="RunePosition"/> from the provided span with UTF-8 Unicode
    /// data.
    /// </returns>
    /// <remarks>
    /// Invalid Unicode symbols will be represented by <see cref="Rune.ReplacementChar"/> value.
    /// </remarks>
    public static Utf8Enumerator EnumerateUtf8(ReadOnlySpan<byte> span) => new(span);

    /// <summary>
    /// Unicode scalar value <see cref="System.Text.Rune"/> of the current symbol in Unicode data.
    /// Invalid Unicode symbols will be represented by <see cref="System.Text.Rune.ReplacementChar"/> value.
    /// </summary>
    public Rune Rune { get; }

    /// <summary>
    /// The index of current symbol in Unicode data.
    /// </summary>
    public int StartIndex { get; }

    /// <summary>
    /// The length of current symbol in Unicode data.
    /// </summary>
    public int Length { get; }

    /// <summary>
    /// <see langword="false"/> it current Unicode symbol is correct encoded and <see cref="RunePosition.Rune"/>
    /// contain its scalar value.
    /// <br />
    /// <see langword="true"/> if current Unicode symbol is invalid encoded and <see cref="RunePosition.Rune"/> was
    /// replaced by <see cref="System.Text.Rune.ReplacementChar"/> value.
    /// </summary>
    public bool WasReplaced { get; }

    /// <summary>
    /// Initializes a new instance of the <see cref="RunePosition"/> struct.
    /// </summary>
    /// <param name="rune">The Unicode scalar value.</param>
    /// <param name="startIndex">The index of the current symbol in Unicode data.</param>
    /// <param name="length">The length of the current symbol in Unicode data.</param>
    /// <param name="wasReplaced">Indicates if the current Unicode symbol was replaced.</param>
    public RunePosition(Rune rune, int startIndex, int length, bool wasReplaced)
    {
        if (startIndex < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_NeedNonNegNum);
        }

        if ((uint)length > Rune.MaxUtf8BytesPerRune)
        {
            throw new ArgumentOutOfRangeException(nameof(length), SR.ArgumentOutOfRange_NeedNonNegNum);
        }

        StartIndex = startIndex;
        Length = length;
        Rune = rune;
        WasReplaced = wasReplaced;
    }

    /// <summary>
    /// Determines whether the specified <see cref="RunePosition"/> is equal to the current <see cref="RunePosition"/>.
    /// </summary>
    /// <param name="other">The other <see cref="RunePosition"/> to compare with.</param>
    /// <returns>
    /// <see langword="true"/> if the specified <see cref="RunePosition"/> is equal to the current
    /// <see cref="RunePosition"/>; otherwise, <see langword="false"/>.
    /// </returns>
    public bool Equals(RunePosition other) =>
        Rune == other.Rune && StartIndex == other.StartIndex && Length == other.Length && WasReplaced == other.WasReplaced;

    /// <summary>
    /// Determines whether the specified <see cref="object"/> is equal to the current <see cref="RunePosition"/>.
    /// </summary>
    /// <param name="obj">The object to compare with the current <see cref="RunePosition"/>.</param>
    /// <returns>
    /// <see langword="true"/> if the specified <see cref="object"/> is equal to the current
    /// <see cref="RunePosition"/>; otherwise, <see langword="false"/>.
    /// </returns>
    public override bool Equals(object? obj) =>
        obj is RunePosition runePosition && Equals(runePosition);

    /// <summary>
    /// Returns the hash code for the current <see cref="RunePosition"/>.
    /// </summary>
    /// <returns>The hash code for the current <see cref="RunePosition"/>.</returns>
    public override int GetHashCode() =>
        HashCode.Combine(Rune, StartIndex, Length, WasReplaced);

    /// <summary>
    /// Deconstructs the <see cref="RunePosition"/> into its components.
    /// </summary>
    /// <param name="rune">The Unicode scalar value.</param>
    /// <param name="startIndex">The index of the current symbol in Unicode data.</param>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public void Deconstruct(out Rune rune, out int startIndex)
    {
        rune = Rune;
        startIndex = StartIndex;
    }

    /// <summary>
    /// Deconstructs the <see cref="RunePosition"/> into its components.
    /// </summary>
    /// <param name="rune">The Unicode scalar value.</param>
    /// <param name="startIndex">The index of the current symbol in Unicode data.</param>
    /// <param name="length">The length of the current symbol in Unicode data.</param>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public void Deconstruct(out Rune rune, out int startIndex, out int length)
    {
        rune = Rune;
        startIndex = StartIndex;
        length = Length;
    }

    /// <summary>
    /// Determines whether two specified <see cref="RunePosition"/> instances are equal.
    /// </summary>
    /// <param name="left">The first <see cref="RunePosition"/> to compare.</param>
    /// <param name="right">The second <see cref="RunePosition"/> to compare.</param>
    /// <returns>
    /// <see langword="true"/> if the two <see cref="RunePosition"/> instances are equal; otherwise,
    /// <see langword="false"/>.
    /// </returns>
    public static bool operator ==(RunePosition left, RunePosition right) => left.Equals(right);

    /// <summary>
    /// Determines whether two specified <see cref="RunePosition"/> instances are not equal.
    /// </summary>
    /// <param name="left">The first <see cref="RunePosition"/> to compare.</param>
    /// <param name="right">The second <see cref="RunePosition"/> to compare.</param>
    /// <returns>
    /// <see langword="true"/> if the two <see cref="RunePosition"/> instances are not equal; otherwise,
    /// <see langword="false"/>.
    /// </returns>
    public static bool operator !=(RunePosition left, RunePosition right) => !(left == right);

    /// <summary>
    /// An enumerator for retrieving <see cref="RunePosition"/> instances from Unicode data.
    /// </summary>
    /// <remarks>
    /// Methods are pattern-matched by compiler to allow using foreach pattern.
    /// </remarks>
    public ref struct Utf16Enumerator : IEnumerator<RunePosition>
    {
        private ReadOnlySpan<char> _original;
        private ReadOnlySpan<char> _remaining;

        /// <summary>
        /// The current <see cref="RunePosition"/> in the Unicode data.
        /// </summary>
        public RunePosition Current { get; private set; }

        /// <summary>
        /// Returns the current enumerator instance.
        /// </summary>
        /// <returns>The current enumerator instance.</returns>
        public Utf16Enumerator GetEnumerator() => this;

        internal Utf16Enumerator(ReadOnlySpan<char> buffer)
        {
            _original = _remaining = buffer;
            Current = default;
        }

        /// <summary>
        /// Moves to the next <see cref="RunePosition"/> in the Unicode data.
        /// </summary>
        /// <returns>
        /// <see langword="true"/> if the enumerator was successfully advanced to the next <see cref="RunePosition"/>;
        /// <br />
        /// <see langword="false"/> if the enumerator has passed the end of the span.</returns>
        public bool MoveNext()
        {
            if (_remaining.IsEmpty)
            {
                // reached the end of the buffer
                Current = default;
                return false;
            }

            // In UTF-16 specifically, invalid sequences always have length 1, which is the same
            // length as the replacement character U+FFFD. This means that we can always bump the
            // next index by the current scalar's UTF-16 sequence length. This optimization is not
            // generally applicable; for example, enumerating scalars from UTF-8 cannot utilize
            // this same trick.

            int scalarValue = Rune.ReadFirstRuneFromUtf16Buffer(_remaining);
            if (scalarValue >= 0)
            {
                Rune rune = Rune.UnsafeCreate((uint)scalarValue);
                int length = rune.Utf16SequenceLength;
                Current = new RunePosition(rune, Current.StartIndex + Current.Length, length, false);
                _remaining = _remaining.Slice(length);
            }
            else
            {
                Current = new RunePosition(Rune.ReplacementChar, Current.StartIndex + Current.Length, 1, true);
                _remaining = _remaining.Slice(1);
            }
            return true;
        }

        public void Reset()
        {
            _remaining = _original;
            Current = default;
        }

        object IEnumerator.Current => Current;

        void IEnumerator.Reset() => Reset();

        void IDisposable.Dispose() { }
    }

    /// <summary>
    /// An enumerator for retrieving <see cref="RunePosition"/> instances from Unicode data.
    /// </summary>
    /// <remarks>
    /// Methods are pattern-matched by compiler to allow using foreach pattern.
    /// </remarks>
    public ref struct Utf8Enumerator : IEnumerator<RunePosition>
    {
        private ReadOnlySpan<byte> _original;
        private ReadOnlySpan<byte> _remaining;

        /// <summary>
        /// The current <see cref="RunePosition"/> in the Unicode data.
        /// </summary>
        public RunePosition Current { get; private set; }

        /// <summary>
        /// Returns the current enumerator instance.
        /// </summary>
        /// <returns>The current enumerator instance.</returns>
        public Utf8Enumerator GetEnumerator() => this;

        /// <summary>
        /// Initializes a new instance of the <see cref="Utf8Enumerator"/> struct.
        /// </summary>
        /// <param name="buffer">The buffer containing the Unicode data.</param>
        internal Utf8Enumerator(ReadOnlySpan<byte> buffer)
        {
            _original = _remaining = buffer;
            Current = default;
        }

        /// <summary>
        /// Moves to the next <see cref="RunePosition"/> in the Unicode data.
        /// </summary>
        /// <returns>
        /// <see langword="true"/> if the enumerator was successfully advanced to the next <see cref="RunePosition"/>;
        /// <br />
        /// <see langword="false"/> if the enumerator has passed the end of the span.
        /// </returns>
        public bool MoveNext()
        {
            if (_remaining.IsEmpty)
            {
                // reached the end of the buffer
                Current = default;
                return false;
            }

            bool wasReplaced = Rune.DecodeFromUtf8(_remaining, out Rune rune, out int charsConsumed) != OperationStatus.Done;
            Current = new RunePosition(rune, Current.StartIndex + Current.Length, charsConsumed, wasReplaced);
            _remaining = _remaining.Slice(charsConsumed);
            return true;
        }

        public void Reset()
        {
            _remaining = _original;
            Current = default;
        }

        object IEnumerator.Current => Current;

        void IEnumerator.Reset() => Reset();

        void IDisposable.Dispose() { }
    }
}