File: System\Text\RegularExpressions\GroupCollection.cs
Web Access
Project: src\src\libraries\System.Text.RegularExpressions\src\System.Text.RegularExpressions.csproj (System.Text.RegularExpressions)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
 
namespace System.Text.RegularExpressions
{
    /// <summary>
    /// Returns the set of captured groups in a single match. The collection is immutable (read-only) and
    /// has no public constructor.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The <see cref="GroupCollection" /> class is a zero-based collection class that consists of one or
    /// more <see cref="Group" /> objects that provide information about captured groups in a regular
    /// expression match. The collection is immutable (read-only) and has no public constructor. A
    /// <see cref="GroupCollection" /> object is returned by the <see cref="Match.Groups" /> property.
    /// </para>
    /// <para>
    /// The collection contains one or more <see cref="Group" /> objects. If the match is successful, the
    /// first element in the collection contains the <see cref="Group" /> object that corresponds to the
    /// entire match. Each subsequent element represents a captured group, if the regular expression
    /// includes capturing groups. Matches from numbered (unnamed) capturing groups appear in numeric
    /// order before matches from named capturing groups. If the match is unsuccessful, the collection
    /// contains a single <see cref="Group" /> object whose <see cref="Group.Success" /> property is
    /// <see langword="false" /> and whose <see cref="Capture.Value" /> property equals
    /// <see cref="string.Empty" />.
    /// </para>
    /// <para>
    /// To iterate through the members of the collection, you should use the collection iteration
    /// construct provided by your language (such as <c>foreach</c> in C#) instead of retrieving the
    /// enumerator that is returned by the <see cref="GetEnumerator" /> method.
    /// </para>
    /// </remarks>
    [DebuggerDisplay("Count = {Count}")]
    [DebuggerTypeProxy(typeof(CollectionDebuggerProxy<Group>))]
    public class GroupCollection : IList<Group>, IReadOnlyList<Group>, IList, IReadOnlyDictionary<string, Group>
    {
        private readonly Match _match;
        private readonly Hashtable? _captureMap;
 
        /// <summary>Cache of Group objects fed to the user.</summary>
        private Group[]? _groups;
 
        internal GroupCollection(Match match, Hashtable? caps)
        {
            _match = match;
            _captureMap = caps;
        }
 
        internal void Reset() => _groups = null;
 
        /// <summary>Gets a value that indicates whether the collection is read only.</summary>
        /// <value><see langword="true" /> in all cases.</value>
        public bool IsReadOnly => true;
 
        /// <summary>Returns the number of groups in the collection.</summary>
        /// <value>The number of groups in the collection.</value>
        /// <remarks>
        /// The <see cref="GroupCollection" /> object always has at least one member. If a match is
        /// unsuccessful, the <see cref="Match.Groups" /> property returns a
        /// <see cref="GroupCollection" /> object that contains a single member.
        /// </remarks>
        public int Count => _match._matchcount.Length;
 
        /// <summary>Enables access to a member of the collection by integer index.</summary>
        /// <param name="groupnum">The zero-based index of the collection member to be retrieved.</param>
        /// <value>The member of the collection specified by <paramref name="groupnum" />.</value>
        /// <remarks>
        /// <para>
        /// This property is the indexer of the <see cref="GroupCollection" />
        /// class. It allows you to enumerate the members of the collection by using a <c>foreach</c>
        /// statement.
        /// </para>
        /// <para>
        /// You can also use this property to retrieve individual captured groups by their index number.
        /// You can determine the number of items in the collection by retrieving the value of the
        /// <see cref="Count" /> property. Valid values for the <paramref name="groupnum" /> parameter
        /// range from 0 to one less than the number of items in the collection.
        /// </para>
        /// <para>
        /// If <paramref name="groupnum" /> is not the index of a member of the collection, or if
        /// <paramref name="groupnum" /> is the index of a capturing group that has not been matched in
        /// the input string, the method returns a <see cref="Group" /> object whose
        /// <see cref="Group.Success" /> property is <see langword="false" /> and whose
        /// <c>Group.Value</c> property is <see cref="string.Empty" />.
        /// </para>
        /// </remarks>
        public Group this[int groupnum] => GetGroup(groupnum);
 
        /// <summary>Enables access to a member of the collection by string index.</summary>
        /// <param name="groupname">The name of a capturing group.</param>
        /// <value>The member of the collection specified by <paramref name="groupname" />.</value>
        /// <remarks>
        /// <para>
        /// <paramref name="groupname" /> can be either the name of a capturing group that is defined by
        /// the <c>(?&lt;name&gt;)</c> element in a regular expression, or the string representation of
        /// the number of a capturing group.
        /// </para>
        /// <para>
        /// If <paramref name="groupname" /> is not the name of a capturing group in the collection, or
        /// if <paramref name="groupname" /> is the name of a capturing group that has not been matched in
        /// the input string, the method returns a <see cref="Group" /> object whose
        /// <see cref="Group.Success" /> property is <see langword="false" /> and whose
        /// <c>Group.Value</c> property is <see cref="string.Empty" />.
        /// </para>
        /// </remarks>
        public Group this[string groupname] => _match._regex is null ?
            Group.s_emptyGroup :
            GetGroup(_match._regex.GroupNumberFromName(groupname));
 
        /// <summary>Provides an enumerator that iterates through the collection.</summary>
        /// <returns>
        /// An enumerator that contains all <see cref="Group" /> objects in the
        /// <see cref="GroupCollection" />.
        /// </returns>
        /// <remarks>
        /// Instead of calling the <see cref="GetEnumerator" /> method to retrieve an enumerator that lets
        /// you iterate through the <see cref="Group" /> objects in the collection, you should use the
        /// collection iteration construct provided by your programming language (such as <c>foreach</c>
        /// in C#).
        /// </remarks>
        public IEnumerator GetEnumerator() => new Enumerator(this);
 
        IEnumerator<Group> IEnumerable<Group>.GetEnumerator() => new Enumerator(this);
 
        private Group GetGroup(int groupnum)
        {
            if (_captureMap != null)
            {
                if (_captureMap.TryGetValue(groupnum, out int groupNumImpl))
                {
                    return GetGroupImpl(groupNumImpl);
                }
            }
            else if ((uint)groupnum < _match._matchcount.Length)
            {
                return GetGroupImpl(groupnum);
            }
 
            return Group.s_emptyGroup;
        }
 
        /// <summary>
        /// Caches the group objects
        /// </summary>
        private Group GetGroupImpl(int groupnum)
        {
            if (groupnum == 0)
            {
                return _match;
            }
 
            // Construct all the Group objects the first time GetGroup is called
            if (_groups is null)
            {
                _groups = new Group[_match._matchcount.Length - 1];
                for (int i = 0; i < _groups.Length; i++)
                {
                    string groupname = _match._regex!.GroupNameFromNumber(i + 1);
                    _groups[i] = new Group(_match.Text, _match._matches[i + 1], _match._matchcount[i + 1], groupname);
                }
            }
 
            return _groups[groupnum - 1];
        }
 
        /// <summary>
        /// Gets a value that indicates whether access to the <see cref="GroupCollection" /> is
        /// synchronized (thread-safe).
        /// </summary>
        /// <value><see langword="false" /> in all cases.</value>
        public bool IsSynchronized => false;
 
        /// <summary>
        /// Gets an object that can be used to synchronize access to the
        /// <see cref="GroupCollection" />.
        /// </summary>
        /// <value>
        /// A copy of the <see cref="Match" /> object to synchronize.
        /// </value>
        public object SyncRoot => _match;
 
        /// <summary>
        /// Copies all the elements of the collection to the given array beginning at the given index.
        /// </summary>
        /// <param name="array">The array the collection is to be copied into.</param>
        /// <param name="arrayIndex">The position in the destination array where copying is to begin.</param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="array" /> is <see langword="null" />.
        /// </exception>
        /// <exception cref="IndexOutOfRangeException">
        /// <paramref name="arrayIndex" /> is outside the bounds of <paramref name="array" />.
        /// -or-
        /// <paramref name="arrayIndex" /> plus <see cref="Count" /> is outside the bounds of
        /// <paramref name="array" />.
        /// </exception>
        public void CopyTo(Array array, int arrayIndex)
        {
            if (array is null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array);
            }
 
            for (int i = arrayIndex, j = 0; j < Count; i++, j++)
            {
                array.SetValue(this[j], i);
            }
        }
 
        /// <summary>
        /// Copies the elements of the group collection to a <see cref="Group" /> array, starting at a
        /// particular array index.
        /// </summary>
        /// <param name="array">
        /// The one-dimensional array that is the destination of the elements copied from the group
        /// collection. The array must have zero-based indexing.
        /// </param>
        /// <param name="arrayIndex">
        /// The zero-based index in <paramref name="array" /> at which copying begins.
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="array" /> is <see langword="null" />.
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="arrayIndex" /> is less than zero.
        /// -or-
        /// <paramref name="arrayIndex" /> is greater than the length of <paramref name="array" />.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The length of <paramref name="array" /> minus <paramref name="arrayIndex" /> is less than the
        /// group collection count.
        /// </exception>
        public void CopyTo(Group[] array, int arrayIndex)
        {
            if (array is null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array);
            }
 
            ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex);
            ArgumentOutOfRangeException.ThrowIfGreaterThan(arrayIndex, array.Length);
 
            if (array.Length - arrayIndex < Count)
            {
                throw new ArgumentException(SR.Arg_ArrayPlusOffTooSmall);
            }
 
            for (int i = arrayIndex, j = 0; j < Count; i++, j++)
            {
                array[i] = this[j];
            }
        }
 
        int IList<Group>.IndexOf(Group item)
        {
            for (int i = 0; i < Count; i++)
            {
                if (EqualityComparer<Group>.Default.Equals(this[i], item))
                {
                    return i;
                }
            }
 
            return -1;
        }
 
        void IList<Group>.Insert(int index, Group item) =>
            throw new NotSupportedException(SR.NotSupported_ReadOnlyCollection);
 
        void IList<Group>.RemoveAt(int index) =>
            throw new NotSupportedException(SR.NotSupported_ReadOnlyCollection);
 
        Group IList<Group>.this[int index]
        {
            get => this[index];
            set => throw new NotSupportedException(SR.NotSupported_ReadOnlyCollection);
        }
 
        void ICollection<Group>.Add(Group item) =>
            throw new NotSupportedException(SR.NotSupported_ReadOnlyCollection);
 
        void ICollection<Group>.Clear() =>
            throw new NotSupportedException(SR.NotSupported_ReadOnlyCollection);
 
        bool ICollection<Group>.Contains(Group item) =>
            ((IList<Group>)this).IndexOf(item) >= 0;
 
        bool ICollection<Group>.Remove(Group item) =>
            throw new NotSupportedException(SR.NotSupported_ReadOnlyCollection);
 
        int IList.Add(object? value) =>
            throw new NotSupportedException(SR.NotSupported_ReadOnlyCollection);
 
        void IList.Clear() =>
            throw new NotSupportedException(SR.NotSupported_ReadOnlyCollection);
 
        bool IList.Contains(object? value) =>
            value is Group other && ((ICollection<Group>)this).Contains(other);
 
        int IList.IndexOf(object? value) =>
            value is Group other ? ((IList<Group>)this).IndexOf(other) : -1;
 
        void IList.Insert(int index, object? value) =>
            throw new NotSupportedException(SR.NotSupported_ReadOnlyCollection);
 
        bool IList.IsFixedSize => true;
 
        void IList.Remove(object? value) =>
            throw new NotSupportedException(SR.NotSupported_ReadOnlyCollection);
 
        void IList.RemoveAt(int index) =>
            throw new NotSupportedException(SR.NotSupported_ReadOnlyCollection);
 
        object? IList.this[int index]
        {
            get => this[index];
            set => throw new NotSupportedException(SR.NotSupported_ReadOnlyCollection);
        }
 
        IEnumerator<KeyValuePair<string, Group>> IEnumerable<KeyValuePair<string, Group>>.GetEnumerator() =>
            new Enumerator(this);
 
        /// <summary>
        /// Attempts to retrieve a group identified by the provided name key, if it exists in the group
        /// collection.
        /// </summary>
        /// <param name="key">A string with the group name key to look for.</param>
        /// <param name="value">
        /// When the method returns, the group whose name is <paramref name="key" />, if it is found;
        /// otherwise, <see langword="null" /> if not found.
        /// </param>
        /// <returns>
        /// <see langword="true" /> if a group identified by the provided name key exists;
        /// <see langword="false" /> otherwise.
        /// </returns>
        public bool TryGetValue(string key, [NotNullWhen(true)] out Group? value)
        {
            Group group = this[key];
            if (group == Group.s_emptyGroup)
            {
                value = null;
                return false;
            }
 
            value = group;
            return true;
        }
 
        /// <summary>
        /// Determines whether the group collection contains a captured group identified by the specified
        /// name.
        /// </summary>
        /// <param name="key">A string with the name of the captured group to locate.</param>
        /// <returns>
        /// <see langword="true" /> if the group collection contains a captured group identified by
        /// <paramref name="key" />; <see langword="false" /> otherwise.
        /// </returns>
        public bool ContainsKey(string key) => _match._regex!.GroupNumberFromName(key) >= 0;
 
        /// <summary>Gets a string enumeration that contains the name keys of the group collection.</summary>
        /// <value>The name keys of the group collection.</value>
        public IEnumerable<string> Keys
        {
            get
            {
                for (int i = 0; i < Count; ++i)
                {
                    yield return GetGroup(i).Name;
                }
            }
        }
 
        /// <summary>Gets a group enumeration with all the groups in the group collection.</summary>
        /// <value>A group enumeration.</value>
        public IEnumerable<Group> Values
        {
            get
            {
                for (int i = 0; i < Count; ++i)
                {
                    yield return GetGroup(i);
                }
            }
        }
 
        private sealed class Enumerator : IEnumerator<Group>, IEnumerator<KeyValuePair<string, Group>>
        {
            private readonly GroupCollection _collection;
            private int _index;
 
            internal Enumerator(GroupCollection collection)
            {
                Debug.Assert(collection != null, "collection cannot be null.");
 
                _collection = collection;
                _index = -1;
            }
 
            public bool MoveNext()
            {
                int size = _collection.Count;
 
                if (_index >= size)
                {
                    return false;
                }
 
                _index++;
                return _index < size;
            }
 
            public Group Current
            {
                get
                {
                    if (_index < 0 || _index >= _collection.Count)
                    {
                        throw new InvalidOperationException(SR.EnumNotStarted);
                    }
 
                    return _collection[_index];
                }
            }
 
            KeyValuePair<string, Group> IEnumerator<KeyValuePair<string, Group>>.Current
            {
                get
                {
                    if ((uint)_index >= _collection.Count)
                    {
                        throw new InvalidOperationException(SR.EnumNotStarted);
                    }
 
                    Group value = _collection[_index];
                    return new KeyValuePair<string, Group>(value.Name, value);
 
                }
            }
 
            object IEnumerator.Current => Current;
 
            void IEnumerator.Reset() => _index = -1;
 
            void IDisposable.Dispose() { }
        }
    }
}