diff --git a/Nuclex.Support (net-4.0).csproj b/Nuclex.Support (net-4.0).csproj index 85e2d5b..9b94dc3 100644 --- a/Nuclex.Support (net-4.0).csproj +++ b/Nuclex.Support (net-4.0).csproj @@ -348,11 +348,11 @@ - \ No newline at end of file diff --git a/Nuclex.Support (net-4.6).csproj b/Nuclex.Support (net-4.6).csproj index 7dac898..06b3d2c 100644 --- a/Nuclex.Support (net-4.6).csproj +++ b/Nuclex.Support (net-4.6).csproj @@ -351,11 +351,11 @@ - \ No newline at end of file diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 204363e..4b45d5d 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -1,37 +1,56 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Nuclex.Support")] -[assembly: AssemblyProduct("Nuclex.Support")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyCompany("Nuclex Development Labs")] -[assembly: AssemblyCopyright("Copyright © Nuclex Development Labs 2008-2017")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("1308e4c3-a0c1-423a-aaae-61c7314777e0")] - -#if UNITTEST -// This is required to NMock can derive its proxies from interfaces in -// the internal unit test classes -[assembly: InternalsVisibleTo(NMock.Constants.InternalsVisibleToDynamicProxy)] -#endif - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -[assembly: AssemblyVersion("1.0.0.0")] +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Nuclex.Support")] +[assembly: AssemblyProduct("Nuclex.Support")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyCompany("Nuclex Development Labs")] +[assembly: AssemblyCopyright("Copyright © Markus Ewald / Nuclex Development Labs 2002-2024")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("1308e4c3-a0c1-423a-aaae-61c7314777e0")] + +#if UNITTEST +// This is required to NMock can derive its proxies from interfaces in +// the internal unit test classes +[assembly: InternalsVisibleTo(NMock.Constants.InternalsVisibleToDynamicProxy)] +#endif + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +[assembly: AssemblyVersion("1.0.0.0")] diff --git a/Source/Async/AsyncStatus.cs b/Source/Async/AsyncStatus.cs old mode 100755 new mode 100644 index 3b53c08..8cb0d59 --- a/Source/Async/AsyncStatus.cs +++ b/Source/Async/AsyncStatus.cs @@ -1,22 +1,21 @@ -#region CPL License +#region Apache License 2.0 /* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. + http://www.apache.org/licenses/LICENSE-2.0 -You should have received a copy of the IBM Common Public -License along with this library +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ -#endregion +#endregion // Apache License 2.0 using System; diff --git a/Source/Async/AsyncStatusEventArgs.cs b/Source/Async/AsyncStatusEventArgs.cs old mode 100755 new mode 100644 index 3ef0319..106746f --- a/Source/Async/AsyncStatusEventArgs.cs +++ b/Source/Async/AsyncStatusEventArgs.cs @@ -1,22 +1,21 @@ -#region CPL License +#region Apache License 2.0 /* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. + http://www.apache.org/licenses/LICENSE-2.0 -You should have received a copy of the IBM Common Public -License along with this library +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ -#endregion +#endregion // Apache License 2.0 using System; diff --git a/Source/Async/IAsyncAction.cs b/Source/Async/IAsyncAction.cs old mode 100755 new mode 100644 index 7e47f71..b952062 --- a/Source/Async/IAsyncAction.cs +++ b/Source/Async/IAsyncAction.cs @@ -1,22 +1,21 @@ -#region CPL License +#region Apache License 2.0 /* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. + http://www.apache.org/licenses/LICENSE-2.0 -You should have received a copy of the IBM Common Public -License along with this library +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ -#endregion +#endregion // Apache License 2.0 using System; using System.Threading; diff --git a/Source/Async/IAsyncSwitch.cs b/Source/Async/IAsyncSwitch.cs old mode 100755 new mode 100644 index 80f74b5..bc7935a --- a/Source/Async/IAsyncSwitch.cs +++ b/Source/Async/IAsyncSwitch.cs @@ -1,22 +1,21 @@ -#region CPL License +#region Apache License 2.0 /* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. + http://www.apache.org/licenses/LICENSE-2.0 -You should have received a copy of the IBM Common Public -License along with this library +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ -#endregion +#endregion // Apache License 2.0 using System; using System.Threading; diff --git a/Source/Async/IAsyncTask.cs b/Source/Async/IAsyncTask.cs old mode 100755 new mode 100644 index 1cb7ed4..f77459f --- a/Source/Async/IAsyncTask.cs +++ b/Source/Async/IAsyncTask.cs @@ -1,22 +1,21 @@ -#region CPL License +#region Apache License 2.0 /* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. + http://www.apache.org/licenses/LICENSE-2.0 -You should have received a copy of the IBM Common Public -License along with this library +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ -#endregion +#endregion // Apache License 2.0 using System; diff --git a/Source/Async/ICancellable.cs b/Source/Async/ICancellable.cs old mode 100755 new mode 100644 index 49292c2..5aca48d --- a/Source/Async/ICancellable.cs +++ b/Source/Async/ICancellable.cs @@ -1,22 +1,21 @@ -#region CPL License +#region Apache License 2.0 /* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. + http://www.apache.org/licenses/LICENSE-2.0 -You should have received a copy of the IBM Common Public -License along with this library +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ -#endregion +#endregion // Apache License 2.0 using System; diff --git a/Source/Async/IProgressSource.cs b/Source/Async/IProgressSource.cs old mode 100755 new mode 100644 index 8cc1bdf..947ce1a --- a/Source/Async/IProgressSource.cs +++ b/Source/Async/IProgressSource.cs @@ -1,22 +1,21 @@ -#region CPL License +#region Apache License 2.0 /* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. + http://www.apache.org/licenses/LICENSE-2.0 -You should have received a copy of the IBM Common Public -License along with this library +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ -#endregion +#endregion // Apache License 2.0 using System; diff --git a/Source/Cloning/CloneFactoryTest.cs b/Source/Cloning/CloneFactoryTest.cs index afe222c..3a4f1f4 100644 --- a/Source/Cloning/CloneFactoryTest.cs +++ b/Source/Cloning/CloneFactoryTest.cs @@ -1,597 +1,596 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; - -using NUnit.Framework; - -namespace Nuclex.Support.Cloning { - - /// Base class for unit tests verifying the clone factory - internal abstract class CloneFactoryTest { - - #region class DerivedReferenceType - - /// A derived reference type being used for testing - protected class DerivedReferenceType : TestReferenceType { - - /// Field holding an integer value for testing - public int DerivedField; - /// Property holding an integer value for testing - public int DerivedProperty { get; set; } - - } - - #endregion // class DerivedReferenceType - - #region class TestReferenceType - - /// A reference type being used for testing - protected class TestReferenceType { - - /// Field holding an integer value for testing - public int TestField; - /// Property holding an integer value for testing - public int TestProperty { get; set; } - - } - - #endregion // class TestReferenceType - - #region struct TestValueType - - /// A value type being used for testing - protected struct TestValueType { - - /// Field holding an integer value for testing - public int TestField; - /// Property holding an integer value for testing - public int TestProperty { get; set; } - - } - - #endregion // struct TestValueType - - #region struct HierarchicalValueType - - /// A value type containiner other complex types used for testing - protected struct HierarchicalValueType { - - /// Field holding an integer value for testing - public int TestField; - /// Property holding an integer value for testing - public int TestProperty { get; set; } - /// Value type field for testing - public TestValueType ValueTypeField; - /// Value type property for testing - public TestValueType ValueTypeProperty { get; set; } - /// Reference type field for testing - public TestReferenceType ReferenceTypeField; - /// Reference type property for testing - public TestReferenceType ReferenceTypeProperty { get; set; } - /// An array field of reference types - public TestReferenceType[,][] ReferenceTypeArrayField; - /// An array property of reference types - public TestReferenceType[,][] ReferenceTypeArrayProperty { get; set; } - /// A reference type field that's always null - public TestReferenceType AlwaysNullField; - /// A reference type property that's always null - public TestReferenceType AlwaysNullProperty { get; set; } - /// A property that only has a getter - public TestReferenceType GetOnlyProperty { get { return null; } } - /// A property that only has a setter - public TestReferenceType SetOnlyProperty { set { } } - /// A read-only field - public readonly TestValueType ReadOnlyField; - /// Field typed as base class holding a derived instance - public TestReferenceType DerivedField; - /// Field typed as base class holding a derived instance - public TestReferenceType DerivedProperty { get; set; } - - } - - #endregion // struct HierarchicalValueType - - #region struct HierarchicalReferenceType - - /// A value type containiner other complex types used for testing - protected class HierarchicalReferenceType { - - /// Field holding an integer value for testing - public int TestField; - /// Property holding an integer value for testing - public int TestProperty { get; set; } - /// Value type field for testing - public TestValueType ValueTypeField; - /// Value type property for testing - public TestValueType ValueTypeProperty { get; set; } - /// Reference type field for testing - public TestReferenceType ReferenceTypeField; - /// Reference type property for testing - public TestReferenceType ReferenceTypeProperty { get; set; } - /// An array field of reference types - public TestReferenceType[,][] ReferenceTypeArrayField; - /// An array property of reference types - public TestReferenceType[,][] ReferenceTypeArrayProperty { get; set; } - /// A reference type field that's always null - public TestReferenceType AlwaysNullField; - /// A reference type property that's always null - public TestReferenceType AlwaysNullProperty { get; set; } - /// A property that only has a getter - public TestReferenceType GetOnlyProperty { get { return null; } } - /// A property that only has a setter - public TestReferenceType SetOnlyProperty { set { } } - /// A read-only field - public readonly TestValueType ReadOnlyField; - /// Field typed as base class holding a derived instance - public TestReferenceType DerivedField; - /// Field typed as base class holding a derived instance - public TestReferenceType DerivedProperty { get; set; } - - } - - #endregion // struct HierarchicalReferenceType - - #region class ClassWithoutDefaultConstructor - - /// A class that does not have a default constructor - public class ClassWithoutDefaultConstructor { - - /// - /// Initializes a new instance of the class without default constructor - /// - /// Dummy value that will be saved by the instance - public ClassWithoutDefaultConstructor(int dummy) { - this.dummy = dummy; - } - - /// Dummy value that has been saved by the instance - public int Dummy { - get { return this.dummy; } - } - - /// Dummy value that has been saved by the instance - private int dummy; - - } - - #endregion // class ClassWithoutDefaultConstructor - - /// - /// Verifies that a cloned object exhibits the expected state for the type of - /// clone that has been performed - /// - /// Original instance the clone was created from - /// Cloned instance that will be checked for correctness - /// Whether the cloned instance is a deep clone - /// - /// Whether a property-based clone was performed - /// - protected static void VerifyClone( - HierarchicalReferenceType original, HierarchicalReferenceType clone, - bool isDeepClone, bool isPropertyBasedClone - ) { - Assert.AreNotSame(original, clone); - - if(isPropertyBasedClone) { - Assert.AreEqual(0, clone.TestField); - Assert.AreEqual(0, clone.ValueTypeField.TestField); - Assert.AreEqual(0, clone.ValueTypeField.TestProperty); - Assert.AreEqual(0, clone.ValueTypeProperty.TestField); - Assert.IsNull(clone.ReferenceTypeField); - Assert.IsNull(clone.DerivedField); - - if(isDeepClone) { - Assert.AreNotSame(original.ReferenceTypeProperty, clone.ReferenceTypeProperty); - Assert.AreNotSame( - original.ReferenceTypeArrayProperty, clone.ReferenceTypeArrayProperty - ); - Assert.AreNotSame(original.DerivedProperty, clone.DerivedProperty); - Assert.IsInstanceOf(clone.DerivedProperty); - - var originalDerived = (DerivedReferenceType)original.DerivedProperty; - var clonedDerived = (DerivedReferenceType)clone.DerivedProperty; - Assert.AreEqual(originalDerived.TestProperty, clonedDerived.TestProperty); - Assert.AreEqual(originalDerived.DerivedProperty, clonedDerived.DerivedProperty); - - Assert.AreEqual(0, clone.ReferenceTypeProperty.TestField); - Assert.AreNotSame( - original.ReferenceTypeArrayProperty[1, 3][0], - clone.ReferenceTypeArrayProperty[1, 3][0] - ); - Assert.AreNotSame( - original.ReferenceTypeArrayProperty[1, 3][2], - clone.ReferenceTypeArrayProperty[1, 3][2] - ); - Assert.AreEqual(0, clone.ReferenceTypeArrayProperty[1, 3][0].TestField); - Assert.AreEqual(0, clone.ReferenceTypeArrayProperty[1, 3][2].TestField); - } else { - Assert.AreSame(original.ReferenceTypeProperty, clone.ReferenceTypeProperty); - Assert.AreSame(original.ReferenceTypeProperty, clone.ReferenceTypeProperty); - Assert.AreSame( - original.ReferenceTypeArrayProperty, clone.ReferenceTypeArrayProperty - ); - } - } else { - Assert.AreEqual(original.TestField, clone.TestField); - Assert.AreEqual(original.ValueTypeField.TestField, clone.ValueTypeField.TestField); - Assert.AreEqual(original.ValueTypeField.TestProperty, clone.ValueTypeField.TestProperty); - Assert.AreEqual( - original.ValueTypeProperty.TestField, clone.ValueTypeProperty.TestField - ); - Assert.AreEqual( - original.ReferenceTypeField.TestField, clone.ReferenceTypeField.TestField - ); - Assert.AreEqual( - original.ReferenceTypeField.TestProperty, clone.ReferenceTypeField.TestProperty - ); - Assert.AreEqual( - original.ReferenceTypeProperty.TestField, clone.ReferenceTypeProperty.TestField - ); - - if(isDeepClone) { - Assert.AreNotSame(original.ReferenceTypeField, clone.ReferenceTypeField); - Assert.AreNotSame(original.ReferenceTypeProperty, clone.ReferenceTypeProperty); - Assert.AreNotSame(original.DerivedField, clone.DerivedField); - Assert.AreNotSame(original.DerivedProperty, clone.DerivedProperty); - Assert.IsInstanceOf(clone.DerivedField); - Assert.IsInstanceOf(clone.DerivedProperty); - - var originalDerived = (DerivedReferenceType)original.DerivedField; - var clonedDerived = (DerivedReferenceType)clone.DerivedField; - Assert.AreEqual(originalDerived.TestField, clonedDerived.TestField); - Assert.AreEqual(originalDerived.TestProperty, clonedDerived.TestProperty); - Assert.AreEqual(originalDerived.DerivedField, clonedDerived.DerivedField); - Assert.AreEqual(originalDerived.DerivedProperty, clonedDerived.DerivedProperty); - - originalDerived = (DerivedReferenceType)original.DerivedProperty; - clonedDerived = (DerivedReferenceType)clone.DerivedProperty; - Assert.AreEqual(originalDerived.TestField, clonedDerived.TestField); - Assert.AreEqual(originalDerived.TestProperty, clonedDerived.TestProperty); - Assert.AreEqual(originalDerived.DerivedField, clonedDerived.DerivedField); - Assert.AreEqual(originalDerived.DerivedProperty, clonedDerived.DerivedProperty); - - Assert.AreNotSame( - original.ReferenceTypeArrayField, clone.ReferenceTypeArrayField - ); - Assert.AreNotSame( - original.ReferenceTypeArrayProperty, clone.ReferenceTypeArrayProperty - ); - Assert.AreNotSame( - original.ReferenceTypeArrayProperty[1, 3][0], - clone.ReferenceTypeArrayProperty[1, 3][0] - ); - Assert.AreNotSame( - original.ReferenceTypeArrayProperty[1, 3][2], - clone.ReferenceTypeArrayProperty[1, 3][2] - ); - Assert.AreEqual( - original.ReferenceTypeArrayProperty[1, 3][0].TestField, - clone.ReferenceTypeArrayProperty[1, 3][0].TestField - ); - Assert.AreEqual( - original.ReferenceTypeArrayProperty[1, 3][2].TestField, - clone.ReferenceTypeArrayProperty[1, 3][2].TestField - ); - } else { - Assert.AreSame(original.DerivedField, clone.DerivedField); - Assert.AreSame(original.DerivedProperty, clone.DerivedProperty); - Assert.AreSame(original.ReferenceTypeField, clone.ReferenceTypeField); - Assert.AreSame(original.ReferenceTypeProperty, clone.ReferenceTypeProperty); - Assert.AreSame( - original.ReferenceTypeArrayField, clone.ReferenceTypeArrayField - ); - Assert.AreSame( - original.ReferenceTypeArrayProperty, clone.ReferenceTypeArrayProperty - ); - } - } - } - - /// - /// Verifies that a cloned object exhibits the expected state for the type of - /// clone that has been performed - /// - /// Original instance the clone was created from - /// Cloned instance that will be checked for correctness - /// Whether the cloned instance is a deep clone - /// - /// Whether a property-based clone was performed - /// - protected static void VerifyClone( - ref HierarchicalValueType original, ref HierarchicalValueType clone, - bool isDeepClone, bool isPropertyBasedClone - ) { - if(isPropertyBasedClone) { - Assert.AreEqual(0, clone.TestField); - Assert.AreEqual(0, clone.ValueTypeField.TestField); - Assert.AreEqual(0, clone.ValueTypeField.TestProperty); - Assert.AreEqual(0, clone.ValueTypeProperty.TestField); - Assert.IsNull(clone.ReferenceTypeField); - Assert.IsNull(clone.DerivedField); - - if(isDeepClone) { - Assert.AreNotSame(original.ReferenceTypeProperty, clone.ReferenceTypeProperty); - Assert.AreNotSame( - original.ReferenceTypeArrayProperty, clone.ReferenceTypeArrayProperty - ); - Assert.AreNotSame(original.DerivedProperty, clone.DerivedProperty); - Assert.IsInstanceOf(clone.DerivedProperty); - - var originalDerived = (DerivedReferenceType)original.DerivedProperty; - var clonedDerived = (DerivedReferenceType)clone.DerivedProperty; - Assert.AreEqual(originalDerived.TestProperty, clonedDerived.TestProperty); - Assert.AreEqual(originalDerived.DerivedProperty, clonedDerived.DerivedProperty); - - Assert.AreEqual(0, clone.ReferenceTypeProperty.TestField); - Assert.AreNotSame( - original.ReferenceTypeArrayProperty[1, 3][0], - clone.ReferenceTypeArrayProperty[1, 3][0] - ); - Assert.AreNotSame( - original.ReferenceTypeArrayProperty[1, 3][2], - clone.ReferenceTypeArrayProperty[1, 3][2] - ); - Assert.AreEqual(0, clone.ReferenceTypeArrayProperty[1, 3][0].TestField); - Assert.AreEqual(0, clone.ReferenceTypeArrayProperty[1, 3][2].TestField); - } else { - Assert.AreSame(original.ReferenceTypeProperty, clone.ReferenceTypeProperty); - Assert.AreSame(original.ReferenceTypeProperty, clone.ReferenceTypeProperty); - Assert.AreSame( - original.ReferenceTypeArrayProperty, clone.ReferenceTypeArrayProperty - ); - } - } else { - Assert.AreEqual(original.TestField, clone.TestField); - Assert.AreEqual(original.ValueTypeField.TestField, clone.ValueTypeField.TestField); - Assert.AreEqual(original.ValueTypeField.TestProperty, clone.ValueTypeField.TestProperty); - Assert.AreEqual( - original.ValueTypeProperty.TestField, clone.ValueTypeProperty.TestField - ); - Assert.AreEqual( - original.ReferenceTypeField.TestField, clone.ReferenceTypeField.TestField - ); - Assert.AreEqual( - original.ReferenceTypeField.TestProperty, clone.ReferenceTypeField.TestProperty - ); - Assert.AreEqual( - original.ReferenceTypeProperty.TestField, clone.ReferenceTypeProperty.TestField - ); - - if(isDeepClone) { - Assert.AreNotSame(original.ReferenceTypeField, clone.ReferenceTypeField); - Assert.AreNotSame(original.ReferenceTypeProperty, clone.ReferenceTypeProperty); - Assert.AreNotSame(original.DerivedField, clone.DerivedField); - Assert.AreNotSame(original.DerivedProperty, clone.DerivedProperty); - Assert.IsInstanceOf(clone.DerivedField); - Assert.IsInstanceOf(clone.DerivedProperty); - - var originalDerived = (DerivedReferenceType)original.DerivedField; - var clonedDerived = (DerivedReferenceType)clone.DerivedField; - Assert.AreEqual(originalDerived.TestField, clonedDerived.TestField); - Assert.AreEqual(originalDerived.TestProperty, clonedDerived.TestProperty); - Assert.AreEqual(originalDerived.DerivedField, clonedDerived.DerivedField); - Assert.AreEqual(originalDerived.DerivedProperty, clonedDerived.DerivedProperty); - - originalDerived = (DerivedReferenceType)original.DerivedProperty; - clonedDerived = (DerivedReferenceType)clone.DerivedProperty; - Assert.AreEqual(originalDerived.TestField, clonedDerived.TestField); - Assert.AreEqual(originalDerived.TestProperty, clonedDerived.TestProperty); - Assert.AreEqual(originalDerived.DerivedField, clonedDerived.DerivedField); - Assert.AreEqual(originalDerived.DerivedProperty, clonedDerived.DerivedProperty); - - Assert.AreNotSame( - original.ReferenceTypeArrayField, clone.ReferenceTypeArrayField - ); - Assert.AreNotSame( - original.ReferenceTypeArrayProperty, clone.ReferenceTypeArrayProperty - ); - Assert.AreNotSame( - original.ReferenceTypeArrayProperty[1, 3][0], - clone.ReferenceTypeArrayProperty[1, 3][0] - ); - Assert.AreNotSame( - original.ReferenceTypeArrayProperty[1, 3][2], - clone.ReferenceTypeArrayProperty[1, 3][2] - ); - Assert.AreEqual( - original.ReferenceTypeArrayProperty[1, 3][0].TestField, - clone.ReferenceTypeArrayProperty[1, 3][0].TestField - ); - Assert.AreEqual( - original.ReferenceTypeArrayProperty[1, 3][2].TestField, - clone.ReferenceTypeArrayProperty[1, 3][2].TestField - ); - } else { - Assert.AreSame(original.DerivedField, clone.DerivedField); - Assert.AreSame(original.DerivedProperty, clone.DerivedProperty); - Assert.AreSame(original.ReferenceTypeField, clone.ReferenceTypeField); - Assert.AreSame(original.ReferenceTypeProperty, clone.ReferenceTypeProperty); - Assert.AreSame( - original.ReferenceTypeArrayField, clone.ReferenceTypeArrayField - ); - Assert.AreSame( - original.ReferenceTypeArrayProperty, clone.ReferenceTypeArrayProperty - ); - } - } - - Assert.AreEqual(original.TestProperty, clone.TestProperty); - Assert.AreEqual( - original.ValueTypeProperty.TestProperty, clone.ValueTypeProperty.TestProperty - ); - Assert.AreEqual( - original.ReferenceTypeProperty.TestProperty, clone.ReferenceTypeProperty.TestProperty - ); - Assert.AreEqual( - original.ReferenceTypeArrayProperty[1, 3][0].TestProperty, - clone.ReferenceTypeArrayProperty[1, 3][0].TestProperty - ); - Assert.AreEqual( - original.ReferenceTypeArrayProperty[1, 3][2].TestProperty, - clone.ReferenceTypeArrayProperty[1, 3][2].TestProperty - ); - } - - /// Creates a value type with random data for testing - /// A new value type with random data - protected static HierarchicalValueType CreateValueType() { - return new HierarchicalValueType() { - TestField = 123, - TestProperty = 321, - ReferenceTypeArrayField = new TestReferenceType[2, 4][] { - { - null, null, null, null - }, - { - null, null, null, - new TestReferenceType[3] { - new TestReferenceType() { TestField = 101, TestProperty = 202 }, - null, - new TestReferenceType() { TestField = 909, TestProperty = 808 } - } - }, - }, - ReferenceTypeArrayProperty = new TestReferenceType[2, 4][] { - { - null, null, null, null - }, - { - null, null, null, - new TestReferenceType[3] { - new TestReferenceType() { TestField = 303, TestProperty = 404 }, - null, - new TestReferenceType() { TestField = 707, TestProperty = 606 } - } - }, - }, - ValueTypeField = new TestValueType() { - TestField = 456, - TestProperty = 654 - }, - ValueTypeProperty = new TestValueType() { - TestField = 789, - TestProperty = 987, - }, - ReferenceTypeField = new TestReferenceType() { - TestField = 135, - TestProperty = 531 - }, - ReferenceTypeProperty = new TestReferenceType() { - TestField = 246, - TestProperty = 642, - }, - DerivedField = new DerivedReferenceType() { - DerivedField = 100, - DerivedProperty = 200, - TestField = 300, - TestProperty = 400 - }, - DerivedProperty = new DerivedReferenceType() { - DerivedField = 500, - DerivedProperty = 600, - TestField = 700, - TestProperty = 800 - } - }; - } - - /// Creates a reference type with random data for testing - /// A new reference type with random data - protected static HierarchicalReferenceType CreateReferenceType() { - return new HierarchicalReferenceType() { - TestField = 123, - TestProperty = 321, - ReferenceTypeArrayField = new TestReferenceType[2, 4][] { - { - null, null, null, null - }, - { - null, null, null, - new TestReferenceType[3] { - new TestReferenceType() { TestField = 101, TestProperty = 202 }, - null, - new TestReferenceType() { TestField = 909, TestProperty = 808 } - } - }, - }, - ReferenceTypeArrayProperty = new TestReferenceType[2, 4][] { - { - null, null, null, null - }, - { - null, null, null, - new TestReferenceType[3] { - new TestReferenceType() { TestField = 303, TestProperty = 404 }, - null, - new TestReferenceType() { TestField = 707, TestProperty = 606 } - } - }, - }, - ValueTypeField = new TestValueType() { - TestField = 456, - TestProperty = 654 - }, - ValueTypeProperty = new TestValueType() { - TestField = 789, - TestProperty = 987, - }, - ReferenceTypeField = new TestReferenceType() { - TestField = 135, - TestProperty = 531 - }, - ReferenceTypeProperty = new TestReferenceType() { - TestField = 246, - TestProperty = 642, - }, - DerivedField = new DerivedReferenceType() { - DerivedField = 100, - DerivedProperty = 200, - TestField = 300, - TestProperty = 400 - }, - DerivedProperty = new DerivedReferenceType() { - DerivedField = 500, - DerivedProperty = 600, - TestField = 700, - TestProperty = 800 - } - }; - } - - /// - /// Useless method that avoids a compile warnings due to unused fields - /// - protected void AvoidCompilerWarnings() { - var reference = new HierarchicalReferenceType() { - AlwaysNullField = new TestReferenceType() - }; - var value = new HierarchicalValueType() { - AlwaysNullField = new TestReferenceType() - }; - } - - } - -} // namespace Nuclex.Support.Cloning - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; + +using NUnit.Framework; + +namespace Nuclex.Support.Cloning { + + /// Base class for unit tests verifying the clone factory + internal abstract class CloneFactoryTest { + + #region class DerivedReferenceType + + /// A derived reference type being used for testing + protected class DerivedReferenceType : TestReferenceType { + + /// Field holding an integer value for testing + public int DerivedField; + /// Property holding an integer value for testing + public int DerivedProperty { get; set; } + + } + + #endregion // class DerivedReferenceType + + #region class TestReferenceType + + /// A reference type being used for testing + protected class TestReferenceType { + + /// Field holding an integer value for testing + public int TestField; + /// Property holding an integer value for testing + public int TestProperty { get; set; } + + } + + #endregion // class TestReferenceType + + #region struct TestValueType + + /// A value type being used for testing + protected struct TestValueType { + + /// Field holding an integer value for testing + public int TestField; + /// Property holding an integer value for testing + public int TestProperty { get; set; } + + } + + #endregion // struct TestValueType + + #region struct HierarchicalValueType + + /// A value type containiner other complex types used for testing + protected struct HierarchicalValueType { + + /// Field holding an integer value for testing + public int TestField; + /// Property holding an integer value for testing + public int TestProperty { get; set; } + /// Value type field for testing + public TestValueType ValueTypeField; + /// Value type property for testing + public TestValueType ValueTypeProperty { get; set; } + /// Reference type field for testing + public TestReferenceType ReferenceTypeField; + /// Reference type property for testing + public TestReferenceType ReferenceTypeProperty { get; set; } + /// An array field of reference types + public TestReferenceType[,][] ReferenceTypeArrayField; + /// An array property of reference types + public TestReferenceType[,][] ReferenceTypeArrayProperty { get; set; } + /// A reference type field that's always null + public TestReferenceType AlwaysNullField; + /// A reference type property that's always null + public TestReferenceType AlwaysNullProperty { get; set; } + /// A property that only has a getter + public TestReferenceType GetOnlyProperty { get { return null; } } + /// A property that only has a setter + public TestReferenceType SetOnlyProperty { set { } } + /// A read-only field + public readonly TestValueType ReadOnlyField; + /// Field typed as base class holding a derived instance + public TestReferenceType DerivedField; + /// Field typed as base class holding a derived instance + public TestReferenceType DerivedProperty { get; set; } + + } + + #endregion // struct HierarchicalValueType + + #region struct HierarchicalReferenceType + + /// A value type containiner other complex types used for testing + protected class HierarchicalReferenceType { + + /// Field holding an integer value for testing + public int TestField; + /// Property holding an integer value for testing + public int TestProperty { get; set; } + /// Value type field for testing + public TestValueType ValueTypeField; + /// Value type property for testing + public TestValueType ValueTypeProperty { get; set; } + /// Reference type field for testing + public TestReferenceType ReferenceTypeField; + /// Reference type property for testing + public TestReferenceType ReferenceTypeProperty { get; set; } + /// An array field of reference types + public TestReferenceType[,][] ReferenceTypeArrayField; + /// An array property of reference types + public TestReferenceType[,][] ReferenceTypeArrayProperty { get; set; } + /// A reference type field that's always null + public TestReferenceType AlwaysNullField; + /// A reference type property that's always null + public TestReferenceType AlwaysNullProperty { get; set; } + /// A property that only has a getter + public TestReferenceType GetOnlyProperty { get { return null; } } + /// A property that only has a setter + public TestReferenceType SetOnlyProperty { set { } } + /// A read-only field + public readonly TestValueType ReadOnlyField; + /// Field typed as base class holding a derived instance + public TestReferenceType DerivedField; + /// Field typed as base class holding a derived instance + public TestReferenceType DerivedProperty { get; set; } + + } + + #endregion // struct HierarchicalReferenceType + + #region class ClassWithoutDefaultConstructor + + /// A class that does not have a default constructor + public class ClassWithoutDefaultConstructor { + + /// + /// Initializes a new instance of the class without default constructor + /// + /// Dummy value that will be saved by the instance + public ClassWithoutDefaultConstructor(int dummy) { + this.dummy = dummy; + } + + /// Dummy value that has been saved by the instance + public int Dummy { + get { return this.dummy; } + } + + /// Dummy value that has been saved by the instance + private int dummy; + + } + + #endregion // class ClassWithoutDefaultConstructor + + /// + /// Verifies that a cloned object exhibits the expected state for the type of + /// clone that has been performed + /// + /// Original instance the clone was created from + /// Cloned instance that will be checked for correctness + /// Whether the cloned instance is a deep clone + /// + /// Whether a property-based clone was performed + /// + protected static void VerifyClone( + HierarchicalReferenceType original, HierarchicalReferenceType clone, + bool isDeepClone, bool isPropertyBasedClone + ) { + Assert.AreNotSame(original, clone); + + if(isPropertyBasedClone) { + Assert.AreEqual(0, clone.TestField); + Assert.AreEqual(0, clone.ValueTypeField.TestField); + Assert.AreEqual(0, clone.ValueTypeField.TestProperty); + Assert.AreEqual(0, clone.ValueTypeProperty.TestField); + Assert.IsNull(clone.ReferenceTypeField); + Assert.IsNull(clone.DerivedField); + + if(isDeepClone) { + Assert.AreNotSame(original.ReferenceTypeProperty, clone.ReferenceTypeProperty); + Assert.AreNotSame( + original.ReferenceTypeArrayProperty, clone.ReferenceTypeArrayProperty + ); + Assert.AreNotSame(original.DerivedProperty, clone.DerivedProperty); + Assert.IsInstanceOf(clone.DerivedProperty); + + var originalDerived = (DerivedReferenceType)original.DerivedProperty; + var clonedDerived = (DerivedReferenceType)clone.DerivedProperty; + Assert.AreEqual(originalDerived.TestProperty, clonedDerived.TestProperty); + Assert.AreEqual(originalDerived.DerivedProperty, clonedDerived.DerivedProperty); + + Assert.AreEqual(0, clone.ReferenceTypeProperty.TestField); + Assert.AreNotSame( + original.ReferenceTypeArrayProperty[1, 3][0], + clone.ReferenceTypeArrayProperty[1, 3][0] + ); + Assert.AreNotSame( + original.ReferenceTypeArrayProperty[1, 3][2], + clone.ReferenceTypeArrayProperty[1, 3][2] + ); + Assert.AreEqual(0, clone.ReferenceTypeArrayProperty[1, 3][0].TestField); + Assert.AreEqual(0, clone.ReferenceTypeArrayProperty[1, 3][2].TestField); + } else { + Assert.AreSame(original.ReferenceTypeProperty, clone.ReferenceTypeProperty); + Assert.AreSame(original.ReferenceTypeProperty, clone.ReferenceTypeProperty); + Assert.AreSame( + original.ReferenceTypeArrayProperty, clone.ReferenceTypeArrayProperty + ); + } + } else { + Assert.AreEqual(original.TestField, clone.TestField); + Assert.AreEqual(original.ValueTypeField.TestField, clone.ValueTypeField.TestField); + Assert.AreEqual(original.ValueTypeField.TestProperty, clone.ValueTypeField.TestProperty); + Assert.AreEqual( + original.ValueTypeProperty.TestField, clone.ValueTypeProperty.TestField + ); + Assert.AreEqual( + original.ReferenceTypeField.TestField, clone.ReferenceTypeField.TestField + ); + Assert.AreEqual( + original.ReferenceTypeField.TestProperty, clone.ReferenceTypeField.TestProperty + ); + Assert.AreEqual( + original.ReferenceTypeProperty.TestField, clone.ReferenceTypeProperty.TestField + ); + + if(isDeepClone) { + Assert.AreNotSame(original.ReferenceTypeField, clone.ReferenceTypeField); + Assert.AreNotSame(original.ReferenceTypeProperty, clone.ReferenceTypeProperty); + Assert.AreNotSame(original.DerivedField, clone.DerivedField); + Assert.AreNotSame(original.DerivedProperty, clone.DerivedProperty); + Assert.IsInstanceOf(clone.DerivedField); + Assert.IsInstanceOf(clone.DerivedProperty); + + var originalDerived = (DerivedReferenceType)original.DerivedField; + var clonedDerived = (DerivedReferenceType)clone.DerivedField; + Assert.AreEqual(originalDerived.TestField, clonedDerived.TestField); + Assert.AreEqual(originalDerived.TestProperty, clonedDerived.TestProperty); + Assert.AreEqual(originalDerived.DerivedField, clonedDerived.DerivedField); + Assert.AreEqual(originalDerived.DerivedProperty, clonedDerived.DerivedProperty); + + originalDerived = (DerivedReferenceType)original.DerivedProperty; + clonedDerived = (DerivedReferenceType)clone.DerivedProperty; + Assert.AreEqual(originalDerived.TestField, clonedDerived.TestField); + Assert.AreEqual(originalDerived.TestProperty, clonedDerived.TestProperty); + Assert.AreEqual(originalDerived.DerivedField, clonedDerived.DerivedField); + Assert.AreEqual(originalDerived.DerivedProperty, clonedDerived.DerivedProperty); + + Assert.AreNotSame( + original.ReferenceTypeArrayField, clone.ReferenceTypeArrayField + ); + Assert.AreNotSame( + original.ReferenceTypeArrayProperty, clone.ReferenceTypeArrayProperty + ); + Assert.AreNotSame( + original.ReferenceTypeArrayProperty[1, 3][0], + clone.ReferenceTypeArrayProperty[1, 3][0] + ); + Assert.AreNotSame( + original.ReferenceTypeArrayProperty[1, 3][2], + clone.ReferenceTypeArrayProperty[1, 3][2] + ); + Assert.AreEqual( + original.ReferenceTypeArrayProperty[1, 3][0].TestField, + clone.ReferenceTypeArrayProperty[1, 3][0].TestField + ); + Assert.AreEqual( + original.ReferenceTypeArrayProperty[1, 3][2].TestField, + clone.ReferenceTypeArrayProperty[1, 3][2].TestField + ); + } else { + Assert.AreSame(original.DerivedField, clone.DerivedField); + Assert.AreSame(original.DerivedProperty, clone.DerivedProperty); + Assert.AreSame(original.ReferenceTypeField, clone.ReferenceTypeField); + Assert.AreSame(original.ReferenceTypeProperty, clone.ReferenceTypeProperty); + Assert.AreSame( + original.ReferenceTypeArrayField, clone.ReferenceTypeArrayField + ); + Assert.AreSame( + original.ReferenceTypeArrayProperty, clone.ReferenceTypeArrayProperty + ); + } + } + } + + /// + /// Verifies that a cloned object exhibits the expected state for the type of + /// clone that has been performed + /// + /// Original instance the clone was created from + /// Cloned instance that will be checked for correctness + /// Whether the cloned instance is a deep clone + /// + /// Whether a property-based clone was performed + /// + protected static void VerifyClone( + ref HierarchicalValueType original, ref HierarchicalValueType clone, + bool isDeepClone, bool isPropertyBasedClone + ) { + if(isPropertyBasedClone) { + Assert.AreEqual(0, clone.TestField); + Assert.AreEqual(0, clone.ValueTypeField.TestField); + Assert.AreEqual(0, clone.ValueTypeField.TestProperty); + Assert.AreEqual(0, clone.ValueTypeProperty.TestField); + Assert.IsNull(clone.ReferenceTypeField); + Assert.IsNull(clone.DerivedField); + + if(isDeepClone) { + Assert.AreNotSame(original.ReferenceTypeProperty, clone.ReferenceTypeProperty); + Assert.AreNotSame( + original.ReferenceTypeArrayProperty, clone.ReferenceTypeArrayProperty + ); + Assert.AreNotSame(original.DerivedProperty, clone.DerivedProperty); + Assert.IsInstanceOf(clone.DerivedProperty); + + var originalDerived = (DerivedReferenceType)original.DerivedProperty; + var clonedDerived = (DerivedReferenceType)clone.DerivedProperty; + Assert.AreEqual(originalDerived.TestProperty, clonedDerived.TestProperty); + Assert.AreEqual(originalDerived.DerivedProperty, clonedDerived.DerivedProperty); + + Assert.AreEqual(0, clone.ReferenceTypeProperty.TestField); + Assert.AreNotSame( + original.ReferenceTypeArrayProperty[1, 3][0], + clone.ReferenceTypeArrayProperty[1, 3][0] + ); + Assert.AreNotSame( + original.ReferenceTypeArrayProperty[1, 3][2], + clone.ReferenceTypeArrayProperty[1, 3][2] + ); + Assert.AreEqual(0, clone.ReferenceTypeArrayProperty[1, 3][0].TestField); + Assert.AreEqual(0, clone.ReferenceTypeArrayProperty[1, 3][2].TestField); + } else { + Assert.AreSame(original.ReferenceTypeProperty, clone.ReferenceTypeProperty); + Assert.AreSame(original.ReferenceTypeProperty, clone.ReferenceTypeProperty); + Assert.AreSame( + original.ReferenceTypeArrayProperty, clone.ReferenceTypeArrayProperty + ); + } + } else { + Assert.AreEqual(original.TestField, clone.TestField); + Assert.AreEqual(original.ValueTypeField.TestField, clone.ValueTypeField.TestField); + Assert.AreEqual(original.ValueTypeField.TestProperty, clone.ValueTypeField.TestProperty); + Assert.AreEqual( + original.ValueTypeProperty.TestField, clone.ValueTypeProperty.TestField + ); + Assert.AreEqual( + original.ReferenceTypeField.TestField, clone.ReferenceTypeField.TestField + ); + Assert.AreEqual( + original.ReferenceTypeField.TestProperty, clone.ReferenceTypeField.TestProperty + ); + Assert.AreEqual( + original.ReferenceTypeProperty.TestField, clone.ReferenceTypeProperty.TestField + ); + + if(isDeepClone) { + Assert.AreNotSame(original.ReferenceTypeField, clone.ReferenceTypeField); + Assert.AreNotSame(original.ReferenceTypeProperty, clone.ReferenceTypeProperty); + Assert.AreNotSame(original.DerivedField, clone.DerivedField); + Assert.AreNotSame(original.DerivedProperty, clone.DerivedProperty); + Assert.IsInstanceOf(clone.DerivedField); + Assert.IsInstanceOf(clone.DerivedProperty); + + var originalDerived = (DerivedReferenceType)original.DerivedField; + var clonedDerived = (DerivedReferenceType)clone.DerivedField; + Assert.AreEqual(originalDerived.TestField, clonedDerived.TestField); + Assert.AreEqual(originalDerived.TestProperty, clonedDerived.TestProperty); + Assert.AreEqual(originalDerived.DerivedField, clonedDerived.DerivedField); + Assert.AreEqual(originalDerived.DerivedProperty, clonedDerived.DerivedProperty); + + originalDerived = (DerivedReferenceType)original.DerivedProperty; + clonedDerived = (DerivedReferenceType)clone.DerivedProperty; + Assert.AreEqual(originalDerived.TestField, clonedDerived.TestField); + Assert.AreEqual(originalDerived.TestProperty, clonedDerived.TestProperty); + Assert.AreEqual(originalDerived.DerivedField, clonedDerived.DerivedField); + Assert.AreEqual(originalDerived.DerivedProperty, clonedDerived.DerivedProperty); + + Assert.AreNotSame( + original.ReferenceTypeArrayField, clone.ReferenceTypeArrayField + ); + Assert.AreNotSame( + original.ReferenceTypeArrayProperty, clone.ReferenceTypeArrayProperty + ); + Assert.AreNotSame( + original.ReferenceTypeArrayProperty[1, 3][0], + clone.ReferenceTypeArrayProperty[1, 3][0] + ); + Assert.AreNotSame( + original.ReferenceTypeArrayProperty[1, 3][2], + clone.ReferenceTypeArrayProperty[1, 3][2] + ); + Assert.AreEqual( + original.ReferenceTypeArrayProperty[1, 3][0].TestField, + clone.ReferenceTypeArrayProperty[1, 3][0].TestField + ); + Assert.AreEqual( + original.ReferenceTypeArrayProperty[1, 3][2].TestField, + clone.ReferenceTypeArrayProperty[1, 3][2].TestField + ); + } else { + Assert.AreSame(original.DerivedField, clone.DerivedField); + Assert.AreSame(original.DerivedProperty, clone.DerivedProperty); + Assert.AreSame(original.ReferenceTypeField, clone.ReferenceTypeField); + Assert.AreSame(original.ReferenceTypeProperty, clone.ReferenceTypeProperty); + Assert.AreSame( + original.ReferenceTypeArrayField, clone.ReferenceTypeArrayField + ); + Assert.AreSame( + original.ReferenceTypeArrayProperty, clone.ReferenceTypeArrayProperty + ); + } + } + + Assert.AreEqual(original.TestProperty, clone.TestProperty); + Assert.AreEqual( + original.ValueTypeProperty.TestProperty, clone.ValueTypeProperty.TestProperty + ); + Assert.AreEqual( + original.ReferenceTypeProperty.TestProperty, clone.ReferenceTypeProperty.TestProperty + ); + Assert.AreEqual( + original.ReferenceTypeArrayProperty[1, 3][0].TestProperty, + clone.ReferenceTypeArrayProperty[1, 3][0].TestProperty + ); + Assert.AreEqual( + original.ReferenceTypeArrayProperty[1, 3][2].TestProperty, + clone.ReferenceTypeArrayProperty[1, 3][2].TestProperty + ); + } + + /// Creates a value type with random data for testing + /// A new value type with random data + protected static HierarchicalValueType CreateValueType() { + return new HierarchicalValueType() { + TestField = 123, + TestProperty = 321, + ReferenceTypeArrayField = new TestReferenceType[2, 4][] { + { + null, null, null, null + }, + { + null, null, null, + new TestReferenceType[3] { + new TestReferenceType() { TestField = 101, TestProperty = 202 }, + null, + new TestReferenceType() { TestField = 909, TestProperty = 808 } + } + }, + }, + ReferenceTypeArrayProperty = new TestReferenceType[2, 4][] { + { + null, null, null, null + }, + { + null, null, null, + new TestReferenceType[3] { + new TestReferenceType() { TestField = 303, TestProperty = 404 }, + null, + new TestReferenceType() { TestField = 707, TestProperty = 606 } + } + }, + }, + ValueTypeField = new TestValueType() { + TestField = 456, + TestProperty = 654 + }, + ValueTypeProperty = new TestValueType() { + TestField = 789, + TestProperty = 987, + }, + ReferenceTypeField = new TestReferenceType() { + TestField = 135, + TestProperty = 531 + }, + ReferenceTypeProperty = new TestReferenceType() { + TestField = 246, + TestProperty = 642, + }, + DerivedField = new DerivedReferenceType() { + DerivedField = 100, + DerivedProperty = 200, + TestField = 300, + TestProperty = 400 + }, + DerivedProperty = new DerivedReferenceType() { + DerivedField = 500, + DerivedProperty = 600, + TestField = 700, + TestProperty = 800 + } + }; + } + + /// Creates a reference type with random data for testing + /// A new reference type with random data + protected static HierarchicalReferenceType CreateReferenceType() { + return new HierarchicalReferenceType() { + TestField = 123, + TestProperty = 321, + ReferenceTypeArrayField = new TestReferenceType[2, 4][] { + { + null, null, null, null + }, + { + null, null, null, + new TestReferenceType[3] { + new TestReferenceType() { TestField = 101, TestProperty = 202 }, + null, + new TestReferenceType() { TestField = 909, TestProperty = 808 } + } + }, + }, + ReferenceTypeArrayProperty = new TestReferenceType[2, 4][] { + { + null, null, null, null + }, + { + null, null, null, + new TestReferenceType[3] { + new TestReferenceType() { TestField = 303, TestProperty = 404 }, + null, + new TestReferenceType() { TestField = 707, TestProperty = 606 } + } + }, + }, + ValueTypeField = new TestValueType() { + TestField = 456, + TestProperty = 654 + }, + ValueTypeProperty = new TestValueType() { + TestField = 789, + TestProperty = 987, + }, + ReferenceTypeField = new TestReferenceType() { + TestField = 135, + TestProperty = 531 + }, + ReferenceTypeProperty = new TestReferenceType() { + TestField = 246, + TestProperty = 642, + }, + DerivedField = new DerivedReferenceType() { + DerivedField = 100, + DerivedProperty = 200, + TestField = 300, + TestProperty = 400 + }, + DerivedProperty = new DerivedReferenceType() { + DerivedField = 500, + DerivedProperty = 600, + TestField = 700, + TestProperty = 800 + } + }; + } + + /// + /// Useless method that avoids a compile warnings due to unused fields + /// + protected void AvoidCompilerWarnings() { + var reference = new HierarchicalReferenceType() { + AlwaysNullField = new TestReferenceType() + }; + var value = new HierarchicalValueType() { + AlwaysNullField = new TestReferenceType() + }; + } + + } + +} // namespace Nuclex.Support.Cloning + +#endif // UNITTEST diff --git a/Source/Cloning/ExpressionTreeCloner.FieldBased.cs b/Source/Cloning/ExpressionTreeCloner.FieldBased.cs index 31a34eb..0b5405b 100644 --- a/Source/Cloning/ExpressionTreeCloner.FieldBased.cs +++ b/Source/Cloning/ExpressionTreeCloner.FieldBased.cs @@ -1,680 +1,679 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if !NO_SETS - -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Reflection; -using System.Runtime.Serialization; - -namespace Nuclex.Support.Cloning { - - partial class ExpressionTreeCloner : ICloneFactory { - - /// Compiles a method that creates a deep clone of an object - /// Type for which a clone method will be created - /// A method that clones an object of the provided type - /// - /// - /// The 'null' check is supposed to take place before running the cloner. This - /// avoids having redundant 'null' checks on nested types - first before calling - /// GetType() on the field to be cloned and second when runner the matching - /// cloner for the field. - /// - /// - /// This design also enables the cloning of nested value types (which can never - /// be null) without any null check whatsoever. - /// - /// - private static Func createDeepFieldBasedCloner(Type clonedType) { - ParameterExpression original = Expression.Parameter(typeof(object), "original"); - - var transferExpressions = new List(); - var variables = new List(); - - if(clonedType.IsPrimitive || (clonedType == typeof(string))) { - // Primitives and strings are copied on direct assignment - transferExpressions.Add(original); - } else if(clonedType.IsArray) { - // Arrays need to be cloned element-by-element - Type elementType = clonedType.GetElementType(); - - if(elementType.IsPrimitive || (elementType == typeof(string))) { - // For primitive arrays, the Array.Clone() method is sufficient - transferExpressions.Add( - generateFieldBasedPrimitiveArrayTransferExpressions( - clonedType, original, variables, transferExpressions - ) - ); - } else { - // To access the fields of the original type, we need it to be of the actual - // type instead of an object, so perform a downcast - ParameterExpression typedOriginal = Expression.Variable(clonedType); - variables.Add(typedOriginal); - transferExpressions.Add( - Expression.Assign(typedOriginal, Expression.Convert(original, clonedType)) - ); - - // Arrays of complex types require manual cloning - transferExpressions.Add( - generateFieldBasedComplexArrayTransferExpressions( - clonedType, typedOriginal, variables, transferExpressions - ) - ); - } - } else { - // We need a variable to hold the clone because due to the assignments it - // won't be last in the block when we're finished - ParameterExpression clone = Expression.Variable(clonedType); - variables.Add(clone); - - // Give it a new instance of the type being cloned - MethodInfo getUninitializedObjectMethodInfo = typeof(FormatterServices).GetMethod( - "GetUninitializedObject", BindingFlags.Static | BindingFlags.Public - ); - transferExpressions.Add( - Expression.Assign( - clone, - Expression.Convert( - Expression.Call( - getUninitializedObjectMethodInfo, Expression.Constant(clonedType) - ), - clonedType - ) - ) - ); - - // To access the fields of the original type, we need it to be of the actual - // type instead of an object, so perform a downcast - ParameterExpression typedOriginal = Expression.Variable(clonedType); - variables.Add(typedOriginal); - transferExpressions.Add( - Expression.Assign(typedOriginal, Expression.Convert(original, clonedType)) - ); - - // Generate the expressions required to transfer the type field by field - generateFieldBasedComplexTypeTransferExpressions( - clonedType, typedOriginal, clone, variables, transferExpressions - ); - - // Make sure the clone is the last thing in the block to set the return value - transferExpressions.Add(clone); - } - - // Turn all transfer expressions into a single block if necessary - Expression resultExpression; - if((transferExpressions.Count == 1) && (variables.Count == 0)) { - resultExpression = transferExpressions[0]; - } else { - resultExpression = Expression.Block(variables, transferExpressions); - } - - // Value types require manual boxing - if(clonedType.IsValueType) { - resultExpression = Expression.Convert(resultExpression, typeof(object)); - } - - return Expression.Lambda>(resultExpression, original).Compile(); - } - - /// Compiles a method that creates a shallow clone of an object - /// Type for which a clone method will be created - /// A method that clones an object of the provided type - private static Func createShallowFieldBasedCloner(Type clonedType) { - ParameterExpression original = Expression.Parameter(typeof(object), "original"); - - var transferExpressions = new List(); - var variables = new List(); - - if(clonedType.IsPrimitive || clonedType.IsValueType || (clonedType == typeof(string))) { - // Primitives and strings are copied on direct assignment - transferExpressions.Add(original); - } else if(clonedType.IsArray) { - transferExpressions.Add( - generateFieldBasedPrimitiveArrayTransferExpressions( - clonedType, original, variables, transferExpressions - ) - ); - } else { - // We need a variable to hold the clone because due to the assignments it - // won't be last in the block when we're finished - ParameterExpression clone = Expression.Variable(clonedType); - variables.Add(clone); - - // To access the fields of the original type, we need it to be of the actual - // type instead of an object, so perform a downcast - ParameterExpression typedOriginal = Expression.Variable(clonedType); - variables.Add(typedOriginal); - transferExpressions.Add( - Expression.Assign(typedOriginal, Expression.Convert(original, clonedType)) - ); - - // Give it a new instance of the type being cloned - MethodInfo getUninitializedObjectMethodInfo = typeof(FormatterServices).GetMethod( - "GetUninitializedObject", BindingFlags.Static | BindingFlags.Public - ); - transferExpressions.Add( - Expression.Assign( - clone, - Expression.Convert( - Expression.Call( - getUninitializedObjectMethodInfo, Expression.Constant(clonedType) - ), - clonedType - ) - ) - ); - - // Enumerate all of the type's fields and generate transfer expressions for each - FieldInfo[] fieldInfos = clonedType.GetFieldInfosIncludingBaseClasses( - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance - ); - for(int index = 0; index < fieldInfos.Length; ++index) { - FieldInfo fieldInfo = fieldInfos[index]; - - if(fieldInfo.IsInitOnly) { - Expression source = Expression.Field(typedOriginal, fieldInfo); - if(fieldInfo.FieldType.IsValueType) { - source = Expression.Convert(source, typeof(object)); - } - - if(clone.Type.IsValueType) { - MethodInfo assignInitOnlyField = typeof(ExpressionTreeCloner).GetMethod( - "assignInitOnlyField", BindingFlags.Static | BindingFlags.NonPublic - ).MakeGenericMethod(clone.Type); - - transferExpressions.Add( - Expression.Call( - assignInitOnlyField, - clone, - Expression.Constant(fieldInfo), - source - ) - ); - } else { - MethodInfo setValueMethodInfo = typeof(FieldInfo).GetMethod( - "SetValue", new Type[] { typeof(object), typeof(object) } - ); - - transferExpressions.Add( - Expression.Call( - Expression.Constant(fieldInfo), - setValueMethodInfo, - clone, - source - ) - ); - } - } else { - transferExpressions.Add( - Expression.Assign( - Expression.Field(clone, fieldInfo), - Expression.Field(typedOriginal, fieldInfo) - ) - ); - } - } - - // Make sure the clone is the last thing in the block to set the return value - transferExpressions.Add(clone); - } - - // Turn all transfer expressions into a single block if necessary - Expression resultExpression; - if((transferExpressions.Count == 1) && (variables.Count == 0)) { - resultExpression = transferExpressions[0]; - } else { - resultExpression = Expression.Block(variables, transferExpressions); - } - - // Value types require manual boxing - if(clonedType.IsValueType) { - resultExpression = Expression.Convert(resultExpression, typeof(object)); - } - - return Expression.Lambda>(resultExpression, original).Compile(); - } - - /// - /// Generates state transfer expressions to copy an array of primitive types - /// - /// Type of array that will be cloned - /// Variable expression for the original array - /// Receives variables used by the transfer expressions - /// Receives the generated transfer expressions - /// The variable holding the cloned array - private static Expression generateFieldBasedPrimitiveArrayTransferExpressions( - Type clonedType, - Expression original, - ICollection variables, - ICollection transferExpressions - ) { - MethodInfo arrayCloneMethodInfo = typeof(Array).GetMethod("Clone"); - return Expression.Convert( - Expression.Call( - Expression.Convert(original, typeof(Array)), arrayCloneMethodInfo - ), - clonedType - ); - } - - /// - /// Generates state transfer expressions to copy an array of complex types - /// - /// Type of array that will be cloned - /// Variable expression for the original array - /// Receives variables used by the transfer expressions - /// Receives the generated transfer expressions - /// The variable holding the cloned array - private static ParameterExpression generateFieldBasedComplexArrayTransferExpressions( - Type clonedType, - Expression original, - IList variables, - ICollection transferExpressions - ) { - // We need a temporary variable in order to transfer the elements of the array - ParameterExpression clone = Expression.Variable(clonedType); - variables.Add(clone); - - int dimensionCount = clonedType.GetArrayRank(); - Type elementType = clonedType.GetElementType(); - - var lengths = new List(); - var indexes = new List(); - var labels = new List(); - - // Retrieve the length of each of the array's dimensions - MethodInfo arrayGetLengthMethodInfo = typeof(Array).GetMethod("GetLength"); - for(int index = 0; index < dimensionCount; ++index) { - - // Obtain the length of the array in the current dimension - lengths.Add(Expression.Variable(typeof(int))); - variables.Add(lengths[index]); - transferExpressions.Add( - Expression.Assign( - lengths[index], - Expression.Call( - original, arrayGetLengthMethodInfo, Expression.Constant(index) - ) - ) - ); - - // Set up a variable to index the array in this dimension - indexes.Add(Expression.Variable(typeof(int))); - variables.Add(indexes[index]); - - // Also set up a label than can be used to break out of the dimension's - // transfer loop - labels.Add(Expression.Label()); - - } - - // Create a new (empty) array with the same dimensions and lengths as the original - transferExpressions.Add( - Expression.Assign(clone, Expression.NewArrayBounds(elementType, lengths)) - ); - - // Initialize the indexer of the outer loop (indexers are initialized one up - // in the loops (ie. before the loop using it begins), so we have to set this - // one outside of the loop building code. - transferExpressions.Add( - Expression.Assign(indexes[0], Expression.Constant(0)) - ); - - // Build the nested loops (one for each dimension) from the inside out - Expression innerLoop = null; - for(int index = dimensionCount - 1; index >= 0; --index) { - var loopVariables = new List(); - var loopExpressions = new List(); - - // If we reached the end of the current array dimension, break the loop - loopExpressions.Add( - Expression.IfThen( - Expression.GreaterThanOrEqual(indexes[index], lengths[index]), - Expression.Break(labels[index]) - ) - ); - - if(innerLoop == null) { - // The innermost loop clones an actual array element - - if(elementType.IsPrimitive || (elementType == typeof(string))) { - // Primitive array elements can be copied by simple assignment. This case - // should not occur since Array.Clone() should be used instead. - loopExpressions.Add( - Expression.Assign( - Expression.ArrayAccess(clone, indexes), - Expression.ArrayAccess(original, indexes) - ) - ); - } else if(elementType.IsValueType) { - // Arrays of complex value types can be transferred by assigning all fields - // of the source array element to the destination array element (cloning - // any nested reference types appropriately) - generateFieldBasedComplexTypeTransferExpressions( - elementType, - Expression.ArrayAccess(original, indexes), - Expression.ArrayAccess(clone, indexes), - variables, - loopExpressions - ); - - } else { - // Arrays of reference types need to be cloned by creating a new instance - // of the reference type and then transferring the fields over - ParameterExpression originalElement = Expression.Variable(elementType); - loopVariables.Add(originalElement); - - loopExpressions.Add( - Expression.Assign(originalElement, Expression.ArrayAccess(original, indexes)) - ); - - var nestedVariables = new List(); - var nestedTransferExpressions = new List(); - - // A nested array should be cloned by directly creating a new array (not invoking - // a cloner) since you cannot derive from an array - if(elementType.IsArray) { - Expression clonedElement; - - Type nestedElementType = elementType.GetElementType(); - if(nestedElementType.IsPrimitive || (nestedElementType == typeof(string))) { - clonedElement = generateFieldBasedPrimitiveArrayTransferExpressions( - elementType, originalElement, nestedVariables, nestedTransferExpressions - ); - } else { - clonedElement = generateFieldBasedComplexArrayTransferExpressions( - elementType, originalElement, nestedVariables, nestedTransferExpressions - ); - } - nestedTransferExpressions.Add( - Expression.Assign(Expression.ArrayAccess(clone, indexes), clonedElement) - ); - } else { - // Complex types are cloned by checking their actual, concrete type (fields - // may be typed to an interface or base class) and requesting a cloner for that - // type during runtime - MethodInfo getOrCreateClonerMethodInfo = typeof(ExpressionTreeCloner).GetMethod( - "getOrCreateDeepFieldBasedCloner", - BindingFlags.NonPublic | BindingFlags.Static - ); - MethodInfo getTypeMethodInfo = typeof(object).GetMethod("GetType"); - MethodInfo invokeMethodInfo = typeof(Func).GetMethod("Invoke"); - - // Generate expressions to do this: - // clone.SomeField = getOrCreateDeepFieldBasedCloner( - // original.SomeField.GetType() - // ).Invoke(original.SomeField); - nestedTransferExpressions.Add( - Expression.Assign( - Expression.ArrayAccess(clone, indexes), - Expression.Convert( - Expression.Call( - Expression.Call( - getOrCreateClonerMethodInfo, - Expression.Call(originalElement, getTypeMethodInfo) - ), - invokeMethodInfo, - originalElement - ), - elementType - ) - ) - ); - } - - // Whether array-in-array of reference-type-in-array, we need a null check before - // doing anything to avoid NullReferenceExceptions for unset members - loopExpressions.Add( - Expression.IfThen( - Expression.NotEqual(originalElement, Expression.Constant(null)), - Expression.Block( - nestedVariables, - nestedTransferExpressions - ) - ) - ); - } - - } else { - // Outer loops of any level just reset the inner loop's indexer and execute - // the inner loop - loopExpressions.Add( - Expression.Assign(indexes[index + 1], Expression.Constant(0)) - ); - loopExpressions.Add(innerLoop); - } - - // Each time we executed the loop instructions, increment the indexer - loopExpressions.Add(Expression.PreIncrementAssign(indexes[index])); - - // Build the loop using the expressions recorded above - innerLoop = Expression.Loop( - Expression.Block(loopVariables, loopExpressions), - labels[index] - ); - } - - // After the loop builder has finished, the innerLoop variable contains - // the entire hierarchy of nested loops, so add this to the clone expressions. - transferExpressions.Add(innerLoop); - - return clone; - } - - /// Generates state transfer expressions to copy a complex type - /// Complex type that will be cloned - /// Variable expression for the original instance - /// Variable expression for the cloned instance - /// Receives variables used by the transfer expressions - /// Receives the generated transfer expressions - private static void generateFieldBasedComplexTypeTransferExpressions( - Type clonedType, // Actual, concrete type (not declared type) - Expression original, // Expected to be an object - Expression clone, // As actual, concrete type - IList variables, - ICollection transferExpressions - ) { - // Enumerate all of the type's fields and generate transfer expressions for each - FieldInfo[] fieldInfos = clonedType.GetFieldInfosIncludingBaseClasses( - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance - ); - for(int index = 0; index < fieldInfos.Length; ++index) { - FieldInfo fieldInfo = fieldInfos[index]; - Type fieldType = fieldInfo.FieldType; - - if(fieldType.IsPrimitive || (fieldType == typeof(string))) { - // Primitive types and strings can be transferred by simple assignment - transferExpressions.Add( - Expression.Assign( - Expression.Field(clone, fieldInfo), - Expression.Field(original, fieldInfo) - ) - ); - } else if(fieldType.IsValueType) { - // A nested value type is part of the parent and will have its fields directly - // assigned without boxing, new instance creation or anything like that. - generateFieldBasedComplexTypeTransferExpressions( - fieldType, - Expression.Field(original, fieldInfo), - Expression.Field(clone, fieldInfo), - variables, - transferExpressions - ); - } else { - generateFieldBasedReferenceTypeTransferExpressions( - original, clone, transferExpressions, fieldInfo, fieldType - ); - } - } - } - - /// - /// Generates the expressions to transfer a reference type (array or class) - /// - /// Original value that will be cloned - /// Variable that will receive the cloned value - /// - /// Receives the expression generated to transfer the values - /// - /// Reflection informations about the field being cloned - /// Type of the field being cloned - private static void generateFieldBasedReferenceTypeTransferExpressions( - Expression original, - Expression clone, - ICollection transferExpressions, - FieldInfo fieldInfo, - Type fieldType - ) { - // Reference types and arrays require special care because they can be null, - // so gather the transfer expressions in a separate block for the null check - var fieldTransferExpressions = new List(); - var fieldVariables = new List(); - - if(fieldType.IsArray) { - // Arrays need to be cloned element-by-element - Expression fieldClone; - - Type elementType = fieldType.GetElementType(); - if(elementType.IsPrimitive || (elementType == typeof(string))) { - // For primitive arrays, the Array.Clone() method is sufficient - fieldClone = generateFieldBasedPrimitiveArrayTransferExpressions( - fieldType, - Expression.Field(original, fieldInfo), - fieldVariables, - fieldTransferExpressions - ); - } else { - // Arrays of complex types require manual cloning - fieldClone = generateFieldBasedComplexArrayTransferExpressions( - fieldType, - Expression.Field(original, fieldInfo), - fieldVariables, - fieldTransferExpressions - ); - } - - // Add the assignment to the transfer expressions. The array transfer expression - // generator will either have set up a temporary variable to hold the array or - // returned the conversion expression straight away - fieldTransferExpressions.Add( - Expression.Assign(Expression.Field(clone, fieldInfo), fieldClone) - ); - } else { - // Complex types are cloned by checking their actual, concrete type (fields - // may be typed to an interface or base class) and requesting a cloner for that - // type during runtime - MethodInfo getOrCreateClonerMethodInfo = typeof(ExpressionTreeCloner).GetMethod( - "getOrCreateDeepFieldBasedCloner", - BindingFlags.NonPublic | BindingFlags.Static - ); - MethodInfo getTypeMethodInfo = typeof(object).GetMethod("GetType"); - MethodInfo invokeMethodInfo = typeof(Func).GetMethod("Invoke"); - - // Equivalent to - // (TField)getOrCreateDeepFieldBasedCloner( - // original.SomeField.GetType() - // ).Invoke(original.SomeField); - Expression result = Expression.Call( - Expression.Call( - getOrCreateClonerMethodInfo, - Expression.Call( - Expression.Field(original, fieldInfo), getTypeMethodInfo - ) - ), - invokeMethodInfo, - Expression.Field(original, fieldInfo) - ); - - // If the field is a readonly field, set the value via reflection because - // Expression Trees do not support assigning .initonly fields directly yet - if(fieldInfo.IsInitOnly) { - if(fieldInfo.FieldType.IsValueType) { - result = Expression.Convert(result, typeof(object)); - } - - if(clone.Type.IsValueType) { - MethodInfo assignInitOnlyField = typeof(ExpressionTreeCloner).GetMethod( - "assignInitOnlyField", BindingFlags.Static | BindingFlags.NonPublic - ).MakeGenericMethod(clone.Type); - fieldTransferExpressions.Add( - Expression.Call( - assignInitOnlyField, - clone, - Expression.Constant(fieldInfo), - result - ) - ); - } else { - MethodInfo setValueMethodInfo = typeof(FieldInfo).GetMethod( - "SetValue", new Type[] { typeof(object), typeof(object) } - ); - - fieldTransferExpressions.Add( - Expression.Call( - Expression.Constant(fieldInfo), - setValueMethodInfo, - clone, - result - ) - ); - } - } else { - fieldTransferExpressions.Add( - Expression.Assign( - Expression.Field(clone, fieldInfo), - Expression.Convert(result, fieldType) - ) - ); - } - } - - // Wrap up the generated array or complex reference type transfer expressions - // in a null check so the field is skipped if it is not holding an instance. - transferExpressions.Add( - Expression.IfThen( - Expression.NotEqual( - Expression.Field(original, fieldInfo), Expression.Constant(null) - ), - Expression.Block(fieldVariables, fieldTransferExpressions) - ) - ); - } - - /// Assigns the value of an .initonly field - /// Type of structure that contains the field - /// - /// Reference to the structure on which the field will be assigned - /// - /// Field that will be assigned - /// Value that will be assigned to the field - private static void assignInitOnlyField( - ref TValueType instance, FieldInfo fieldInfo, object value - ) where TValueType : struct { - fieldInfo.SetValueDirect(__makeref(instance), value); - } - - } - -} // namespace Nuclex.Support.Cloning - -#endif // !NO_SETS +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if !NO_SETS + +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.Serialization; + +namespace Nuclex.Support.Cloning { + + partial class ExpressionTreeCloner : ICloneFactory { + + /// Compiles a method that creates a deep clone of an object + /// Type for which a clone method will be created + /// A method that clones an object of the provided type + /// + /// + /// The 'null' check is supposed to take place before running the cloner. This + /// avoids having redundant 'null' checks on nested types - first before calling + /// GetType() on the field to be cloned and second when runner the matching + /// cloner for the field. + /// + /// + /// This design also enables the cloning of nested value types (which can never + /// be null) without any null check whatsoever. + /// + /// + private static Func createDeepFieldBasedCloner(Type clonedType) { + ParameterExpression original = Expression.Parameter(typeof(object), "original"); + + var transferExpressions = new List(); + var variables = new List(); + + if(clonedType.IsPrimitive || (clonedType == typeof(string))) { + // Primitives and strings are copied on direct assignment + transferExpressions.Add(original); + } else if(clonedType.IsArray) { + // Arrays need to be cloned element-by-element + Type elementType = clonedType.GetElementType(); + + if(elementType.IsPrimitive || (elementType == typeof(string))) { + // For primitive arrays, the Array.Clone() method is sufficient + transferExpressions.Add( + generateFieldBasedPrimitiveArrayTransferExpressions( + clonedType, original, variables, transferExpressions + ) + ); + } else { + // To access the fields of the original type, we need it to be of the actual + // type instead of an object, so perform a downcast + ParameterExpression typedOriginal = Expression.Variable(clonedType); + variables.Add(typedOriginal); + transferExpressions.Add( + Expression.Assign(typedOriginal, Expression.Convert(original, clonedType)) + ); + + // Arrays of complex types require manual cloning + transferExpressions.Add( + generateFieldBasedComplexArrayTransferExpressions( + clonedType, typedOriginal, variables, transferExpressions + ) + ); + } + } else { + // We need a variable to hold the clone because due to the assignments it + // won't be last in the block when we're finished + ParameterExpression clone = Expression.Variable(clonedType); + variables.Add(clone); + + // Give it a new instance of the type being cloned + MethodInfo getUninitializedObjectMethodInfo = typeof(FormatterServices).GetMethod( + "GetUninitializedObject", BindingFlags.Static | BindingFlags.Public + ); + transferExpressions.Add( + Expression.Assign( + clone, + Expression.Convert( + Expression.Call( + getUninitializedObjectMethodInfo, Expression.Constant(clonedType) + ), + clonedType + ) + ) + ); + + // To access the fields of the original type, we need it to be of the actual + // type instead of an object, so perform a downcast + ParameterExpression typedOriginal = Expression.Variable(clonedType); + variables.Add(typedOriginal); + transferExpressions.Add( + Expression.Assign(typedOriginal, Expression.Convert(original, clonedType)) + ); + + // Generate the expressions required to transfer the type field by field + generateFieldBasedComplexTypeTransferExpressions( + clonedType, typedOriginal, clone, variables, transferExpressions + ); + + // Make sure the clone is the last thing in the block to set the return value + transferExpressions.Add(clone); + } + + // Turn all transfer expressions into a single block if necessary + Expression resultExpression; + if((transferExpressions.Count == 1) && (variables.Count == 0)) { + resultExpression = transferExpressions[0]; + } else { + resultExpression = Expression.Block(variables, transferExpressions); + } + + // Value types require manual boxing + if(clonedType.IsValueType) { + resultExpression = Expression.Convert(resultExpression, typeof(object)); + } + + return Expression.Lambda>(resultExpression, original).Compile(); + } + + /// Compiles a method that creates a shallow clone of an object + /// Type for which a clone method will be created + /// A method that clones an object of the provided type + private static Func createShallowFieldBasedCloner(Type clonedType) { + ParameterExpression original = Expression.Parameter(typeof(object), "original"); + + var transferExpressions = new List(); + var variables = new List(); + + if(clonedType.IsPrimitive || clonedType.IsValueType || (clonedType == typeof(string))) { + // Primitives and strings are copied on direct assignment + transferExpressions.Add(original); + } else if(clonedType.IsArray) { + transferExpressions.Add( + generateFieldBasedPrimitiveArrayTransferExpressions( + clonedType, original, variables, transferExpressions + ) + ); + } else { + // We need a variable to hold the clone because due to the assignments it + // won't be last in the block when we're finished + ParameterExpression clone = Expression.Variable(clonedType); + variables.Add(clone); + + // To access the fields of the original type, we need it to be of the actual + // type instead of an object, so perform a downcast + ParameterExpression typedOriginal = Expression.Variable(clonedType); + variables.Add(typedOriginal); + transferExpressions.Add( + Expression.Assign(typedOriginal, Expression.Convert(original, clonedType)) + ); + + // Give it a new instance of the type being cloned + MethodInfo getUninitializedObjectMethodInfo = typeof(FormatterServices).GetMethod( + "GetUninitializedObject", BindingFlags.Static | BindingFlags.Public + ); + transferExpressions.Add( + Expression.Assign( + clone, + Expression.Convert( + Expression.Call( + getUninitializedObjectMethodInfo, Expression.Constant(clonedType) + ), + clonedType + ) + ) + ); + + // Enumerate all of the type's fields and generate transfer expressions for each + FieldInfo[] fieldInfos = clonedType.GetFieldInfosIncludingBaseClasses( + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance + ); + for(int index = 0; index < fieldInfos.Length; ++index) { + FieldInfo fieldInfo = fieldInfos[index]; + + if(fieldInfo.IsInitOnly) { + Expression source = Expression.Field(typedOriginal, fieldInfo); + if(fieldInfo.FieldType.IsValueType) { + source = Expression.Convert(source, typeof(object)); + } + + if(clone.Type.IsValueType) { + MethodInfo assignInitOnlyField = typeof(ExpressionTreeCloner).GetMethod( + "assignInitOnlyField", BindingFlags.Static | BindingFlags.NonPublic + ).MakeGenericMethod(clone.Type); + + transferExpressions.Add( + Expression.Call( + assignInitOnlyField, + clone, + Expression.Constant(fieldInfo), + source + ) + ); + } else { + MethodInfo setValueMethodInfo = typeof(FieldInfo).GetMethod( + "SetValue", new Type[] { typeof(object), typeof(object) } + ); + + transferExpressions.Add( + Expression.Call( + Expression.Constant(fieldInfo), + setValueMethodInfo, + clone, + source + ) + ); + } + } else { + transferExpressions.Add( + Expression.Assign( + Expression.Field(clone, fieldInfo), + Expression.Field(typedOriginal, fieldInfo) + ) + ); + } + } + + // Make sure the clone is the last thing in the block to set the return value + transferExpressions.Add(clone); + } + + // Turn all transfer expressions into a single block if necessary + Expression resultExpression; + if((transferExpressions.Count == 1) && (variables.Count == 0)) { + resultExpression = transferExpressions[0]; + } else { + resultExpression = Expression.Block(variables, transferExpressions); + } + + // Value types require manual boxing + if(clonedType.IsValueType) { + resultExpression = Expression.Convert(resultExpression, typeof(object)); + } + + return Expression.Lambda>(resultExpression, original).Compile(); + } + + /// + /// Generates state transfer expressions to copy an array of primitive types + /// + /// Type of array that will be cloned + /// Variable expression for the original array + /// Receives variables used by the transfer expressions + /// Receives the generated transfer expressions + /// The variable holding the cloned array + private static Expression generateFieldBasedPrimitiveArrayTransferExpressions( + Type clonedType, + Expression original, + ICollection variables, + ICollection transferExpressions + ) { + MethodInfo arrayCloneMethodInfo = typeof(Array).GetMethod("Clone"); + return Expression.Convert( + Expression.Call( + Expression.Convert(original, typeof(Array)), arrayCloneMethodInfo + ), + clonedType + ); + } + + /// + /// Generates state transfer expressions to copy an array of complex types + /// + /// Type of array that will be cloned + /// Variable expression for the original array + /// Receives variables used by the transfer expressions + /// Receives the generated transfer expressions + /// The variable holding the cloned array + private static ParameterExpression generateFieldBasedComplexArrayTransferExpressions( + Type clonedType, + Expression original, + IList variables, + ICollection transferExpressions + ) { + // We need a temporary variable in order to transfer the elements of the array + ParameterExpression clone = Expression.Variable(clonedType); + variables.Add(clone); + + int dimensionCount = clonedType.GetArrayRank(); + Type elementType = clonedType.GetElementType(); + + var lengths = new List(); + var indexes = new List(); + var labels = new List(); + + // Retrieve the length of each of the array's dimensions + MethodInfo arrayGetLengthMethodInfo = typeof(Array).GetMethod("GetLength"); + for(int index = 0; index < dimensionCount; ++index) { + + // Obtain the length of the array in the current dimension + lengths.Add(Expression.Variable(typeof(int))); + variables.Add(lengths[index]); + transferExpressions.Add( + Expression.Assign( + lengths[index], + Expression.Call( + original, arrayGetLengthMethodInfo, Expression.Constant(index) + ) + ) + ); + + // Set up a variable to index the array in this dimension + indexes.Add(Expression.Variable(typeof(int))); + variables.Add(indexes[index]); + + // Also set up a label than can be used to break out of the dimension's + // transfer loop + labels.Add(Expression.Label()); + + } + + // Create a new (empty) array with the same dimensions and lengths as the original + transferExpressions.Add( + Expression.Assign(clone, Expression.NewArrayBounds(elementType, lengths)) + ); + + // Initialize the indexer of the outer loop (indexers are initialized one up + // in the loops (ie. before the loop using it begins), so we have to set this + // one outside of the loop building code. + transferExpressions.Add( + Expression.Assign(indexes[0], Expression.Constant(0)) + ); + + // Build the nested loops (one for each dimension) from the inside out + Expression innerLoop = null; + for(int index = dimensionCount - 1; index >= 0; --index) { + var loopVariables = new List(); + var loopExpressions = new List(); + + // If we reached the end of the current array dimension, break the loop + loopExpressions.Add( + Expression.IfThen( + Expression.GreaterThanOrEqual(indexes[index], lengths[index]), + Expression.Break(labels[index]) + ) + ); + + if(innerLoop == null) { + // The innermost loop clones an actual array element + + if(elementType.IsPrimitive || (elementType == typeof(string))) { + // Primitive array elements can be copied by simple assignment. This case + // should not occur since Array.Clone() should be used instead. + loopExpressions.Add( + Expression.Assign( + Expression.ArrayAccess(clone, indexes), + Expression.ArrayAccess(original, indexes) + ) + ); + } else if(elementType.IsValueType) { + // Arrays of complex value types can be transferred by assigning all fields + // of the source array element to the destination array element (cloning + // any nested reference types appropriately) + generateFieldBasedComplexTypeTransferExpressions( + elementType, + Expression.ArrayAccess(original, indexes), + Expression.ArrayAccess(clone, indexes), + variables, + loopExpressions + ); + + } else { + // Arrays of reference types need to be cloned by creating a new instance + // of the reference type and then transferring the fields over + ParameterExpression originalElement = Expression.Variable(elementType); + loopVariables.Add(originalElement); + + loopExpressions.Add( + Expression.Assign(originalElement, Expression.ArrayAccess(original, indexes)) + ); + + var nestedVariables = new List(); + var nestedTransferExpressions = new List(); + + // A nested array should be cloned by directly creating a new array (not invoking + // a cloner) since you cannot derive from an array + if(elementType.IsArray) { + Expression clonedElement; + + Type nestedElementType = elementType.GetElementType(); + if(nestedElementType.IsPrimitive || (nestedElementType == typeof(string))) { + clonedElement = generateFieldBasedPrimitiveArrayTransferExpressions( + elementType, originalElement, nestedVariables, nestedTransferExpressions + ); + } else { + clonedElement = generateFieldBasedComplexArrayTransferExpressions( + elementType, originalElement, nestedVariables, nestedTransferExpressions + ); + } + nestedTransferExpressions.Add( + Expression.Assign(Expression.ArrayAccess(clone, indexes), clonedElement) + ); + } else { + // Complex types are cloned by checking their actual, concrete type (fields + // may be typed to an interface or base class) and requesting a cloner for that + // type during runtime + MethodInfo getOrCreateClonerMethodInfo = typeof(ExpressionTreeCloner).GetMethod( + "getOrCreateDeepFieldBasedCloner", + BindingFlags.NonPublic | BindingFlags.Static + ); + MethodInfo getTypeMethodInfo = typeof(object).GetMethod("GetType"); + MethodInfo invokeMethodInfo = typeof(Func).GetMethod("Invoke"); + + // Generate expressions to do this: + // clone.SomeField = getOrCreateDeepFieldBasedCloner( + // original.SomeField.GetType() + // ).Invoke(original.SomeField); + nestedTransferExpressions.Add( + Expression.Assign( + Expression.ArrayAccess(clone, indexes), + Expression.Convert( + Expression.Call( + Expression.Call( + getOrCreateClonerMethodInfo, + Expression.Call(originalElement, getTypeMethodInfo) + ), + invokeMethodInfo, + originalElement + ), + elementType + ) + ) + ); + } + + // Whether array-in-array of reference-type-in-array, we need a null check before + // doing anything to avoid NullReferenceExceptions for unset members + loopExpressions.Add( + Expression.IfThen( + Expression.NotEqual(originalElement, Expression.Constant(null)), + Expression.Block( + nestedVariables, + nestedTransferExpressions + ) + ) + ); + } + + } else { + // Outer loops of any level just reset the inner loop's indexer and execute + // the inner loop + loopExpressions.Add( + Expression.Assign(indexes[index + 1], Expression.Constant(0)) + ); + loopExpressions.Add(innerLoop); + } + + // Each time we executed the loop instructions, increment the indexer + loopExpressions.Add(Expression.PreIncrementAssign(indexes[index])); + + // Build the loop using the expressions recorded above + innerLoop = Expression.Loop( + Expression.Block(loopVariables, loopExpressions), + labels[index] + ); + } + + // After the loop builder has finished, the innerLoop variable contains + // the entire hierarchy of nested loops, so add this to the clone expressions. + transferExpressions.Add(innerLoop); + + return clone; + } + + /// Generates state transfer expressions to copy a complex type + /// Complex type that will be cloned + /// Variable expression for the original instance + /// Variable expression for the cloned instance + /// Receives variables used by the transfer expressions + /// Receives the generated transfer expressions + private static void generateFieldBasedComplexTypeTransferExpressions( + Type clonedType, // Actual, concrete type (not declared type) + Expression original, // Expected to be an object + Expression clone, // As actual, concrete type + IList variables, + ICollection transferExpressions + ) { + // Enumerate all of the type's fields and generate transfer expressions for each + FieldInfo[] fieldInfos = clonedType.GetFieldInfosIncludingBaseClasses( + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance + ); + for(int index = 0; index < fieldInfos.Length; ++index) { + FieldInfo fieldInfo = fieldInfos[index]; + Type fieldType = fieldInfo.FieldType; + + if(fieldType.IsPrimitive || (fieldType == typeof(string))) { + // Primitive types and strings can be transferred by simple assignment + transferExpressions.Add( + Expression.Assign( + Expression.Field(clone, fieldInfo), + Expression.Field(original, fieldInfo) + ) + ); + } else if(fieldType.IsValueType) { + // A nested value type is part of the parent and will have its fields directly + // assigned without boxing, new instance creation or anything like that. + generateFieldBasedComplexTypeTransferExpressions( + fieldType, + Expression.Field(original, fieldInfo), + Expression.Field(clone, fieldInfo), + variables, + transferExpressions + ); + } else { + generateFieldBasedReferenceTypeTransferExpressions( + original, clone, transferExpressions, fieldInfo, fieldType + ); + } + } + } + + /// + /// Generates the expressions to transfer a reference type (array or class) + /// + /// Original value that will be cloned + /// Variable that will receive the cloned value + /// + /// Receives the expression generated to transfer the values + /// + /// Reflection informations about the field being cloned + /// Type of the field being cloned + private static void generateFieldBasedReferenceTypeTransferExpressions( + Expression original, + Expression clone, + ICollection transferExpressions, + FieldInfo fieldInfo, + Type fieldType + ) { + // Reference types and arrays require special care because they can be null, + // so gather the transfer expressions in a separate block for the null check + var fieldTransferExpressions = new List(); + var fieldVariables = new List(); + + if(fieldType.IsArray) { + // Arrays need to be cloned element-by-element + Expression fieldClone; + + Type elementType = fieldType.GetElementType(); + if(elementType.IsPrimitive || (elementType == typeof(string))) { + // For primitive arrays, the Array.Clone() method is sufficient + fieldClone = generateFieldBasedPrimitiveArrayTransferExpressions( + fieldType, + Expression.Field(original, fieldInfo), + fieldVariables, + fieldTransferExpressions + ); + } else { + // Arrays of complex types require manual cloning + fieldClone = generateFieldBasedComplexArrayTransferExpressions( + fieldType, + Expression.Field(original, fieldInfo), + fieldVariables, + fieldTransferExpressions + ); + } + + // Add the assignment to the transfer expressions. The array transfer expression + // generator will either have set up a temporary variable to hold the array or + // returned the conversion expression straight away + fieldTransferExpressions.Add( + Expression.Assign(Expression.Field(clone, fieldInfo), fieldClone) + ); + } else { + // Complex types are cloned by checking their actual, concrete type (fields + // may be typed to an interface or base class) and requesting a cloner for that + // type during runtime + MethodInfo getOrCreateClonerMethodInfo = typeof(ExpressionTreeCloner).GetMethod( + "getOrCreateDeepFieldBasedCloner", + BindingFlags.NonPublic | BindingFlags.Static + ); + MethodInfo getTypeMethodInfo = typeof(object).GetMethod("GetType"); + MethodInfo invokeMethodInfo = typeof(Func).GetMethod("Invoke"); + + // Equivalent to + // (TField)getOrCreateDeepFieldBasedCloner( + // original.SomeField.GetType() + // ).Invoke(original.SomeField); + Expression result = Expression.Call( + Expression.Call( + getOrCreateClonerMethodInfo, + Expression.Call( + Expression.Field(original, fieldInfo), getTypeMethodInfo + ) + ), + invokeMethodInfo, + Expression.Field(original, fieldInfo) + ); + + // If the field is a readonly field, set the value via reflection because + // Expression Trees do not support assigning .initonly fields directly yet + if(fieldInfo.IsInitOnly) { + if(fieldInfo.FieldType.IsValueType) { + result = Expression.Convert(result, typeof(object)); + } + + if(clone.Type.IsValueType) { + MethodInfo assignInitOnlyField = typeof(ExpressionTreeCloner).GetMethod( + "assignInitOnlyField", BindingFlags.Static | BindingFlags.NonPublic + ).MakeGenericMethod(clone.Type); + fieldTransferExpressions.Add( + Expression.Call( + assignInitOnlyField, + clone, + Expression.Constant(fieldInfo), + result + ) + ); + } else { + MethodInfo setValueMethodInfo = typeof(FieldInfo).GetMethod( + "SetValue", new Type[] { typeof(object), typeof(object) } + ); + + fieldTransferExpressions.Add( + Expression.Call( + Expression.Constant(fieldInfo), + setValueMethodInfo, + clone, + result + ) + ); + } + } else { + fieldTransferExpressions.Add( + Expression.Assign( + Expression.Field(clone, fieldInfo), + Expression.Convert(result, fieldType) + ) + ); + } + } + + // Wrap up the generated array or complex reference type transfer expressions + // in a null check so the field is skipped if it is not holding an instance. + transferExpressions.Add( + Expression.IfThen( + Expression.NotEqual( + Expression.Field(original, fieldInfo), Expression.Constant(null) + ), + Expression.Block(fieldVariables, fieldTransferExpressions) + ) + ); + } + + /// Assigns the value of an .initonly field + /// Type of structure that contains the field + /// + /// Reference to the structure on which the field will be assigned + /// + /// Field that will be assigned + /// Value that will be assigned to the field + private static void assignInitOnlyField( + ref TValueType instance, FieldInfo fieldInfo, object value + ) where TValueType : struct { + fieldInfo.SetValueDirect(__makeref(instance), value); + } + + } + +} // namespace Nuclex.Support.Cloning + +#endif // !NO_SETS diff --git a/Source/Cloning/ExpressionTreeCloner.PropertyBased.cs b/Source/Cloning/ExpressionTreeCloner.PropertyBased.cs index aeafd38..67cb215 100644 --- a/Source/Cloning/ExpressionTreeCloner.PropertyBased.cs +++ b/Source/Cloning/ExpressionTreeCloner.PropertyBased.cs @@ -1,668 +1,667 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if !NO_SETS - -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Reflection; - -namespace Nuclex.Support.Cloning { - - partial class ExpressionTreeCloner : ICloneFactory { - - /// Compiles a method that creates a deep clone of an object - /// Type for which a clone method will be created - /// A method that clones an object of the provided type - /// - /// - /// The 'null' check is supposed to take place before running the cloner. This - /// avoids having redundant 'null' checks on nested types - first before calling - /// GetType() on the property to be cloned and second when runner the matching - /// cloner for the property. - /// - /// - /// This design also enables the cloning of nested value types (which can never - /// be null) without any null check whatsoever. - /// - /// - private static Func createDeepPropertyBasedCloner(Type clonedType) { - ParameterExpression original = Expression.Parameter(typeof(object), "original"); - - var transferExpressions = new List(); - var variables = new List(); - - if(clonedType.IsPrimitive || (clonedType == typeof(string))) { - // Primitives and strings are copied on direct assignment - transferExpressions.Add(original); - } else if(clonedType.IsArray) { - // Arrays need to be cloned element-by-element - Type elementType = clonedType.GetElementType(); - - if(elementType.IsPrimitive || (elementType == typeof(string))) { - // For primitive arrays, the Array.Clone() method is sufficient - transferExpressions.Add( - generatePropertyBasedPrimitiveArrayTransferExpressions( - clonedType, original, variables, transferExpressions - ) - ); - } else { - // To access the properties of the original type, we need it to be of the actual - // type instead of an object, so perform a downcast - ParameterExpression typedOriginal = Expression.Variable(clonedType); - variables.Add(typedOriginal); - transferExpressions.Add( - Expression.Assign(typedOriginal, Expression.Convert(original, clonedType)) - ); - - // Arrays of complex types require manual cloning - transferExpressions.Add( - generatePropertyBasedComplexArrayTransferExpressions( - clonedType, typedOriginal, variables, transferExpressions - ) - ); - } - } else { - // We need a variable to hold the clone because due to the assignments it - // won't be last in the block when we're finished - ParameterExpression clone = Expression.Variable(clonedType); - variables.Add(clone); - - // Give it a new instance of the type being cloned - transferExpressions.Add(Expression.Assign(clone, Expression.New(clonedType))); - - // To access the properties of the original type, we need it to be of the actual - // type instead of an object, so perform a downcast - ParameterExpression typedOriginal = Expression.Variable(clonedType); - variables.Add(typedOriginal); - transferExpressions.Add( - Expression.Assign(typedOriginal, Expression.Convert(original, clonedType)) - ); - - // Generate the expressions required to transfer the type property by property - generatePropertyBasedComplexTypeTransferExpressions( - clonedType, typedOriginal, clone, variables, transferExpressions - ); - - // Make sure the clone is the last thing in the block to set the return value - transferExpressions.Add(clone); - } - - // Turn all transfer expressions into a single block if necessary - Expression resultExpression; - if((transferExpressions.Count == 1) && (variables.Count == 0)) { - resultExpression = transferExpressions[0]; - } else { - resultExpression = Expression.Block(variables, transferExpressions); - } - - // Value types require manual boxing - if(clonedType.IsValueType) { - resultExpression = Expression.Convert(resultExpression, typeof(object)); - } - - return Expression.Lambda>(resultExpression, original).Compile(); - } - - /// Compiles a method that creates a deep clone of an object - /// Type for which a clone method will be created - /// A method that clones an object of the provided type - /// - /// - /// The 'null' check is supposed to take place before running the cloner. This - /// avoids having redundant 'null' checks on nested types - first before calling - /// GetType() on the property to be cloned and second when runner the matching - /// cloner for the property. - /// - /// - /// This design also enables the cloning of nested value types (which can never - /// be null) without any null check whatsoever. - /// - /// - private static Func createShallowPropertyBasedCloner(Type clonedType) { - ParameterExpression original = Expression.Parameter(typeof(object), "original"); - - var transferExpressions = new List(); - var variables = new List(); - - if(clonedType.IsPrimitive || (clonedType == typeof(string))) { - // Primitives and strings are copied on direct assignment - transferExpressions.Add(original); - } else if(clonedType.IsArray) { - transferExpressions.Add( - generatePropertyBasedPrimitiveArrayTransferExpressions( - clonedType, original, variables, transferExpressions - ) - ); - } else { - // We need a variable to hold the clone because due to the assignments it - // won't be last in the block when we're finished - ParameterExpression clone = Expression.Variable(clonedType); - variables.Add(clone); - transferExpressions.Add(Expression.Assign(clone, Expression.New(clonedType))); - - // To access the properties of the original type, we need it to be of the actual - // type instead of an object, so perform a downcast - ParameterExpression typedOriginal = Expression.Variable(clonedType); - variables.Add(typedOriginal); - transferExpressions.Add( - Expression.Assign(typedOriginal, Expression.Convert(original, clonedType)) - ); - - generateShallowPropertyBasedComplexCloneExpressions( - clonedType, typedOriginal, clone, transferExpressions, variables - ); - - // Make sure the clone is the last thing in the block to set the return value - transferExpressions.Add(clone); - } - - // Turn all transfer expressions into a single block if necessary - Expression resultExpression; - if((transferExpressions.Count == 1) && (variables.Count == 0)) { - resultExpression = transferExpressions[0]; - } else { - resultExpression = Expression.Block(variables, transferExpressions); - } - - // Value types require manual boxing - if(clonedType.IsValueType) { - resultExpression = Expression.Convert(resultExpression, typeof(object)); - } - - return Expression.Lambda>(resultExpression, original).Compile(); - } - - /// - /// Generates expressions to transfer the properties of a complex value type - /// - /// Complex value type that will be cloned - /// Original instance whose properties will be cloned - /// Target instance into which the properties will be copied - /// Receives the value transfer expressions - /// Receives temporary variables used during the clone - private static void generateShallowPropertyBasedComplexCloneExpressions( - Type clonedType, - ParameterExpression original, - ParameterExpression clone, - ICollection transferExpressions, - ICollection variables - ) { - // Enumerate all of the type's properties and generate transfer expressions for each - PropertyInfo[] propertyInfos = clonedType.GetProperties( - BindingFlags.Public | BindingFlags.NonPublic | - BindingFlags.Instance | BindingFlags.FlattenHierarchy - ); - for(int index = 0; index < propertyInfos.Length; ++index) { - PropertyInfo propertyInfo = propertyInfos[index]; - if(propertyInfo.CanRead && propertyInfo.CanWrite) { - Type propertyType = propertyInfo.PropertyType; - - if(propertyType.IsPrimitive || (propertyType == typeof(string))) { - transferExpressions.Add( - Expression.Assign( - Expression.Property(clone, propertyInfo), - Expression.Property(original, propertyInfo) - ) - ); - } else if(propertyType.IsValueType) { - ParameterExpression originalProperty = Expression.Variable(propertyType); - variables.Add(originalProperty); - transferExpressions.Add( - Expression.Assign( - originalProperty, Expression.Property(original, propertyInfo) - ) - ); - - ParameterExpression clonedProperty = Expression.Variable(propertyType); - variables.Add(clonedProperty); - transferExpressions.Add( - Expression.Assign(clonedProperty, Expression.New(propertyType)) - ); - - generateShallowPropertyBasedComplexCloneExpressions(propertyType, originalProperty, clonedProperty, transferExpressions, variables); - - transferExpressions.Add( - Expression.Assign( - Expression.Property(clone, propertyInfo), clonedProperty - ) - ); - } else { - transferExpressions.Add( - Expression.Assign( - Expression.Property(clone, propertyInfo), - Expression.Property(original, propertyInfo) - ) - ); - } - } - } - } - - /// - /// Generates state transfer expressions to copy an array of primitive types - /// - /// Type of array that will be cloned - /// Variable expression for the original array - /// Receives variables used by the transfer expressions - /// Receives the generated transfer expressions - /// The variable holding the cloned array - private static Expression generatePropertyBasedPrimitiveArrayTransferExpressions( - Type clonedType, - Expression original, - ICollection variables, - ICollection transferExpressions - ) { - MethodInfo arrayCloneMethodInfo = typeof(Array).GetMethod("Clone"); - return Expression.Convert( - Expression.Call( - Expression.Convert(original, typeof(Array)), arrayCloneMethodInfo - ), - clonedType - ); - } - - /// - /// Generates state transfer expressions to copy an array of complex types - /// - /// Type of array that will be cloned - /// Variable expression for the original array - /// Receives variables used by the transfer expressions - /// Receives the generated transfer expressions - /// The variable holding the cloned array - private static ParameterExpression generatePropertyBasedComplexArrayTransferExpressions( - Type clonedType, - Expression original, - IList variables, - ICollection transferExpressions - ) { - // We need a temporary variable in order to transfer the elements of the array - ParameterExpression clone = Expression.Variable(clonedType); - variables.Add(clone); - - int dimensionCount = clonedType.GetArrayRank(); - Type elementType = clonedType.GetElementType(); - - var lengths = new List(); - var indexes = new List(); - var labels = new List(); - - // Retrieve the length of each of the array's dimensions - MethodInfo arrayGetLengthMethodInfo = typeof(Array).GetMethod("GetLength"); - for(int index = 0; index < dimensionCount; ++index) { - - // Obtain the length of the array in the current dimension - lengths.Add(Expression.Variable(typeof(int))); - variables.Add(lengths[index]); - transferExpressions.Add( - Expression.Assign( - lengths[index], - Expression.Call( - original, arrayGetLengthMethodInfo, Expression.Constant(index) - ) - ) - ); - - // Set up a variable to index the array in this dimension - indexes.Add(Expression.Variable(typeof(int))); - variables.Add(indexes[index]); - - // Also set up a label than can be used to break out of the dimension's - // transfer loop - labels.Add(Expression.Label()); - - } - - // Create a new (empty) array with the same dimensions and lengths as the original - transferExpressions.Add( - Expression.Assign(clone, Expression.NewArrayBounds(elementType, lengths)) - ); - - // Initialize the indexer of the outer loop (indexers are initialized one up - // in the loops (ie. before the loop using it begins), so we have to set this - // one outside of the loop building code. - transferExpressions.Add( - Expression.Assign(indexes[0], Expression.Constant(0)) - ); - - // Build the nested loops (one for each dimension) from the inside out - Expression innerLoop = null; - for(int index = dimensionCount - 1; index >= 0; --index) { - var loopVariables = new List(); - var loopExpressions = new List(); - - // If we reached the end of the current array dimension, break the loop - loopExpressions.Add( - Expression.IfThen( - Expression.GreaterThanOrEqual(indexes[index], lengths[index]), - Expression.Break(labels[index]) - ) - ); - - if(innerLoop == null) { - // The innermost loop clones an actual array element - - if(elementType.IsPrimitive || (elementType == typeof(string))) { - // Primitive array elements can be copied by simple assignment. This case - // should not occur since Array.Clone() should be used instead. - loopExpressions.Add( - Expression.Assign( - Expression.ArrayAccess(clone, indexes), - Expression.ArrayAccess(original, indexes) - ) - ); - } else if(elementType.IsValueType) { - // Arrays of complex value types can be transferred by assigning all properties - // of the source array element to the destination array element (cloning - // any nested reference types appropriately) - generatePropertyBasedComplexTypeTransferExpressions( - elementType, - Expression.ArrayAccess(original, indexes), - Expression.ArrayAccess(clone, indexes), - variables, - loopExpressions - ); - - } else { - // Arrays of reference types need to be cloned by creating a new instance - // of the reference type and then transferring the properties over - ParameterExpression originalElement = Expression.Variable(elementType); - loopVariables.Add(originalElement); - - loopExpressions.Add( - Expression.Assign(originalElement, Expression.ArrayAccess(original, indexes)) - ); - - var nestedVariables = new List(); - var nestedTransferExpressions = new List(); - - // A nested array should be cloned by directly creating a new array (not invoking - // a cloner) since you cannot derive from an array - if(elementType.IsArray) { - Expression clonedElement; - - Type nestedElementType = elementType.GetElementType(); - if(nestedElementType.IsPrimitive || (nestedElementType == typeof(string))) { - clonedElement = generatePropertyBasedPrimitiveArrayTransferExpressions( - elementType, originalElement, nestedVariables, nestedTransferExpressions - ); - } else { - clonedElement = generatePropertyBasedComplexArrayTransferExpressions( - elementType, originalElement, nestedVariables, nestedTransferExpressions - ); - } - nestedTransferExpressions.Add( - Expression.Assign(Expression.ArrayAccess(clone, indexes), clonedElement) - ); - } else { - // Complex types are cloned by checking their actual, concrete type (properties - // may be typed to an interface or base class) and requesting a cloner for that - // type during runtime - MethodInfo getOrCreateClonerMethodInfo = typeof(ExpressionTreeCloner).GetMethod( - "getOrCreateDeepPropertyBasedCloner", - BindingFlags.NonPublic | BindingFlags.Static - ); - MethodInfo getTypeMethodInfo = typeof(object).GetMethod("GetType"); - MethodInfo invokeMethodInfo = typeof(Func).GetMethod("Invoke"); - - // Generate expressions to do this: - // clone.SomeProperty = getOrCreateDeepPropertyBasedCloner( - // original.SomeProperty.GetType() - // ).Invoke(original.SomeProperty); - nestedTransferExpressions.Add( - Expression.Assign( - Expression.ArrayAccess(clone, indexes), - Expression.Convert( - Expression.Call( - Expression.Call( - getOrCreateClonerMethodInfo, - Expression.Call(originalElement, getTypeMethodInfo) - ), - invokeMethodInfo, - originalElement - ), - elementType - ) - ) - ); - } - - // Whether array-in-array of reference-type-in-array, we need a null check before - // doing anything to avoid NullReferenceExceptions for unset members - loopExpressions.Add( - Expression.IfThen( - Expression.NotEqual(originalElement, Expression.Constant(null)), - Expression.Block( - nestedVariables, - nestedTransferExpressions - ) - ) - ); - } - - } else { - // Outer loops of any level just reset the inner loop's indexer and execute - // the inner loop - loopExpressions.Add( - Expression.Assign(indexes[index + 1], Expression.Constant(0)) - ); - loopExpressions.Add(innerLoop); - } - - // Each time we executed the loop instructions, increment the indexer - loopExpressions.Add(Expression.PreIncrementAssign(indexes[index])); - - // Build the loop using the expressions recorded above - innerLoop = Expression.Loop( - Expression.Block(loopVariables, loopExpressions), - labels[index] - ); - } - - // After the loop builder has finished, the innerLoop variable contains - // the entire hierarchy of nested loops, so add this to the clone expressions. - transferExpressions.Add(innerLoop); - - return clone; - } - - /// Generates state transfer expressions to copy a complex type - /// Complex type that will be cloned - /// Variable expression for the original instance - /// Variable expression for the cloned instance - /// Receives variables used by the transfer expressions - /// Receives the generated transfer expressions - private static void generatePropertyBasedComplexTypeTransferExpressions( - Type clonedType, // Actual, concrete type (not declared type) - Expression original, // Expected to be an object - Expression clone, // As actual, concrete type - IList variables, - ICollection transferExpressions - ) { - // Enumerate all of the type's properties and generate transfer expressions for each - PropertyInfo[] propertyInfos = clonedType.GetProperties( - BindingFlags.Public | BindingFlags.NonPublic | - BindingFlags.Instance | BindingFlags.FlattenHierarchy - ); - for(int index = 0; index < propertyInfos.Length; ++index) { - PropertyInfo propertyInfo = propertyInfos[index]; - if(propertyInfo.CanRead && propertyInfo.CanWrite) { - Type propertyType = propertyInfo.PropertyType; - - if(propertyType.IsPrimitive || (propertyType == typeof(string))) { - // Primitive types and strings can be transferred by simple assignment - transferExpressions.Add( - Expression.Assign( - Expression.Property(clone, propertyInfo), - Expression.Property(original, propertyInfo) - ) - ); - } else if(propertyType.IsValueType) { - ParameterExpression originalProperty = Expression.Variable(propertyType); - variables.Add(originalProperty); - ParameterExpression clonedProperty = Expression.Variable(propertyType); - variables.Add(clonedProperty); - - transferExpressions.Add( - Expression.Assign( - originalProperty, Expression.Property(original, propertyInfo) - ) - ); - transferExpressions.Add( - Expression.Assign(clonedProperty, Expression.New(propertyType)) - ); - - // A nested value type is part of the parent and will have its properties directly - // assigned without boxing, new instance creation or anything like that. - generatePropertyBasedComplexTypeTransferExpressions( - propertyType, - originalProperty, - clonedProperty, - variables, - transferExpressions - ); - - transferExpressions.Add( - Expression.Assign( - Expression.Property(clone, propertyInfo), - clonedProperty - ) - ); - - } else { - generatePropertyBasedReferenceTypeTransferExpressions( - original, clone, transferExpressions, variables, propertyInfo, propertyType - ); - } - } - } - } - - /// - /// Generates the expressions to transfer a reference type (array or class) - /// - /// Original value that will be cloned - /// Variable that will receive the cloned value - /// - /// Receives the expression generated to transfer the values - /// - /// Receives variables used by the transfer expressions - /// Reflection informations about the property being cloned - /// Type of the property being cloned - private static void generatePropertyBasedReferenceTypeTransferExpressions( - Expression original, - Expression clone, - ICollection transferExpressions, - ICollection variables, - PropertyInfo propertyInfo, - Type propertyType - ) { - ParameterExpression originalProperty = Expression.Variable(propertyType); - variables.Add(originalProperty); - - transferExpressions.Add( - Expression.Assign(originalProperty, Expression.Property(original, propertyInfo)) - ); - - // Reference types and arrays require special care because they can be null, - // so gather the transfer expressions in a separate block for the null check - var propertyTransferExpressions = new List(); - var propertyVariables = new List(); - - if(propertyType.IsArray) { - // Arrays need to be cloned element-by-element - Expression propertyClone; - - Type elementType = propertyType.GetElementType(); - if(elementType.IsPrimitive || (elementType == typeof(string))) { - // For primitive arrays, the Array.Clone() method is sufficient - propertyClone = generatePropertyBasedPrimitiveArrayTransferExpressions( - propertyType, - originalProperty, - propertyVariables, - propertyTransferExpressions - ); - } else { - // Arrays of complex types require manual cloning - propertyClone = generatePropertyBasedComplexArrayTransferExpressions( - propertyType, - originalProperty, - propertyVariables, - propertyTransferExpressions - ); - } - - // Add the assignment to the transfer expressions. The array transfer expression - // generator will either have set up a temporary variable to hold the array or - // returned the conversion expression straight away - propertyTransferExpressions.Add( - Expression.Assign(Expression.Property(clone, propertyInfo), propertyClone) - ); - } else { - // Complex types are cloned by checking their actual, concrete type (properties - // may be typed to an interface or base class) and requesting a cloner for that - // type during runtime - MethodInfo getOrCreateClonerMethodInfo = typeof(ExpressionTreeCloner).GetMethod( - "getOrCreateDeepPropertyBasedCloner", - BindingFlags.NonPublic | BindingFlags.Static - ); - MethodInfo getTypeMethodInfo = typeof(object).GetMethod("GetType"); - MethodInfo invokeMethodInfo = typeof(Func).GetMethod("Invoke"); - - // Generate expressions to do this: - // clone.SomeProperty = getOrCreateDeepPropertyBasedCloner( - // original.SomeProperty.GetType() - // ).Invoke(original.SomeProperty); - propertyTransferExpressions.Add( - Expression.Assign( - Expression.Property(clone, propertyInfo), - Expression.Convert( - Expression.Call( - Expression.Call( - getOrCreateClonerMethodInfo, - Expression.Call(originalProperty, getTypeMethodInfo) - ), - invokeMethodInfo, - originalProperty - ), - propertyType - ) - ) - ); - } - - // Wrap up the generated array or complex reference type transfer expressions - // in a null check so the property is skipped if it is not holding an instance. - transferExpressions.Add( - Expression.IfThen( - Expression.NotEqual( - originalProperty, Expression.Constant(null) - ), - Expression.Block(propertyVariables, propertyTransferExpressions) - ) - ); - } - - } - -} // namespace Nuclex.Support.Cloning - -#endif // !NO_SETS +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if !NO_SETS + +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; + +namespace Nuclex.Support.Cloning { + + partial class ExpressionTreeCloner : ICloneFactory { + + /// Compiles a method that creates a deep clone of an object + /// Type for which a clone method will be created + /// A method that clones an object of the provided type + /// + /// + /// The 'null' check is supposed to take place before running the cloner. This + /// avoids having redundant 'null' checks on nested types - first before calling + /// GetType() on the property to be cloned and second when runner the matching + /// cloner for the property. + /// + /// + /// This design also enables the cloning of nested value types (which can never + /// be null) without any null check whatsoever. + /// + /// + private static Func createDeepPropertyBasedCloner(Type clonedType) { + ParameterExpression original = Expression.Parameter(typeof(object), "original"); + + var transferExpressions = new List(); + var variables = new List(); + + if(clonedType.IsPrimitive || (clonedType == typeof(string))) { + // Primitives and strings are copied on direct assignment + transferExpressions.Add(original); + } else if(clonedType.IsArray) { + // Arrays need to be cloned element-by-element + Type elementType = clonedType.GetElementType(); + + if(elementType.IsPrimitive || (elementType == typeof(string))) { + // For primitive arrays, the Array.Clone() method is sufficient + transferExpressions.Add( + generatePropertyBasedPrimitiveArrayTransferExpressions( + clonedType, original, variables, transferExpressions + ) + ); + } else { + // To access the properties of the original type, we need it to be of the actual + // type instead of an object, so perform a downcast + ParameterExpression typedOriginal = Expression.Variable(clonedType); + variables.Add(typedOriginal); + transferExpressions.Add( + Expression.Assign(typedOriginal, Expression.Convert(original, clonedType)) + ); + + // Arrays of complex types require manual cloning + transferExpressions.Add( + generatePropertyBasedComplexArrayTransferExpressions( + clonedType, typedOriginal, variables, transferExpressions + ) + ); + } + } else { + // We need a variable to hold the clone because due to the assignments it + // won't be last in the block when we're finished + ParameterExpression clone = Expression.Variable(clonedType); + variables.Add(clone); + + // Give it a new instance of the type being cloned + transferExpressions.Add(Expression.Assign(clone, Expression.New(clonedType))); + + // To access the properties of the original type, we need it to be of the actual + // type instead of an object, so perform a downcast + ParameterExpression typedOriginal = Expression.Variable(clonedType); + variables.Add(typedOriginal); + transferExpressions.Add( + Expression.Assign(typedOriginal, Expression.Convert(original, clonedType)) + ); + + // Generate the expressions required to transfer the type property by property + generatePropertyBasedComplexTypeTransferExpressions( + clonedType, typedOriginal, clone, variables, transferExpressions + ); + + // Make sure the clone is the last thing in the block to set the return value + transferExpressions.Add(clone); + } + + // Turn all transfer expressions into a single block if necessary + Expression resultExpression; + if((transferExpressions.Count == 1) && (variables.Count == 0)) { + resultExpression = transferExpressions[0]; + } else { + resultExpression = Expression.Block(variables, transferExpressions); + } + + // Value types require manual boxing + if(clonedType.IsValueType) { + resultExpression = Expression.Convert(resultExpression, typeof(object)); + } + + return Expression.Lambda>(resultExpression, original).Compile(); + } + + /// Compiles a method that creates a deep clone of an object + /// Type for which a clone method will be created + /// A method that clones an object of the provided type + /// + /// + /// The 'null' check is supposed to take place before running the cloner. This + /// avoids having redundant 'null' checks on nested types - first before calling + /// GetType() on the property to be cloned and second when runner the matching + /// cloner for the property. + /// + /// + /// This design also enables the cloning of nested value types (which can never + /// be null) without any null check whatsoever. + /// + /// + private static Func createShallowPropertyBasedCloner(Type clonedType) { + ParameterExpression original = Expression.Parameter(typeof(object), "original"); + + var transferExpressions = new List(); + var variables = new List(); + + if(clonedType.IsPrimitive || (clonedType == typeof(string))) { + // Primitives and strings are copied on direct assignment + transferExpressions.Add(original); + } else if(clonedType.IsArray) { + transferExpressions.Add( + generatePropertyBasedPrimitiveArrayTransferExpressions( + clonedType, original, variables, transferExpressions + ) + ); + } else { + // We need a variable to hold the clone because due to the assignments it + // won't be last in the block when we're finished + ParameterExpression clone = Expression.Variable(clonedType); + variables.Add(clone); + transferExpressions.Add(Expression.Assign(clone, Expression.New(clonedType))); + + // To access the properties of the original type, we need it to be of the actual + // type instead of an object, so perform a downcast + ParameterExpression typedOriginal = Expression.Variable(clonedType); + variables.Add(typedOriginal); + transferExpressions.Add( + Expression.Assign(typedOriginal, Expression.Convert(original, clonedType)) + ); + + generateShallowPropertyBasedComplexCloneExpressions( + clonedType, typedOriginal, clone, transferExpressions, variables + ); + + // Make sure the clone is the last thing in the block to set the return value + transferExpressions.Add(clone); + } + + // Turn all transfer expressions into a single block if necessary + Expression resultExpression; + if((transferExpressions.Count == 1) && (variables.Count == 0)) { + resultExpression = transferExpressions[0]; + } else { + resultExpression = Expression.Block(variables, transferExpressions); + } + + // Value types require manual boxing + if(clonedType.IsValueType) { + resultExpression = Expression.Convert(resultExpression, typeof(object)); + } + + return Expression.Lambda>(resultExpression, original).Compile(); + } + + /// + /// Generates expressions to transfer the properties of a complex value type + /// + /// Complex value type that will be cloned + /// Original instance whose properties will be cloned + /// Target instance into which the properties will be copied + /// Receives the value transfer expressions + /// Receives temporary variables used during the clone + private static void generateShallowPropertyBasedComplexCloneExpressions( + Type clonedType, + ParameterExpression original, + ParameterExpression clone, + ICollection transferExpressions, + ICollection variables + ) { + // Enumerate all of the type's properties and generate transfer expressions for each + PropertyInfo[] propertyInfos = clonedType.GetProperties( + BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Instance | BindingFlags.FlattenHierarchy + ); + for(int index = 0; index < propertyInfos.Length; ++index) { + PropertyInfo propertyInfo = propertyInfos[index]; + if(propertyInfo.CanRead && propertyInfo.CanWrite) { + Type propertyType = propertyInfo.PropertyType; + + if(propertyType.IsPrimitive || (propertyType == typeof(string))) { + transferExpressions.Add( + Expression.Assign( + Expression.Property(clone, propertyInfo), + Expression.Property(original, propertyInfo) + ) + ); + } else if(propertyType.IsValueType) { + ParameterExpression originalProperty = Expression.Variable(propertyType); + variables.Add(originalProperty); + transferExpressions.Add( + Expression.Assign( + originalProperty, Expression.Property(original, propertyInfo) + ) + ); + + ParameterExpression clonedProperty = Expression.Variable(propertyType); + variables.Add(clonedProperty); + transferExpressions.Add( + Expression.Assign(clonedProperty, Expression.New(propertyType)) + ); + + generateShallowPropertyBasedComplexCloneExpressions(propertyType, originalProperty, clonedProperty, transferExpressions, variables); + + transferExpressions.Add( + Expression.Assign( + Expression.Property(clone, propertyInfo), clonedProperty + ) + ); + } else { + transferExpressions.Add( + Expression.Assign( + Expression.Property(clone, propertyInfo), + Expression.Property(original, propertyInfo) + ) + ); + } + } + } + } + + /// + /// Generates state transfer expressions to copy an array of primitive types + /// + /// Type of array that will be cloned + /// Variable expression for the original array + /// Receives variables used by the transfer expressions + /// Receives the generated transfer expressions + /// The variable holding the cloned array + private static Expression generatePropertyBasedPrimitiveArrayTransferExpressions( + Type clonedType, + Expression original, + ICollection variables, + ICollection transferExpressions + ) { + MethodInfo arrayCloneMethodInfo = typeof(Array).GetMethod("Clone"); + return Expression.Convert( + Expression.Call( + Expression.Convert(original, typeof(Array)), arrayCloneMethodInfo + ), + clonedType + ); + } + + /// + /// Generates state transfer expressions to copy an array of complex types + /// + /// Type of array that will be cloned + /// Variable expression for the original array + /// Receives variables used by the transfer expressions + /// Receives the generated transfer expressions + /// The variable holding the cloned array + private static ParameterExpression generatePropertyBasedComplexArrayTransferExpressions( + Type clonedType, + Expression original, + IList variables, + ICollection transferExpressions + ) { + // We need a temporary variable in order to transfer the elements of the array + ParameterExpression clone = Expression.Variable(clonedType); + variables.Add(clone); + + int dimensionCount = clonedType.GetArrayRank(); + Type elementType = clonedType.GetElementType(); + + var lengths = new List(); + var indexes = new List(); + var labels = new List(); + + // Retrieve the length of each of the array's dimensions + MethodInfo arrayGetLengthMethodInfo = typeof(Array).GetMethod("GetLength"); + for(int index = 0; index < dimensionCount; ++index) { + + // Obtain the length of the array in the current dimension + lengths.Add(Expression.Variable(typeof(int))); + variables.Add(lengths[index]); + transferExpressions.Add( + Expression.Assign( + lengths[index], + Expression.Call( + original, arrayGetLengthMethodInfo, Expression.Constant(index) + ) + ) + ); + + // Set up a variable to index the array in this dimension + indexes.Add(Expression.Variable(typeof(int))); + variables.Add(indexes[index]); + + // Also set up a label than can be used to break out of the dimension's + // transfer loop + labels.Add(Expression.Label()); + + } + + // Create a new (empty) array with the same dimensions and lengths as the original + transferExpressions.Add( + Expression.Assign(clone, Expression.NewArrayBounds(elementType, lengths)) + ); + + // Initialize the indexer of the outer loop (indexers are initialized one up + // in the loops (ie. before the loop using it begins), so we have to set this + // one outside of the loop building code. + transferExpressions.Add( + Expression.Assign(indexes[0], Expression.Constant(0)) + ); + + // Build the nested loops (one for each dimension) from the inside out + Expression innerLoop = null; + for(int index = dimensionCount - 1; index >= 0; --index) { + var loopVariables = new List(); + var loopExpressions = new List(); + + // If we reached the end of the current array dimension, break the loop + loopExpressions.Add( + Expression.IfThen( + Expression.GreaterThanOrEqual(indexes[index], lengths[index]), + Expression.Break(labels[index]) + ) + ); + + if(innerLoop == null) { + // The innermost loop clones an actual array element + + if(elementType.IsPrimitive || (elementType == typeof(string))) { + // Primitive array elements can be copied by simple assignment. This case + // should not occur since Array.Clone() should be used instead. + loopExpressions.Add( + Expression.Assign( + Expression.ArrayAccess(clone, indexes), + Expression.ArrayAccess(original, indexes) + ) + ); + } else if(elementType.IsValueType) { + // Arrays of complex value types can be transferred by assigning all properties + // of the source array element to the destination array element (cloning + // any nested reference types appropriately) + generatePropertyBasedComplexTypeTransferExpressions( + elementType, + Expression.ArrayAccess(original, indexes), + Expression.ArrayAccess(clone, indexes), + variables, + loopExpressions + ); + + } else { + // Arrays of reference types need to be cloned by creating a new instance + // of the reference type and then transferring the properties over + ParameterExpression originalElement = Expression.Variable(elementType); + loopVariables.Add(originalElement); + + loopExpressions.Add( + Expression.Assign(originalElement, Expression.ArrayAccess(original, indexes)) + ); + + var nestedVariables = new List(); + var nestedTransferExpressions = new List(); + + // A nested array should be cloned by directly creating a new array (not invoking + // a cloner) since you cannot derive from an array + if(elementType.IsArray) { + Expression clonedElement; + + Type nestedElementType = elementType.GetElementType(); + if(nestedElementType.IsPrimitive || (nestedElementType == typeof(string))) { + clonedElement = generatePropertyBasedPrimitiveArrayTransferExpressions( + elementType, originalElement, nestedVariables, nestedTransferExpressions + ); + } else { + clonedElement = generatePropertyBasedComplexArrayTransferExpressions( + elementType, originalElement, nestedVariables, nestedTransferExpressions + ); + } + nestedTransferExpressions.Add( + Expression.Assign(Expression.ArrayAccess(clone, indexes), clonedElement) + ); + } else { + // Complex types are cloned by checking their actual, concrete type (properties + // may be typed to an interface or base class) and requesting a cloner for that + // type during runtime + MethodInfo getOrCreateClonerMethodInfo = typeof(ExpressionTreeCloner).GetMethod( + "getOrCreateDeepPropertyBasedCloner", + BindingFlags.NonPublic | BindingFlags.Static + ); + MethodInfo getTypeMethodInfo = typeof(object).GetMethod("GetType"); + MethodInfo invokeMethodInfo = typeof(Func).GetMethod("Invoke"); + + // Generate expressions to do this: + // clone.SomeProperty = getOrCreateDeepPropertyBasedCloner( + // original.SomeProperty.GetType() + // ).Invoke(original.SomeProperty); + nestedTransferExpressions.Add( + Expression.Assign( + Expression.ArrayAccess(clone, indexes), + Expression.Convert( + Expression.Call( + Expression.Call( + getOrCreateClonerMethodInfo, + Expression.Call(originalElement, getTypeMethodInfo) + ), + invokeMethodInfo, + originalElement + ), + elementType + ) + ) + ); + } + + // Whether array-in-array of reference-type-in-array, we need a null check before + // doing anything to avoid NullReferenceExceptions for unset members + loopExpressions.Add( + Expression.IfThen( + Expression.NotEqual(originalElement, Expression.Constant(null)), + Expression.Block( + nestedVariables, + nestedTransferExpressions + ) + ) + ); + } + + } else { + // Outer loops of any level just reset the inner loop's indexer and execute + // the inner loop + loopExpressions.Add( + Expression.Assign(indexes[index + 1], Expression.Constant(0)) + ); + loopExpressions.Add(innerLoop); + } + + // Each time we executed the loop instructions, increment the indexer + loopExpressions.Add(Expression.PreIncrementAssign(indexes[index])); + + // Build the loop using the expressions recorded above + innerLoop = Expression.Loop( + Expression.Block(loopVariables, loopExpressions), + labels[index] + ); + } + + // After the loop builder has finished, the innerLoop variable contains + // the entire hierarchy of nested loops, so add this to the clone expressions. + transferExpressions.Add(innerLoop); + + return clone; + } + + /// Generates state transfer expressions to copy a complex type + /// Complex type that will be cloned + /// Variable expression for the original instance + /// Variable expression for the cloned instance + /// Receives variables used by the transfer expressions + /// Receives the generated transfer expressions + private static void generatePropertyBasedComplexTypeTransferExpressions( + Type clonedType, // Actual, concrete type (not declared type) + Expression original, // Expected to be an object + Expression clone, // As actual, concrete type + IList variables, + ICollection transferExpressions + ) { + // Enumerate all of the type's properties and generate transfer expressions for each + PropertyInfo[] propertyInfos = clonedType.GetProperties( + BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Instance | BindingFlags.FlattenHierarchy + ); + for(int index = 0; index < propertyInfos.Length; ++index) { + PropertyInfo propertyInfo = propertyInfos[index]; + if(propertyInfo.CanRead && propertyInfo.CanWrite) { + Type propertyType = propertyInfo.PropertyType; + + if(propertyType.IsPrimitive || (propertyType == typeof(string))) { + // Primitive types and strings can be transferred by simple assignment + transferExpressions.Add( + Expression.Assign( + Expression.Property(clone, propertyInfo), + Expression.Property(original, propertyInfo) + ) + ); + } else if(propertyType.IsValueType) { + ParameterExpression originalProperty = Expression.Variable(propertyType); + variables.Add(originalProperty); + ParameterExpression clonedProperty = Expression.Variable(propertyType); + variables.Add(clonedProperty); + + transferExpressions.Add( + Expression.Assign( + originalProperty, Expression.Property(original, propertyInfo) + ) + ); + transferExpressions.Add( + Expression.Assign(clonedProperty, Expression.New(propertyType)) + ); + + // A nested value type is part of the parent and will have its properties directly + // assigned without boxing, new instance creation or anything like that. + generatePropertyBasedComplexTypeTransferExpressions( + propertyType, + originalProperty, + clonedProperty, + variables, + transferExpressions + ); + + transferExpressions.Add( + Expression.Assign( + Expression.Property(clone, propertyInfo), + clonedProperty + ) + ); + + } else { + generatePropertyBasedReferenceTypeTransferExpressions( + original, clone, transferExpressions, variables, propertyInfo, propertyType + ); + } + } + } + } + + /// + /// Generates the expressions to transfer a reference type (array or class) + /// + /// Original value that will be cloned + /// Variable that will receive the cloned value + /// + /// Receives the expression generated to transfer the values + /// + /// Receives variables used by the transfer expressions + /// Reflection informations about the property being cloned + /// Type of the property being cloned + private static void generatePropertyBasedReferenceTypeTransferExpressions( + Expression original, + Expression clone, + ICollection transferExpressions, + ICollection variables, + PropertyInfo propertyInfo, + Type propertyType + ) { + ParameterExpression originalProperty = Expression.Variable(propertyType); + variables.Add(originalProperty); + + transferExpressions.Add( + Expression.Assign(originalProperty, Expression.Property(original, propertyInfo)) + ); + + // Reference types and arrays require special care because they can be null, + // so gather the transfer expressions in a separate block for the null check + var propertyTransferExpressions = new List(); + var propertyVariables = new List(); + + if(propertyType.IsArray) { + // Arrays need to be cloned element-by-element + Expression propertyClone; + + Type elementType = propertyType.GetElementType(); + if(elementType.IsPrimitive || (elementType == typeof(string))) { + // For primitive arrays, the Array.Clone() method is sufficient + propertyClone = generatePropertyBasedPrimitiveArrayTransferExpressions( + propertyType, + originalProperty, + propertyVariables, + propertyTransferExpressions + ); + } else { + // Arrays of complex types require manual cloning + propertyClone = generatePropertyBasedComplexArrayTransferExpressions( + propertyType, + originalProperty, + propertyVariables, + propertyTransferExpressions + ); + } + + // Add the assignment to the transfer expressions. The array transfer expression + // generator will either have set up a temporary variable to hold the array or + // returned the conversion expression straight away + propertyTransferExpressions.Add( + Expression.Assign(Expression.Property(clone, propertyInfo), propertyClone) + ); + } else { + // Complex types are cloned by checking their actual, concrete type (properties + // may be typed to an interface or base class) and requesting a cloner for that + // type during runtime + MethodInfo getOrCreateClonerMethodInfo = typeof(ExpressionTreeCloner).GetMethod( + "getOrCreateDeepPropertyBasedCloner", + BindingFlags.NonPublic | BindingFlags.Static + ); + MethodInfo getTypeMethodInfo = typeof(object).GetMethod("GetType"); + MethodInfo invokeMethodInfo = typeof(Func).GetMethod("Invoke"); + + // Generate expressions to do this: + // clone.SomeProperty = getOrCreateDeepPropertyBasedCloner( + // original.SomeProperty.GetType() + // ).Invoke(original.SomeProperty); + propertyTransferExpressions.Add( + Expression.Assign( + Expression.Property(clone, propertyInfo), + Expression.Convert( + Expression.Call( + Expression.Call( + getOrCreateClonerMethodInfo, + Expression.Call(originalProperty, getTypeMethodInfo) + ), + invokeMethodInfo, + originalProperty + ), + propertyType + ) + ) + ); + } + + // Wrap up the generated array or complex reference type transfer expressions + // in a null check so the property is skipped if it is not holding an instance. + transferExpressions.Add( + Expression.IfThen( + Expression.NotEqual( + originalProperty, Expression.Constant(null) + ), + Expression.Block(propertyVariables, propertyTransferExpressions) + ) + ); + } + + } + +} // namespace Nuclex.Support.Cloning + +#endif // !NO_SETS diff --git a/Source/Cloning/ExpressionTreeCloner.Test.cs b/Source/Cloning/ExpressionTreeCloner.Test.cs index b5ff074..c774932 100644 --- a/Source/Cloning/ExpressionTreeCloner.Test.cs +++ b/Source/Cloning/ExpressionTreeCloner.Test.cs @@ -1,226 +1,225 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if !NO_SETS - -#if UNITTEST - -using System; -using System.Collections.Generic; - -using NUnit.Framework; - -namespace Nuclex.Support.Cloning { - - /// Unit Test for the expression tree-based cloner - [TestFixture] - internal class ExpressionTreeClonerTest : CloneFactoryTest { - - /// Initializes a new unit test suite for the reflection cloner - public ExpressionTreeClonerTest() { - this.cloneFactory = new ExpressionTreeCloner(); - } - - /// Verifies that cloning a null object simply returns null - [Test] - public void CloningNullYieldsNull() { - Assert.IsNull(this.cloneFactory.DeepFieldClone(null)); - Assert.IsNull(this.cloneFactory.DeepPropertyClone(null)); - Assert.IsNull(this.cloneFactory.ShallowFieldClone(null)); - Assert.IsNull(this.cloneFactory.ShallowPropertyClone(null)); - } - - /// - /// Verifies that clones of objects whose class doesn't possess a default constructor - /// can be made - /// - [Test] - public void ClassWithoutDefaultConstructorCanBeCloned() { - var original = new ClassWithoutDefaultConstructor(1234); - ClassWithoutDefaultConstructor clone = this.cloneFactory.DeepFieldClone(original); - - Assert.AreNotSame(original, clone); - Assert.AreEqual(original.Dummy, clone.Dummy); - } - - /// Verifies that clones of primitive types can be created - [Test] - public void PrimitiveTypesCanBeCloned() { - int original = 12345; - int clone = this.cloneFactory.DeepFieldClone(original); - Assert.AreEqual(original, clone); - } - - /// Verifies that shallow clones of arrays can be made - [Test] - public void ReferenceTypesCanBeCloned() { - var original = new TestReferenceType() { TestField = 123, TestProperty = 456 }; - TestReferenceType clone = this.cloneFactory.DeepFieldClone(original); - - Assert.AreNotSame(original, clone); - Assert.AreEqual(original.TestField, clone.TestField); - Assert.AreEqual(original.TestProperty, clone.TestProperty); - } - - /// Verifies that shallow clones of arrays can be made - [Test] - public void PrimitiveArraysCanBeCloned() { - var original = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; - int[] clone = this.cloneFactory.DeepFieldClone(original); - - Assert.AreNotSame(original, clone); - CollectionAssert.AreEqual(original, clone); - } - - /// Verifies that shallow clones of arrays can be made - [Test] - public void ShallowClonesOfArraysCanBeMade() { - var original = new TestReferenceType[] { - new TestReferenceType() { TestField = 123, TestProperty = 456 } - }; - TestReferenceType[] clone = this.cloneFactory.ShallowFieldClone(original); - - Assert.AreSame(original[0], clone[0]); - } - - /// Verifies that deep clones of arrays can be made - [Test] - public void DeepClonesOfArraysCanBeMade() { - var original = new TestReferenceType[,] { - { - new TestReferenceType() { TestField = 123, TestProperty = 456 } - } - }; - TestReferenceType[,] clone = this.cloneFactory.DeepFieldClone(original); - - Assert.AreNotSame(original[0, 0], clone[0, 0]); - Assert.AreEqual(original[0, 0].TestField, clone[0, 0].TestField); - Assert.AreEqual(original[0, 0].TestProperty, clone[0, 0].TestProperty); - } - - /// Verifies that deep clones of a generic list can be made - [Test] - public void GenericListsCanBeCloned() { - var original = new List(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }); - List clone = this.cloneFactory.DeepFieldClone(original); - - CollectionAssert.AreEqual(original, clone); - } - - /// Verifies that deep clones of a generic dictionary can be made - [Test] - public void GenericDictionariesCanBeCloned() { - var original = new Dictionary(); - original.Add(1, "one"); - Dictionary clone = this.cloneFactory.DeepFieldClone(original); - - Assert.AreEqual("one", clone[1]); - } - - /// - /// Verifies that a field-based shallow clone of a value type can be performed - /// - [Test] - public void ShallowFieldBasedClonesOfValueTypesCanBeMade() { - HierarchicalValueType original = CreateValueType(); - HierarchicalValueType clone = this.cloneFactory.ShallowFieldClone(original); - VerifyClone(ref original, ref clone, isDeepClone: false, isPropertyBasedClone: false); - } - - /// - /// Verifies that a field-based shallow clone of a reference type can be performed - /// - [Test] - public void ShallowFieldBasedClonesOfReferenceTypesCanBeMade() { - HierarchicalReferenceType original = CreateReferenceType(); - HierarchicalReferenceType clone = this.cloneFactory.ShallowFieldClone(original); - VerifyClone(original, clone, isDeepClone: false, isPropertyBasedClone: false); - } - - /// - /// Verifies that a field-based deep clone of a value type can be performed - /// - [Test] - public void DeepFieldBasedClonesOfValueTypesCanBeMade() { - HierarchicalValueType original = CreateValueType(); - HierarchicalValueType clone = this.cloneFactory.DeepFieldClone(original); - VerifyClone(ref original, ref clone, isDeepClone: true, isPropertyBasedClone: false); - } - - /// - /// Verifies that a field-based deep clone of a reference type can be performed - /// - [Test] - public void DeepFieldBasedClonesOfReferenceTypesCanBeMade() { - HierarchicalReferenceType original = CreateReferenceType(); - HierarchicalReferenceType clone = this.cloneFactory.DeepFieldClone(original); - VerifyClone(original, clone, isDeepClone: true, isPropertyBasedClone: false); - } - - /// - /// Verifies that a property-based shallow clone of a value type can be performed - /// - [Test] - public void ShallowPropertyBasedClonesOfValueTypesCanBeMade() { - HierarchicalValueType original = CreateValueType(); - HierarchicalValueType clone = this.cloneFactory.ShallowPropertyClone(original); - VerifyClone(ref original, ref clone, isDeepClone: false, isPropertyBasedClone: true); - } - - /// - /// Verifies that a property-based shallow clone of a reference type can be performed - /// - [Test] - public void ShallowPropertyBasedClonesOfReferenceTypesCanBeMade() { - HierarchicalReferenceType original = CreateReferenceType(); - HierarchicalReferenceType clone = this.cloneFactory.ShallowPropertyClone(original); - VerifyClone(original, clone, isDeepClone: false, isPropertyBasedClone: true); - } - - /// - /// Verifies that a property-based deep clone of a value type can be performed - /// - [Test] - public void DeepPropertyBasedClonesOfValueTypesCanBeMade() { - HierarchicalValueType original = CreateValueType(); - HierarchicalValueType clone = this.cloneFactory.DeepPropertyClone(original); - VerifyClone(ref original, ref clone, isDeepClone: true, isPropertyBasedClone: true); - } - - /// - /// Verifies that a property-based deep clone of a reference type can be performed - /// - [Test] - public void DeepPropertyBasedClonesOfReferenceTypesCanBeMade() { - HierarchicalReferenceType original = CreateReferenceType(); - HierarchicalReferenceType clone = this.cloneFactory.DeepPropertyClone(original); - VerifyClone(original, clone, isDeepClone: true, isPropertyBasedClone: true); - } - - /// Clone factory being tested - private ICloneFactory cloneFactory; - - } - -} // namespace Nuclex.Support.Cloning - -#endif // UNITTEST - -#endif // !NO_SETS +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if !NO_SETS + +#if UNITTEST + +using System; +using System.Collections.Generic; + +using NUnit.Framework; + +namespace Nuclex.Support.Cloning { + + /// Unit Test for the expression tree-based cloner + [TestFixture] + internal class ExpressionTreeClonerTest : CloneFactoryTest { + + /// Initializes a new unit test suite for the reflection cloner + public ExpressionTreeClonerTest() { + this.cloneFactory = new ExpressionTreeCloner(); + } + + /// Verifies that cloning a null object simply returns null + [Test] + public void CloningNullYieldsNull() { + Assert.IsNull(this.cloneFactory.DeepFieldClone(null)); + Assert.IsNull(this.cloneFactory.DeepPropertyClone(null)); + Assert.IsNull(this.cloneFactory.ShallowFieldClone(null)); + Assert.IsNull(this.cloneFactory.ShallowPropertyClone(null)); + } + + /// + /// Verifies that clones of objects whose class doesn't possess a default constructor + /// can be made + /// + [Test] + public void ClassWithoutDefaultConstructorCanBeCloned() { + var original = new ClassWithoutDefaultConstructor(1234); + ClassWithoutDefaultConstructor clone = this.cloneFactory.DeepFieldClone(original); + + Assert.AreNotSame(original, clone); + Assert.AreEqual(original.Dummy, clone.Dummy); + } + + /// Verifies that clones of primitive types can be created + [Test] + public void PrimitiveTypesCanBeCloned() { + int original = 12345; + int clone = this.cloneFactory.DeepFieldClone(original); + Assert.AreEqual(original, clone); + } + + /// Verifies that shallow clones of arrays can be made + [Test] + public void ReferenceTypesCanBeCloned() { + var original = new TestReferenceType() { TestField = 123, TestProperty = 456 }; + TestReferenceType clone = this.cloneFactory.DeepFieldClone(original); + + Assert.AreNotSame(original, clone); + Assert.AreEqual(original.TestField, clone.TestField); + Assert.AreEqual(original.TestProperty, clone.TestProperty); + } + + /// Verifies that shallow clones of arrays can be made + [Test] + public void PrimitiveArraysCanBeCloned() { + var original = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + int[] clone = this.cloneFactory.DeepFieldClone(original); + + Assert.AreNotSame(original, clone); + CollectionAssert.AreEqual(original, clone); + } + + /// Verifies that shallow clones of arrays can be made + [Test] + public void ShallowClonesOfArraysCanBeMade() { + var original = new TestReferenceType[] { + new TestReferenceType() { TestField = 123, TestProperty = 456 } + }; + TestReferenceType[] clone = this.cloneFactory.ShallowFieldClone(original); + + Assert.AreSame(original[0], clone[0]); + } + + /// Verifies that deep clones of arrays can be made + [Test] + public void DeepClonesOfArraysCanBeMade() { + var original = new TestReferenceType[,] { + { + new TestReferenceType() { TestField = 123, TestProperty = 456 } + } + }; + TestReferenceType[,] clone = this.cloneFactory.DeepFieldClone(original); + + Assert.AreNotSame(original[0, 0], clone[0, 0]); + Assert.AreEqual(original[0, 0].TestField, clone[0, 0].TestField); + Assert.AreEqual(original[0, 0].TestProperty, clone[0, 0].TestProperty); + } + + /// Verifies that deep clones of a generic list can be made + [Test] + public void GenericListsCanBeCloned() { + var original = new List(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }); + List clone = this.cloneFactory.DeepFieldClone(original); + + CollectionAssert.AreEqual(original, clone); + } + + /// Verifies that deep clones of a generic dictionary can be made + [Test] + public void GenericDictionariesCanBeCloned() { + var original = new Dictionary(); + original.Add(1, "one"); + Dictionary clone = this.cloneFactory.DeepFieldClone(original); + + Assert.AreEqual("one", clone[1]); + } + + /// + /// Verifies that a field-based shallow clone of a value type can be performed + /// + [Test] + public void ShallowFieldBasedClonesOfValueTypesCanBeMade() { + HierarchicalValueType original = CreateValueType(); + HierarchicalValueType clone = this.cloneFactory.ShallowFieldClone(original); + VerifyClone(ref original, ref clone, isDeepClone: false, isPropertyBasedClone: false); + } + + /// + /// Verifies that a field-based shallow clone of a reference type can be performed + /// + [Test] + public void ShallowFieldBasedClonesOfReferenceTypesCanBeMade() { + HierarchicalReferenceType original = CreateReferenceType(); + HierarchicalReferenceType clone = this.cloneFactory.ShallowFieldClone(original); + VerifyClone(original, clone, isDeepClone: false, isPropertyBasedClone: false); + } + + /// + /// Verifies that a field-based deep clone of a value type can be performed + /// + [Test] + public void DeepFieldBasedClonesOfValueTypesCanBeMade() { + HierarchicalValueType original = CreateValueType(); + HierarchicalValueType clone = this.cloneFactory.DeepFieldClone(original); + VerifyClone(ref original, ref clone, isDeepClone: true, isPropertyBasedClone: false); + } + + /// + /// Verifies that a field-based deep clone of a reference type can be performed + /// + [Test] + public void DeepFieldBasedClonesOfReferenceTypesCanBeMade() { + HierarchicalReferenceType original = CreateReferenceType(); + HierarchicalReferenceType clone = this.cloneFactory.DeepFieldClone(original); + VerifyClone(original, clone, isDeepClone: true, isPropertyBasedClone: false); + } + + /// + /// Verifies that a property-based shallow clone of a value type can be performed + /// + [Test] + public void ShallowPropertyBasedClonesOfValueTypesCanBeMade() { + HierarchicalValueType original = CreateValueType(); + HierarchicalValueType clone = this.cloneFactory.ShallowPropertyClone(original); + VerifyClone(ref original, ref clone, isDeepClone: false, isPropertyBasedClone: true); + } + + /// + /// Verifies that a property-based shallow clone of a reference type can be performed + /// + [Test] + public void ShallowPropertyBasedClonesOfReferenceTypesCanBeMade() { + HierarchicalReferenceType original = CreateReferenceType(); + HierarchicalReferenceType clone = this.cloneFactory.ShallowPropertyClone(original); + VerifyClone(original, clone, isDeepClone: false, isPropertyBasedClone: true); + } + + /// + /// Verifies that a property-based deep clone of a value type can be performed + /// + [Test] + public void DeepPropertyBasedClonesOfValueTypesCanBeMade() { + HierarchicalValueType original = CreateValueType(); + HierarchicalValueType clone = this.cloneFactory.DeepPropertyClone(original); + VerifyClone(ref original, ref clone, isDeepClone: true, isPropertyBasedClone: true); + } + + /// + /// Verifies that a property-based deep clone of a reference type can be performed + /// + [Test] + public void DeepPropertyBasedClonesOfReferenceTypesCanBeMade() { + HierarchicalReferenceType original = CreateReferenceType(); + HierarchicalReferenceType clone = this.cloneFactory.DeepPropertyClone(original); + VerifyClone(original, clone, isDeepClone: true, isPropertyBasedClone: true); + } + + /// Clone factory being tested + private ICloneFactory cloneFactory; + + } + +} // namespace Nuclex.Support.Cloning + +#endif // UNITTEST + +#endif // !NO_SETS diff --git a/Source/Cloning/ExpressionTreeCloner.cs b/Source/Cloning/ExpressionTreeCloner.cs index 6754086..388016c 100644 --- a/Source/Cloning/ExpressionTreeCloner.cs +++ b/Source/Cloning/ExpressionTreeCloner.cs @@ -1,286 +1,285 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if !NO_SETS - -using System; -using System.Collections.Concurrent; - -namespace Nuclex.Support.Cloning { - - /// - /// Cloning factory which uses expression trees to improve performance when cloning - /// is a high-frequency action. - /// - public partial class ExpressionTreeCloner : ICloneFactory { - - /// Initializes the static members of the expression tree cloner - static ExpressionTreeCloner() { - shallowFieldBasedCloners = new ConcurrentDictionary>(); - deepFieldBasedCloners = new ConcurrentDictionary>(); - shallowPropertyBasedCloners = new ConcurrentDictionary>(); - deepPropertyBasedCloners = new ConcurrentDictionary>(); - } - - /// - /// Creates a deep clone of the specified object, also creating clones of all - /// child objects being referenced - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A deep clone of the provided object - public static TCloned DeepFieldClone(TCloned objectToClone) { - object objectToCloneAsObject = objectToClone; - if(objectToCloneAsObject == null) { - return default(TCloned); - } - - Func cloner = getOrCreateDeepFieldBasedCloner(typeof(TCloned)); - return (TCloned)cloner(objectToCloneAsObject); - } - - /// - /// Creates a deep clone of the specified object, also creating clones of all - /// child objects being referenced - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A deep clone of the provided object - public static TCloned DeepPropertyClone(TCloned objectToClone) { - object objectToCloneAsObject = objectToClone; - if(objectToCloneAsObject == null) { - return default(TCloned); - } - - Func cloner = getOrCreateDeepPropertyBasedCloner(typeof(TCloned)); - return (TCloned)cloner(objectToCloneAsObject); - } - - /// - /// Creates a shallow clone of the specified object, reusing any referenced objects - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A shallow clone of the provided object - public static TCloned ShallowFieldClone(TCloned objectToClone) { - object objectToCloneAsObject = objectToClone; - if(objectToCloneAsObject == null) { - return default(TCloned); - } - - Func cloner = getOrCreateShallowFieldBasedCloner(typeof(TCloned)); - return (TCloned)cloner(objectToCloneAsObject); - } - - /// - /// Creates a shallow clone of the specified object, reusing any referenced objects - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A shallow clone of the provided object - public static TCloned ShallowPropertyClone(TCloned objectToClone) { - object objectToCloneAsObject = objectToClone; - if(objectToCloneAsObject == null) { - return default(TCloned); - } - - Func cloner = getOrCreateShallowPropertyBasedCloner(typeof(TCloned)); - return (TCloned)cloner(objectToCloneAsObject); - } - - /// - /// Creates a shallow clone of the specified object, reusing any referenced objects - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A shallow clone of the provided object - TCloned ICloneFactory.ShallowFieldClone(TCloned objectToClone) { - return ExpressionTreeCloner.ShallowFieldClone(objectToClone); - } - - /// - /// Creates a shallow clone of the specified object, reusing any referenced objects - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A shallow clone of the provided object - TCloned ICloneFactory.ShallowPropertyClone(TCloned objectToClone) { - return ExpressionTreeCloner.ShallowPropertyClone(objectToClone); - } - - /// - /// Creates a deep clone of the specified object, also creating clones of all - /// child objects being referenced - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A deep clone of the provided object - TCloned ICloneFactory.DeepFieldClone(TCloned objectToClone) { - return ExpressionTreeCloner.DeepFieldClone(objectToClone); - } - - /// - /// Creates a deep clone of the specified object, also creating clones of all - /// child objects being referenced - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A deep clone of the provided object - TCloned ICloneFactory.DeepPropertyClone(TCloned objectToClone) { - return ExpressionTreeCloner.DeepPropertyClone(objectToClone); - } - -#if false - /// - /// Transfers the state of one object into another, creating clones of referenced objects - /// - /// Type of the object whose sate will be transferred - /// Original instance the state will be taken from - /// Target instance the state will be written to - /// Whether to perform a property-based state copy - public void DeepCopyState(TState original, TState target, bool propertyBased) - where TState : class { - throw new NotImplementedException(); - } - - /// - /// Transfers the state of one object into another, creating clones of referenced objects - /// - /// Type of the object whose sate will be transferred - /// Original instance the state will be taken from - /// Target instance the state will be written to - /// Whether to perform a property-based state copy - public void DeepCopyState(ref TState original, ref TState target, bool propertyBased) - where TState : struct { - throw new NotImplementedException(); - } - - /// Transfers the state of one object into another - /// Type of the object whose sate will be transferred - /// Original instance the state will be taken from - /// Target instance the state will be written to - /// Whether to perform a property-based state copy - public void ShallowCopyState(TState original, TState target, bool propertyBased) - where TState : class { - throw new NotImplementedException(); - } - - /// Transfers the state of one object into another - /// Type of the object whose sate will be transferred - /// Original instance the state will be taken from - /// Target instance the state will be written to - /// Whether to perform a property-based state copy - public void ShallowCopyState(ref TState original, ref TState target, bool propertyBased) - where TState : struct { - throw new NotImplementedException(); - } - -#endif - - /// - /// Retrieves the existing clone method for the specified type or compiles one if - /// none exists for the type yet - /// - /// Type for which a clone method will be retrieved - /// The clone method for the specified type - private static Func getOrCreateShallowFieldBasedCloner(Type clonedType) { - Func cloner; - - if(!shallowFieldBasedCloners.TryGetValue(clonedType, out cloner)) { - cloner = createShallowFieldBasedCloner(clonedType); - shallowFieldBasedCloners.TryAdd(clonedType, cloner); - } - - return cloner; - } - - /// - /// Retrieves the existing clone method for the specified type or compiles one if - /// none exists for the type yet - /// - /// Type for which a clone method will be retrieved - /// The clone method for the specified type - private static Func getOrCreateDeepFieldBasedCloner(Type clonedType) { - Func cloner; - - if(!deepFieldBasedCloners.TryGetValue(clonedType, out cloner)) { - cloner = createDeepFieldBasedCloner(clonedType); - deepFieldBasedCloners.TryAdd(clonedType, cloner); - } - - return cloner; - } - - /// - /// Retrieves the existing clone method for the specified type or compiles one if - /// none exists for the type yet - /// - /// Type for which a clone method will be retrieved - /// The clone method for the specified type - private static Func getOrCreateShallowPropertyBasedCloner(Type clonedType) { - Func cloner; - - if(!shallowPropertyBasedCloners.TryGetValue(clonedType, out cloner)) { - cloner = createShallowPropertyBasedCloner(clonedType); - shallowPropertyBasedCloners.TryAdd(clonedType, cloner); - } - - return cloner; - } - - /// - /// Retrieves the existing clone method for the specified type or compiles one if - /// none exists for the type yet - /// - /// Type for which a clone method will be retrieved - /// The clone method for the specified type - private static Func getOrCreateDeepPropertyBasedCloner(Type clonedType) { - Func cloner; - - if(!deepPropertyBasedCloners.TryGetValue(clonedType, out cloner)) { - cloner = createDeepPropertyBasedCloner(clonedType); - deepPropertyBasedCloners.TryAdd(clonedType, cloner); - } - - return cloner; - } - - /// Compiled cloners that perform shallow clone operations - private static readonly ConcurrentDictionary< - Type, Func - > shallowFieldBasedCloners; - /// Compiled cloners that perform deep clone operations - private static readonly ConcurrentDictionary< - Type, Func - > deepFieldBasedCloners; - /// Compiled cloners that perform shallow clone operations - private static readonly ConcurrentDictionary< - Type, Func - > shallowPropertyBasedCloners; - /// Compiled cloners that perform deep clone operations - private static readonly ConcurrentDictionary< - Type, Func - > deepPropertyBasedCloners; - - } - -} // namespace Nuclex.Support.Cloning - -#endif // !NO_SETS +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if !NO_SETS + +using System; +using System.Collections.Concurrent; + +namespace Nuclex.Support.Cloning { + + /// + /// Cloning factory which uses expression trees to improve performance when cloning + /// is a high-frequency action. + /// + public partial class ExpressionTreeCloner : ICloneFactory { + + /// Initializes the static members of the expression tree cloner + static ExpressionTreeCloner() { + shallowFieldBasedCloners = new ConcurrentDictionary>(); + deepFieldBasedCloners = new ConcurrentDictionary>(); + shallowPropertyBasedCloners = new ConcurrentDictionary>(); + deepPropertyBasedCloners = new ConcurrentDictionary>(); + } + + /// + /// Creates a deep clone of the specified object, also creating clones of all + /// child objects being referenced + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A deep clone of the provided object + public static TCloned DeepFieldClone(TCloned objectToClone) { + object objectToCloneAsObject = objectToClone; + if(objectToCloneAsObject == null) { + return default(TCloned); + } + + Func cloner = getOrCreateDeepFieldBasedCloner(typeof(TCloned)); + return (TCloned)cloner(objectToCloneAsObject); + } + + /// + /// Creates a deep clone of the specified object, also creating clones of all + /// child objects being referenced + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A deep clone of the provided object + public static TCloned DeepPropertyClone(TCloned objectToClone) { + object objectToCloneAsObject = objectToClone; + if(objectToCloneAsObject == null) { + return default(TCloned); + } + + Func cloner = getOrCreateDeepPropertyBasedCloner(typeof(TCloned)); + return (TCloned)cloner(objectToCloneAsObject); + } + + /// + /// Creates a shallow clone of the specified object, reusing any referenced objects + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A shallow clone of the provided object + public static TCloned ShallowFieldClone(TCloned objectToClone) { + object objectToCloneAsObject = objectToClone; + if(objectToCloneAsObject == null) { + return default(TCloned); + } + + Func cloner = getOrCreateShallowFieldBasedCloner(typeof(TCloned)); + return (TCloned)cloner(objectToCloneAsObject); + } + + /// + /// Creates a shallow clone of the specified object, reusing any referenced objects + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A shallow clone of the provided object + public static TCloned ShallowPropertyClone(TCloned objectToClone) { + object objectToCloneAsObject = objectToClone; + if(objectToCloneAsObject == null) { + return default(TCloned); + } + + Func cloner = getOrCreateShallowPropertyBasedCloner(typeof(TCloned)); + return (TCloned)cloner(objectToCloneAsObject); + } + + /// + /// Creates a shallow clone of the specified object, reusing any referenced objects + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A shallow clone of the provided object + TCloned ICloneFactory.ShallowFieldClone(TCloned objectToClone) { + return ExpressionTreeCloner.ShallowFieldClone(objectToClone); + } + + /// + /// Creates a shallow clone of the specified object, reusing any referenced objects + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A shallow clone of the provided object + TCloned ICloneFactory.ShallowPropertyClone(TCloned objectToClone) { + return ExpressionTreeCloner.ShallowPropertyClone(objectToClone); + } + + /// + /// Creates a deep clone of the specified object, also creating clones of all + /// child objects being referenced + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A deep clone of the provided object + TCloned ICloneFactory.DeepFieldClone(TCloned objectToClone) { + return ExpressionTreeCloner.DeepFieldClone(objectToClone); + } + + /// + /// Creates a deep clone of the specified object, also creating clones of all + /// child objects being referenced + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A deep clone of the provided object + TCloned ICloneFactory.DeepPropertyClone(TCloned objectToClone) { + return ExpressionTreeCloner.DeepPropertyClone(objectToClone); + } + +#if false + /// + /// Transfers the state of one object into another, creating clones of referenced objects + /// + /// Type of the object whose sate will be transferred + /// Original instance the state will be taken from + /// Target instance the state will be written to + /// Whether to perform a property-based state copy + public void DeepCopyState(TState original, TState target, bool propertyBased) + where TState : class { + throw new NotImplementedException(); + } + + /// + /// Transfers the state of one object into another, creating clones of referenced objects + /// + /// Type of the object whose sate will be transferred + /// Original instance the state will be taken from + /// Target instance the state will be written to + /// Whether to perform a property-based state copy + public void DeepCopyState(ref TState original, ref TState target, bool propertyBased) + where TState : struct { + throw new NotImplementedException(); + } + + /// Transfers the state of one object into another + /// Type of the object whose sate will be transferred + /// Original instance the state will be taken from + /// Target instance the state will be written to + /// Whether to perform a property-based state copy + public void ShallowCopyState(TState original, TState target, bool propertyBased) + where TState : class { + throw new NotImplementedException(); + } + + /// Transfers the state of one object into another + /// Type of the object whose sate will be transferred + /// Original instance the state will be taken from + /// Target instance the state will be written to + /// Whether to perform a property-based state copy + public void ShallowCopyState(ref TState original, ref TState target, bool propertyBased) + where TState : struct { + throw new NotImplementedException(); + } + +#endif + + /// + /// Retrieves the existing clone method for the specified type or compiles one if + /// none exists for the type yet + /// + /// Type for which a clone method will be retrieved + /// The clone method for the specified type + private static Func getOrCreateShallowFieldBasedCloner(Type clonedType) { + Func cloner; + + if(!shallowFieldBasedCloners.TryGetValue(clonedType, out cloner)) { + cloner = createShallowFieldBasedCloner(clonedType); + shallowFieldBasedCloners.TryAdd(clonedType, cloner); + } + + return cloner; + } + + /// + /// Retrieves the existing clone method for the specified type or compiles one if + /// none exists for the type yet + /// + /// Type for which a clone method will be retrieved + /// The clone method for the specified type + private static Func getOrCreateDeepFieldBasedCloner(Type clonedType) { + Func cloner; + + if(!deepFieldBasedCloners.TryGetValue(clonedType, out cloner)) { + cloner = createDeepFieldBasedCloner(clonedType); + deepFieldBasedCloners.TryAdd(clonedType, cloner); + } + + return cloner; + } + + /// + /// Retrieves the existing clone method for the specified type or compiles one if + /// none exists for the type yet + /// + /// Type for which a clone method will be retrieved + /// The clone method for the specified type + private static Func getOrCreateShallowPropertyBasedCloner(Type clonedType) { + Func cloner; + + if(!shallowPropertyBasedCloners.TryGetValue(clonedType, out cloner)) { + cloner = createShallowPropertyBasedCloner(clonedType); + shallowPropertyBasedCloners.TryAdd(clonedType, cloner); + } + + return cloner; + } + + /// + /// Retrieves the existing clone method for the specified type or compiles one if + /// none exists for the type yet + /// + /// Type for which a clone method will be retrieved + /// The clone method for the specified type + private static Func getOrCreateDeepPropertyBasedCloner(Type clonedType) { + Func cloner; + + if(!deepPropertyBasedCloners.TryGetValue(clonedType, out cloner)) { + cloner = createDeepPropertyBasedCloner(clonedType); + deepPropertyBasedCloners.TryAdd(clonedType, cloner); + } + + return cloner; + } + + /// Compiled cloners that perform shallow clone operations + private static readonly ConcurrentDictionary< + Type, Func + > shallowFieldBasedCloners; + /// Compiled cloners that perform deep clone operations + private static readonly ConcurrentDictionary< + Type, Func + > deepFieldBasedCloners; + /// Compiled cloners that perform shallow clone operations + private static readonly ConcurrentDictionary< + Type, Func + > shallowPropertyBasedCloners; + /// Compiled cloners that perform deep clone operations + private static readonly ConcurrentDictionary< + Type, Func + > deepPropertyBasedCloners; + + } + +} // namespace Nuclex.Support.Cloning + +#endif // !NO_SETS diff --git a/Source/Cloning/ICloneFactory.cs b/Source/Cloning/ICloneFactory.cs index 0a29970..94f8a4a 100644 --- a/Source/Cloning/ICloneFactory.cs +++ b/Source/Cloning/ICloneFactory.cs @@ -1,100 +1,99 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; - -namespace Nuclex.Support.Cloning { - - /// Constructs new objects by cloning existing objects - public interface ICloneFactory { - - /// - /// Creates a shallow clone of the specified object, reusing any referenced objects - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A shallow clone of the provided object - /// - /// Field-based clones are guaranteed to be complete - there will be no missed - /// members. This type of clone is also able to clone types that do not provide - /// a default constructor. - /// - TCloned ShallowFieldClone(TCloned objectToClone); - - /// - /// Creates a shallow clone of the specified object, reusing any referenced objects - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A shallow clone of the provided object - /// - /// - /// A property-based clone is useful if you're using dynamically generated proxies, - /// such as when working with entities returned by an ORM like NHibernate. - /// When not using a property-based clone, internal proxy fields would be cloned - /// and might cause problems with the ORM. - /// - /// - /// Property-based clones require a default constructor because there's no guarantee - /// that all fields will are assignable through properties and starting with - /// an uninitialized object is likely to end up with a broken clone. - /// - /// - TCloned ShallowPropertyClone(TCloned objectToClone); - - /// - /// Creates a deep clone of the specified object, also creating clones of all - /// child objects being referenced - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A deep clone of the provided object - /// - /// Field-based clones are guaranteed to be complete - there will be no missed - /// members. This type of clone is also able to clone types that do not provide - /// a default constructor. - /// - TCloned DeepFieldClone(TCloned objectToClone); - - /// - /// Creates a deep clone of the specified object, also creating clones of all - /// child objects being referenced - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A deep clone of the provided object - /// - /// - /// A property-based clone is useful if you're using dynamically generated proxies, - /// such as when working with entities returned by an ORM like NHibernate. - /// When not using a property-based clone, internal proxy fields would be cloned - /// and might cause problems with the ORM. - /// - /// - /// Property-based clones require a default constructor because there's no guarantee - /// that all fields will are assignable through properties and starting with - /// an uninitialized object is likely to end up with a broken clone. - /// - /// - TCloned DeepPropertyClone(TCloned objectToClone); - - } - -} // namespace Nuclex.Support.Cloning +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +namespace Nuclex.Support.Cloning { + + /// Constructs new objects by cloning existing objects + public interface ICloneFactory { + + /// + /// Creates a shallow clone of the specified object, reusing any referenced objects + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A shallow clone of the provided object + /// + /// Field-based clones are guaranteed to be complete - there will be no missed + /// members. This type of clone is also able to clone types that do not provide + /// a default constructor. + /// + TCloned ShallowFieldClone(TCloned objectToClone); + + /// + /// Creates a shallow clone of the specified object, reusing any referenced objects + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A shallow clone of the provided object + /// + /// + /// A property-based clone is useful if you're using dynamically generated proxies, + /// such as when working with entities returned by an ORM like NHibernate. + /// When not using a property-based clone, internal proxy fields would be cloned + /// and might cause problems with the ORM. + /// + /// + /// Property-based clones require a default constructor because there's no guarantee + /// that all fields will are assignable through properties and starting with + /// an uninitialized object is likely to end up with a broken clone. + /// + /// + TCloned ShallowPropertyClone(TCloned objectToClone); + + /// + /// Creates a deep clone of the specified object, also creating clones of all + /// child objects being referenced + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A deep clone of the provided object + /// + /// Field-based clones are guaranteed to be complete - there will be no missed + /// members. This type of clone is also able to clone types that do not provide + /// a default constructor. + /// + TCloned DeepFieldClone(TCloned objectToClone); + + /// + /// Creates a deep clone of the specified object, also creating clones of all + /// child objects being referenced + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A deep clone of the provided object + /// + /// + /// A property-based clone is useful if you're using dynamically generated proxies, + /// such as when working with entities returned by an ORM like NHibernate. + /// When not using a property-based clone, internal proxy fields would be cloned + /// and might cause problems with the ORM. + /// + /// + /// Property-based clones require a default constructor because there's no guarantee + /// that all fields will are assignable through properties and starting with + /// an uninitialized object is likely to end up with a broken clone. + /// + /// + TCloned DeepPropertyClone(TCloned objectToClone); + + } + +} // namespace Nuclex.Support.Cloning diff --git a/Source/Cloning/IStateCopier.cs b/Source/Cloning/IStateCopier.cs index 0ab20dc..29bf91c 100644 --- a/Source/Cloning/IStateCopier.cs +++ b/Source/Cloning/IStateCopier.cs @@ -1,90 +1,89 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; - -namespace Nuclex.Support.Cloning { - - /// Copies the state of objects - public interface IStateCopier { - - /// - /// Transfers the state of one object into another, creating clones of referenced objects - /// - /// Type of the object whose sate will be transferred - /// Original instance the state will be taken from - /// Target instance the state will be written to - /// Whether to perform a property-based state copy - /// - /// A property-based copy is useful if you're using dynamically generated proxies, - /// such as when working with entities returned by an ORM like NHibernate. - /// When not using a property-based copy, internal proxy fields would be copied - /// and might cause problems with the ORM. - /// - void DeepCopyState(TState original, TState target, bool propertyBased) - where TState : class; - - /// - /// Transfers the state of one object into another, creating clones of referenced objects - /// - /// Type of the object whose sate will be transferred - /// Original instance the state will be taken from - /// Target instance the state will be written to - /// Whether to perform a property-based state copy - /// - /// A property-based copy is useful if you're using dynamically generated proxies, - /// such as when working with entities returned by an ORM like NHibernate. - /// When not using a property-based copy, internal proxy fields would be copied - /// and might cause problems with the ORM. - /// - void DeepCopyState(ref TState original, ref TState target, bool propertyBased) - where TState : struct; - - /// Transfers the state of one object into another - /// Type of the object whose sate will be transferred - /// Original instance the state will be taken from - /// Target instance the state will be written to - /// Whether to perform a property-based state copy - /// - /// A property-based copy is useful if you're using dynamically generated proxies, - /// such as when working with entities returned by an ORM like NHibernate. - /// When not using a property-based copy, internal proxy fields would be copied - /// and might cause problems with the ORM. - /// - void ShallowCopyState(TState original, TState target, bool propertyBased) - where TState : class; - - /// Transfers the state of one object into another - /// Type of the object whose sate will be transferred - /// Original instance the state will be taken from - /// Target instance the state will be written to - /// Whether to perform a property-based state copy - /// - /// A property-based copy is useful if you're using dynamically generated proxies, - /// such as when working with entities returned by an ORM like NHibernate. - /// When not using a property-based copy, internal proxy fields would be copied - /// and might cause problems with the ORM. - /// - void ShallowCopyState(ref TState original, ref TState target, bool propertyBased) - where TState : struct; - - } - -} // namespace Nuclex.Support.Cloning +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +namespace Nuclex.Support.Cloning { + + /// Copies the state of objects + public interface IStateCopier { + + /// + /// Transfers the state of one object into another, creating clones of referenced objects + /// + /// Type of the object whose sate will be transferred + /// Original instance the state will be taken from + /// Target instance the state will be written to + /// Whether to perform a property-based state copy + /// + /// A property-based copy is useful if you're using dynamically generated proxies, + /// such as when working with entities returned by an ORM like NHibernate. + /// When not using a property-based copy, internal proxy fields would be copied + /// and might cause problems with the ORM. + /// + void DeepCopyState(TState original, TState target, bool propertyBased) + where TState : class; + + /// + /// Transfers the state of one object into another, creating clones of referenced objects + /// + /// Type of the object whose sate will be transferred + /// Original instance the state will be taken from + /// Target instance the state will be written to + /// Whether to perform a property-based state copy + /// + /// A property-based copy is useful if you're using dynamically generated proxies, + /// such as when working with entities returned by an ORM like NHibernate. + /// When not using a property-based copy, internal proxy fields would be copied + /// and might cause problems with the ORM. + /// + void DeepCopyState(ref TState original, ref TState target, bool propertyBased) + where TState : struct; + + /// Transfers the state of one object into another + /// Type of the object whose sate will be transferred + /// Original instance the state will be taken from + /// Target instance the state will be written to + /// Whether to perform a property-based state copy + /// + /// A property-based copy is useful if you're using dynamically generated proxies, + /// such as when working with entities returned by an ORM like NHibernate. + /// When not using a property-based copy, internal proxy fields would be copied + /// and might cause problems with the ORM. + /// + void ShallowCopyState(TState original, TState target, bool propertyBased) + where TState : class; + + /// Transfers the state of one object into another + /// Type of the object whose sate will be transferred + /// Original instance the state will be taken from + /// Target instance the state will be written to + /// Whether to perform a property-based state copy + /// + /// A property-based copy is useful if you're using dynamically generated proxies, + /// such as when working with entities returned by an ORM like NHibernate. + /// When not using a property-based copy, internal proxy fields would be copied + /// and might cause problems with the ORM. + /// + void ShallowCopyState(ref TState original, ref TState target, bool propertyBased) + where TState : struct; + + } + +} // namespace Nuclex.Support.Cloning diff --git a/Source/Cloning/ReflectionCloner.Test.cs b/Source/Cloning/ReflectionCloner.Test.cs index 657b642..2bd7acf 100644 --- a/Source/Cloning/ReflectionCloner.Test.cs +++ b/Source/Cloning/ReflectionCloner.Test.cs @@ -1,199 +1,198 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.Collections.Generic; - -using NUnit.Framework; - -namespace Nuclex.Support.Cloning { - - /// Unit Test for the reflection-based cloner - [TestFixture] - internal class ReflectionClonerTest : CloneFactoryTest { - - /// Initializes a new unit test suite for the reflection cloner - public ReflectionClonerTest() { - this.cloneFactory = new ReflectionCloner(); - } - - /// Verifies that cloning a null object simply returns null - [Test] - public void CloningNullYieldsNull() { - Assert.IsNull(this.cloneFactory.DeepFieldClone(null)); - Assert.IsNull(this.cloneFactory.DeepPropertyClone(null)); - Assert.IsNull(this.cloneFactory.ShallowFieldClone(null)); - Assert.IsNull(this.cloneFactory.ShallowPropertyClone(null)); - } - - /// - /// Verifies that clones of objects whose class doesn't possess a default constructor - /// can be made - /// - [Test] - public void ClassWithoutDefaultConstructorCanBeCloned() { - var original = new ClassWithoutDefaultConstructor(1234); - ClassWithoutDefaultConstructor clone = this.cloneFactory.DeepFieldClone(original); - - Assert.AreNotSame(original, clone); - Assert.AreEqual(original.Dummy, clone.Dummy); - } - - /// Verifies that clones of primitive types can be created - [Test] - public void PrimitiveTypesCanBeCloned() { - int original = 12345; - int clone = this.cloneFactory.ShallowFieldClone(original); - Assert.AreEqual(original, clone); - } - - /// Verifies that shallow clones of arrays can be made - [Test] - public void ShallowClonesOfArraysCanBeMade() { - var original = new TestReferenceType[] { - new TestReferenceType() { TestField = 123, TestProperty = 456 } - }; - TestReferenceType[] clone = this.cloneFactory.ShallowFieldClone(original); - - Assert.AreSame(original[0], clone[0]); - } - - /// Verifies that deep clones of arrays can be made - [Test] - public void DeepClonesOfArraysCanBeMade() { - var original = new TestReferenceType[] { - new TestReferenceType() { TestField = 123, TestProperty = 456 } - }; - TestReferenceType[] clone = this.cloneFactory.DeepFieldClone(original); - - Assert.AreNotSame(original[0], clone[0]); - Assert.AreEqual(original[0].TestField, clone[0].TestField); - Assert.AreEqual(original[0].TestProperty, clone[0].TestProperty); - } - - /// Verifies that deep clones of a generic list can be made - [Test] - public void GenericListsCanBeCloned() { - var original = new List(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }); - List clone = this.cloneFactory.DeepFieldClone(original); - - CollectionAssert.AreEqual(original, clone); - } - - /// Verifies that deep clones of a generic dictionary can be made - [Test] - public void GenericDictionariesCanBeCloned() { - var original = new Dictionary(); - original.Add(1, "one"); - Dictionary clone = this.cloneFactory.DeepFieldClone(original); - - Assert.AreEqual("one", clone[1]); - } - - /// - /// Verifies that a field-based shallow clone of a value type can be performed - /// - [Test] - public void ShallowFieldBasedClonesOfValueTypesCanBeMade() { - HierarchicalValueType original = CreateValueType(); - HierarchicalValueType clone = this.cloneFactory.ShallowFieldClone(original); - VerifyClone(ref original, ref clone, isDeepClone: false, isPropertyBasedClone: false); - } - - /// - /// Verifies that a field-based shallow clone of a reference type can be performed - /// - [Test] - public void ShallowFieldBasedClonesOfReferenceTypesCanBeMade() { - HierarchicalReferenceType original = CreateReferenceType(); - HierarchicalReferenceType clone = this.cloneFactory.ShallowFieldClone(original); - VerifyClone(original, clone, isDeepClone: false, isPropertyBasedClone: false); - } - - /// - /// Verifies that a field-based deep clone of a value type can be performed - /// - [Test] - public void DeepFieldBasedClonesOfValueTypesCanBeMade() { - HierarchicalValueType original = CreateValueType(); - HierarchicalValueType clone = this.cloneFactory.DeepFieldClone(original); - VerifyClone(ref original, ref clone, isDeepClone: true, isPropertyBasedClone: false); - } - - /// - /// Verifies that a field-based deep clone of a reference type can be performed - /// - [Test] - public void DeepFieldBasedClonesOfReferenceTypesCanBeMade() { - HierarchicalReferenceType original = CreateReferenceType(); - HierarchicalReferenceType clone = this.cloneFactory.DeepFieldClone(original); - VerifyClone(original, clone, isDeepClone: true, isPropertyBasedClone: false); - } - - /// - /// Verifies that a property-based shallow clone of a value type can be performed - /// - [Test] - public void ShallowPropertyBasedClonesOfValueTypesCanBeMade() { - HierarchicalValueType original = CreateValueType(); - HierarchicalValueType clone = this.cloneFactory.ShallowPropertyClone(original); - VerifyClone(ref original, ref clone, isDeepClone: false, isPropertyBasedClone: true); - } - - /// - /// Verifies that a property-based shallow clone of a reference type can be performed - /// - [Test] - public void ShallowPropertyBasedClonesOfReferenceTypesCanBeMade() { - HierarchicalReferenceType original = CreateReferenceType(); - HierarchicalReferenceType clone = this.cloneFactory.ShallowPropertyClone(original); - VerifyClone(original, clone, isDeepClone: false, isPropertyBasedClone: true); - } - - /// - /// Verifies that a property-based deep clone of a value type can be performed - /// - [Test] - public void DeepPropertyBasedClonesOfValueTypesCanBeMade() { - HierarchicalValueType original = CreateValueType(); - HierarchicalValueType clone = this.cloneFactory.DeepPropertyClone(original); - VerifyClone(ref original, ref clone, isDeepClone: true, isPropertyBasedClone: true); - } - - /// - /// Verifies that a property-based deep clone of a reference type can be performed - /// - [Test] - public void DeepPropertyBasedClonesOfReferenceTypesCanBeMade() { - HierarchicalReferenceType original = CreateReferenceType(); - HierarchicalReferenceType clone = this.cloneFactory.DeepPropertyClone(original); - VerifyClone(original, clone, isDeepClone: true, isPropertyBasedClone: true); - } - - /// Clone factory being tested - private ICloneFactory cloneFactory; - - } - -} // namespace Nuclex.Support.Cloning - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.Collections.Generic; + +using NUnit.Framework; + +namespace Nuclex.Support.Cloning { + + /// Unit Test for the reflection-based cloner + [TestFixture] + internal class ReflectionClonerTest : CloneFactoryTest { + + /// Initializes a new unit test suite for the reflection cloner + public ReflectionClonerTest() { + this.cloneFactory = new ReflectionCloner(); + } + + /// Verifies that cloning a null object simply returns null + [Test] + public void CloningNullYieldsNull() { + Assert.IsNull(this.cloneFactory.DeepFieldClone(null)); + Assert.IsNull(this.cloneFactory.DeepPropertyClone(null)); + Assert.IsNull(this.cloneFactory.ShallowFieldClone(null)); + Assert.IsNull(this.cloneFactory.ShallowPropertyClone(null)); + } + + /// + /// Verifies that clones of objects whose class doesn't possess a default constructor + /// can be made + /// + [Test] + public void ClassWithoutDefaultConstructorCanBeCloned() { + var original = new ClassWithoutDefaultConstructor(1234); + ClassWithoutDefaultConstructor clone = this.cloneFactory.DeepFieldClone(original); + + Assert.AreNotSame(original, clone); + Assert.AreEqual(original.Dummy, clone.Dummy); + } + + /// Verifies that clones of primitive types can be created + [Test] + public void PrimitiveTypesCanBeCloned() { + int original = 12345; + int clone = this.cloneFactory.ShallowFieldClone(original); + Assert.AreEqual(original, clone); + } + + /// Verifies that shallow clones of arrays can be made + [Test] + public void ShallowClonesOfArraysCanBeMade() { + var original = new TestReferenceType[] { + new TestReferenceType() { TestField = 123, TestProperty = 456 } + }; + TestReferenceType[] clone = this.cloneFactory.ShallowFieldClone(original); + + Assert.AreSame(original[0], clone[0]); + } + + /// Verifies that deep clones of arrays can be made + [Test] + public void DeepClonesOfArraysCanBeMade() { + var original = new TestReferenceType[] { + new TestReferenceType() { TestField = 123, TestProperty = 456 } + }; + TestReferenceType[] clone = this.cloneFactory.DeepFieldClone(original); + + Assert.AreNotSame(original[0], clone[0]); + Assert.AreEqual(original[0].TestField, clone[0].TestField); + Assert.AreEqual(original[0].TestProperty, clone[0].TestProperty); + } + + /// Verifies that deep clones of a generic list can be made + [Test] + public void GenericListsCanBeCloned() { + var original = new List(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }); + List clone = this.cloneFactory.DeepFieldClone(original); + + CollectionAssert.AreEqual(original, clone); + } + + /// Verifies that deep clones of a generic dictionary can be made + [Test] + public void GenericDictionariesCanBeCloned() { + var original = new Dictionary(); + original.Add(1, "one"); + Dictionary clone = this.cloneFactory.DeepFieldClone(original); + + Assert.AreEqual("one", clone[1]); + } + + /// + /// Verifies that a field-based shallow clone of a value type can be performed + /// + [Test] + public void ShallowFieldBasedClonesOfValueTypesCanBeMade() { + HierarchicalValueType original = CreateValueType(); + HierarchicalValueType clone = this.cloneFactory.ShallowFieldClone(original); + VerifyClone(ref original, ref clone, isDeepClone: false, isPropertyBasedClone: false); + } + + /// + /// Verifies that a field-based shallow clone of a reference type can be performed + /// + [Test] + public void ShallowFieldBasedClonesOfReferenceTypesCanBeMade() { + HierarchicalReferenceType original = CreateReferenceType(); + HierarchicalReferenceType clone = this.cloneFactory.ShallowFieldClone(original); + VerifyClone(original, clone, isDeepClone: false, isPropertyBasedClone: false); + } + + /// + /// Verifies that a field-based deep clone of a value type can be performed + /// + [Test] + public void DeepFieldBasedClonesOfValueTypesCanBeMade() { + HierarchicalValueType original = CreateValueType(); + HierarchicalValueType clone = this.cloneFactory.DeepFieldClone(original); + VerifyClone(ref original, ref clone, isDeepClone: true, isPropertyBasedClone: false); + } + + /// + /// Verifies that a field-based deep clone of a reference type can be performed + /// + [Test] + public void DeepFieldBasedClonesOfReferenceTypesCanBeMade() { + HierarchicalReferenceType original = CreateReferenceType(); + HierarchicalReferenceType clone = this.cloneFactory.DeepFieldClone(original); + VerifyClone(original, clone, isDeepClone: true, isPropertyBasedClone: false); + } + + /// + /// Verifies that a property-based shallow clone of a value type can be performed + /// + [Test] + public void ShallowPropertyBasedClonesOfValueTypesCanBeMade() { + HierarchicalValueType original = CreateValueType(); + HierarchicalValueType clone = this.cloneFactory.ShallowPropertyClone(original); + VerifyClone(ref original, ref clone, isDeepClone: false, isPropertyBasedClone: true); + } + + /// + /// Verifies that a property-based shallow clone of a reference type can be performed + /// + [Test] + public void ShallowPropertyBasedClonesOfReferenceTypesCanBeMade() { + HierarchicalReferenceType original = CreateReferenceType(); + HierarchicalReferenceType clone = this.cloneFactory.ShallowPropertyClone(original); + VerifyClone(original, clone, isDeepClone: false, isPropertyBasedClone: true); + } + + /// + /// Verifies that a property-based deep clone of a value type can be performed + /// + [Test] + public void DeepPropertyBasedClonesOfValueTypesCanBeMade() { + HierarchicalValueType original = CreateValueType(); + HierarchicalValueType clone = this.cloneFactory.DeepPropertyClone(original); + VerifyClone(ref original, ref clone, isDeepClone: true, isPropertyBasedClone: true); + } + + /// + /// Verifies that a property-based deep clone of a reference type can be performed + /// + [Test] + public void DeepPropertyBasedClonesOfReferenceTypesCanBeMade() { + HierarchicalReferenceType original = CreateReferenceType(); + HierarchicalReferenceType clone = this.cloneFactory.DeepPropertyClone(original); + VerifyClone(original, clone, isDeepClone: true, isPropertyBasedClone: true); + } + + /// Clone factory being tested + private ICloneFactory cloneFactory; + + } + +} // namespace Nuclex.Support.Cloning + +#endif // UNITTEST diff --git a/Source/Cloning/ReflectionCloner.cs b/Source/Cloning/ReflectionCloner.cs index 86b395b..fea863b 100644 --- a/Source/Cloning/ReflectionCloner.cs +++ b/Source/Cloning/ReflectionCloner.cs @@ -1,451 +1,450 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Reflection; -using System.Runtime.Serialization; - -namespace Nuclex.Support.Cloning { - - /// Clones objects using reflection - /// - /// - /// This type of cloning is a lot faster than cloning by serialization and - /// incurs no set-up cost, but requires cloned types to provide a default - /// constructor in order to work. - /// - /// - public class ReflectionCloner : ICloneFactory { - - /// - /// Creates a shallow clone of the specified object, reusing any referenced objects - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A shallow clone of the provided object - public static TCloned ShallowFieldClone(TCloned objectToClone) { - Type originalType = objectToClone.GetType(); - if(originalType.IsPrimitive || (originalType == typeof(string))) { - return objectToClone; // Being value types, primitives are copied by default - } else if(originalType.IsArray) { - return (TCloned)shallowCloneArray(objectToClone); - } else if(originalType.IsValueType) { - return objectToClone; // Value types can be copied directly - } else { - return (TCloned)shallowCloneComplexFieldBased(objectToClone); - } - } - - /// - /// Creates a shallow clone of the specified object, reusing any referenced objects - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A shallow clone of the provided object - public static TCloned ShallowPropertyClone(TCloned objectToClone) { - Type originalType = objectToClone.GetType(); - if(originalType.IsPrimitive || (originalType == typeof(string))) { - return objectToClone; // Being value types, primitives are copied by default - } else if(originalType.IsArray) { - return (TCloned)shallowCloneArray(objectToClone); - } else if(originalType.IsValueType) { - return (TCloned)shallowCloneComplexPropertyBased(objectToClone); - } else { - return (TCloned)shallowCloneComplexPropertyBased(objectToClone); - } - } - - /// - /// Creates a deep clone of the specified object, also creating clones of all - /// child objects being referenced - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A deep clone of the provided object - public static TCloned DeepFieldClone(TCloned objectToClone) { - object objectToCloneAsObject = objectToClone; - if(objectToClone == null) { - return default(TCloned); - } else { - return (TCloned)deepCloneSingleFieldBased(objectToCloneAsObject); - } - } - - /// - /// Creates a deep clone of the specified object, also creating clones of all - /// child objects being referenced - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A deep clone of the provided object - public static TCloned DeepPropertyClone(TCloned objectToClone) { - object objectToCloneAsObject = objectToClone; - if(objectToClone == null) { - return default(TCloned); - } else { - return (TCloned)deepCloneSinglePropertyBased(objectToCloneAsObject); - } - } - - /// - /// Creates a shallow clone of the specified object, reusing any referenced objects - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A shallow clone of the provided object - TCloned ICloneFactory.ShallowFieldClone(TCloned objectToClone) { - if(typeof(TCloned).IsClass || typeof(TCloned).IsArray) { - if(ReferenceEquals(objectToClone, null)) { - return default(TCloned); - } - } - return ReflectionCloner.ShallowFieldClone(objectToClone); - } - - /// - /// Creates a shallow clone of the specified object, reusing any referenced objects - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A shallow clone of the provided object - TCloned ICloneFactory.ShallowPropertyClone(TCloned objectToClone) { - if(typeof(TCloned).IsClass || typeof(TCloned).IsArray) { - if(ReferenceEquals(objectToClone, null)) { - return default(TCloned); - } - } - return ReflectionCloner.ShallowPropertyClone(objectToClone); - } - - /// - /// Creates a deep clone of the specified object, also creating clones of all - /// child objects being referenced - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A deep clone of the provided object - TCloned ICloneFactory.DeepFieldClone(TCloned objectToClone) { - return ReflectionCloner.DeepFieldClone(objectToClone); - } - - /// - /// Creates a deep clone of the specified object, also creating clones of all - /// child objects being referenced - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A deep clone of the provided object - TCloned ICloneFactory.DeepPropertyClone(TCloned objectToClone) { - return ReflectionCloner.DeepPropertyClone(objectToClone); - } - - /// Clones a complex type using field-based value transfer - /// Original instance that will be cloned - /// A clone of the original instance - private static object shallowCloneComplexFieldBased(object original) { - Type originalType = original.GetType(); - object clone = FormatterServices.GetUninitializedObject(originalType); - - FieldInfo[] fieldInfos = originalType.GetFieldInfosIncludingBaseClasses( - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance - ); - for(int index = 0; index < fieldInfos.Length; ++index) { - FieldInfo fieldInfo = fieldInfos[index]; - object originalValue = fieldInfo.GetValue(original); - if(originalValue != null) { - // Everything's just directly assigned in a shallow clone - fieldInfo.SetValue(clone, originalValue); - } - } - - return clone; - } - - /// Clones a complex type using property-based value transfer - /// Original instance that will be cloned - /// A clone of the original instance - private static object shallowCloneComplexPropertyBased(object original) { - Type originalType = original.GetType(); - object clone = Activator.CreateInstance(originalType); - - PropertyInfo[] propertyInfos = originalType.GetProperties( - BindingFlags.Public | BindingFlags.NonPublic | - BindingFlags.Instance | BindingFlags.FlattenHierarchy - ); - for(int index = 0; index < propertyInfos.Length; ++index) { - PropertyInfo propertyInfo = propertyInfos[index]; - if(propertyInfo.CanRead && propertyInfo.CanWrite) { - Type propertyType = propertyInfo.PropertyType; - object originalValue = propertyInfo.GetValue(original, null); - if(originalValue != null) { - if(propertyType.IsPrimitive || (propertyType == typeof(string))) { - // Primitive types can be assigned directly - propertyInfo.SetValue(clone, originalValue, null); - } else if(propertyType.IsValueType) { - // Value types are seen as part of the original type and are thus recursed into - propertyInfo.SetValue(clone, shallowCloneComplexPropertyBased(originalValue), null); - } else if(propertyType.IsArray) { // Arrays are assigned directly in a shallow clone - propertyInfo.SetValue(clone, originalValue, null); - } else { // Complex types are directly assigned without creating a copy - propertyInfo.SetValue(clone, originalValue, null); - } - } - } - } - - return clone; - } - - /// Clones an array using field-based value transfer - /// Original array that will be cloned - /// A clone of the original array - private static object shallowCloneArray(object original) { - return ((Array)original).Clone(); - } - - /// Copies a single object using field-based value transfer - /// Original object that will be cloned - /// A clone of the original object - private static object deepCloneSingleFieldBased(object original) { - Type originalType = original.GetType(); - if(originalType.IsPrimitive || (originalType == typeof(string))) { - return original; // Creates another box, does not reference boxed primitive - } else if(originalType.IsArray) { - return deepCloneArrayFieldBased((Array)original, originalType.GetElementType()); - } else { - return deepCloneComplexFieldBased(original); - } - } - - /// Clones a complex type using field-based value transfer - /// Original instance that will be cloned - /// A clone of the original instance - private static object deepCloneComplexFieldBased(object original) { - Type originalType = original.GetType(); - object clone = FormatterServices.GetUninitializedObject(originalType); - - FieldInfo[] fieldInfos = originalType.GetFieldInfosIncludingBaseClasses( - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance - ); - for(int index = 0; index < fieldInfos.Length; ++index) { - FieldInfo fieldInfo = fieldInfos[index]; - Type fieldType = fieldInfo.FieldType; - object originalValue = fieldInfo.GetValue(original); - if(originalValue != null) { - // Primitive types can be assigned directly - if(fieldType.IsPrimitive || (fieldType == typeof(string))) { - fieldInfo.SetValue(clone, originalValue); - } else if(fieldType.IsArray) { // Arrays need to be cloned element-by-element - fieldInfo.SetValue( - clone, - deepCloneArrayFieldBased((Array)originalValue, fieldType.GetElementType()) - ); - } else { // Complex types need to be cloned member-by-member - fieldInfo.SetValue(clone, deepCloneSingleFieldBased(originalValue)); - } - } - } - - return clone; - } - - /// Clones an array using field-based value transfer - /// Original array that will be cloned - /// Type of elements the original array contains - /// A clone of the original array - private static object deepCloneArrayFieldBased(Array original, Type elementType) { - if(elementType.IsPrimitive || (elementType == typeof(string))) { - return original.Clone(); - } - - int dimensionCount = original.Rank; - - // Find out the length of each of the array's dimensions, also calculate how - // many elements there are in the array in total. - var lengths = new int[dimensionCount]; - int totalElementCount = 0; - for(int index = 0; index < dimensionCount; ++index) { - lengths[index] = original.GetLength(index); - if(index == 0) { - totalElementCount = lengths[index]; - } else { - totalElementCount *= lengths[index]; - } - } - - // Knowing the number of dimensions and the length of each dimension, we can - // create another array of the exact same sizes. - Array clone = Array.CreateInstance(elementType, lengths); - - // If this is a one-dimensional array (most common type), do an optimized copy - // directly specifying the indices - if(dimensionCount == 1) { - - // Clone each element of the array directly - for(int index = 0; index < totalElementCount; ++index) { - object originalElement = original.GetValue(index); - if(originalElement != null) { - clone.SetValue(deepCloneSingleFieldBased(originalElement), index); - } - } - - } else { // Otherwise use the generic code for multi-dimensional arrays - - var indices = new int[dimensionCount]; - for(int index = 0; index < totalElementCount; ++index) { - - // Determine the index for each of the array's dimensions - int elementIndex = index; - for(int dimensionIndex = dimensionCount - 1; dimensionIndex >= 0; --dimensionIndex) { - indices[dimensionIndex] = elementIndex % lengths[dimensionIndex]; - elementIndex /= lengths[dimensionIndex]; - } - - // Clone the current array element - object originalElement = original.GetValue(indices); - if(originalElement != null) { - clone.SetValue(deepCloneSingleFieldBased(originalElement), indices); - } - - } - - } - - return clone; - } - - /// Copies a single object using property-based value transfer - /// Original object that will be cloned - /// A clone of the original object - private static object deepCloneSinglePropertyBased(object original) { - Type originalType = original.GetType(); - if(originalType.IsPrimitive || (originalType == typeof(string))) { - return original; // Creates another box, does not reference boxed primitive - } else if(originalType.IsArray) { - return deepCloneArrayPropertyBased((Array)original, originalType.GetElementType()); - } else { - return deepCloneComplexPropertyBased(original); - } - } - - /// Clones a complex type using property-based value transfer - /// Original instance that will be cloned - /// A clone of the original instance - private static object deepCloneComplexPropertyBased(object original) { - Type originalType = original.GetType(); - object clone = Activator.CreateInstance(originalType); - - PropertyInfo[] propertyInfos = originalType.GetProperties( - BindingFlags.Public | BindingFlags.NonPublic | - BindingFlags.Instance | BindingFlags.FlattenHierarchy - ); - for(int index = 0; index < propertyInfos.Length; ++index) { - PropertyInfo propertyInfo = propertyInfos[index]; - if(propertyInfo.CanRead && propertyInfo.CanWrite) { - Type propertyType = propertyInfo.PropertyType; - object originalValue = propertyInfo.GetValue(original, null); - if(originalValue != null) { - if(propertyType.IsPrimitive || (propertyType == typeof(string))) { - // Primitive types can be assigned directly - propertyInfo.SetValue(clone, originalValue, null); - } else if(propertyType.IsArray) { // Arrays need to be cloned element-by-element - propertyInfo.SetValue( - clone, - deepCloneArrayPropertyBased((Array)originalValue, propertyType.GetElementType()), - null - ); - } else { // Complex types need to be cloned member-by-member - propertyInfo.SetValue(clone, deepCloneSinglePropertyBased(originalValue), null); - } - } - } - } - - return clone; - } - - /// Clones an array using property-based value transfer - /// Original array that will be cloned - /// Type of elements the original array contains - /// A clone of the original array - private static object deepCloneArrayPropertyBased(Array original, Type elementType) { - if(elementType.IsPrimitive || (elementType == typeof(string))) { - return original.Clone(); - } - - int dimensionCount = original.Rank; - - // Find out the length of each of the array's dimensions, also calculate how - // many elements there are in the array in total. - var lengths = new int[dimensionCount]; - int totalElementCount = 0; - for(int index = 0; index < dimensionCount; ++index) { - lengths[index] = original.GetLength(index); - if(index == 0) { - totalElementCount = lengths[index]; - } else { - totalElementCount *= lengths[index]; - } - } - - // Knowing the number of dimensions and the length of each dimension, we can - // create another array of the exact same sizes. - Array clone = Array.CreateInstance(elementType, lengths); - - // If this is a one-dimensional array (most common type), do an optimized copy - // directly specifying the indices - if(dimensionCount == 1) { - - // Clone each element of the array directly - for(int index = 0; index < totalElementCount; ++index) { - object originalElement = original.GetValue(index); - if(originalElement != null) { - clone.SetValue(deepCloneSinglePropertyBased(originalElement), index); - } - } - - } else { // Otherwise use the generic code for multi-dimensional arrays - - var indices = new int[dimensionCount]; - for(int index = 0; index < totalElementCount; ++index) { - - // Determine the index for each of the array's dimensions - int elementIndex = index; - for(int dimensionIndex = dimensionCount - 1; dimensionIndex >= 0; --dimensionIndex) { - indices[dimensionIndex] = elementIndex % lengths[dimensionIndex]; - elementIndex /= lengths[dimensionIndex]; - } - - // Clone the current array element - object originalElement = original.GetValue(indices); - if(originalElement != null) { - clone.SetValue(deepCloneSinglePropertyBased(originalElement), indices); - } - - } - - } - - return clone; - } - - } - -} // namespace Nuclex.Support.Cloning +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Reflection; +using System.Runtime.Serialization; + +namespace Nuclex.Support.Cloning { + + /// Clones objects using reflection + /// + /// + /// This type of cloning is a lot faster than cloning by serialization and + /// incurs no set-up cost, but requires cloned types to provide a default + /// constructor in order to work. + /// + /// + public class ReflectionCloner : ICloneFactory { + + /// + /// Creates a shallow clone of the specified object, reusing any referenced objects + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A shallow clone of the provided object + public static TCloned ShallowFieldClone(TCloned objectToClone) { + Type originalType = objectToClone.GetType(); + if(originalType.IsPrimitive || (originalType == typeof(string))) { + return objectToClone; // Being value types, primitives are copied by default + } else if(originalType.IsArray) { + return (TCloned)shallowCloneArray(objectToClone); + } else if(originalType.IsValueType) { + return objectToClone; // Value types can be copied directly + } else { + return (TCloned)shallowCloneComplexFieldBased(objectToClone); + } + } + + /// + /// Creates a shallow clone of the specified object, reusing any referenced objects + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A shallow clone of the provided object + public static TCloned ShallowPropertyClone(TCloned objectToClone) { + Type originalType = objectToClone.GetType(); + if(originalType.IsPrimitive || (originalType == typeof(string))) { + return objectToClone; // Being value types, primitives are copied by default + } else if(originalType.IsArray) { + return (TCloned)shallowCloneArray(objectToClone); + } else if(originalType.IsValueType) { + return (TCloned)shallowCloneComplexPropertyBased(objectToClone); + } else { + return (TCloned)shallowCloneComplexPropertyBased(objectToClone); + } + } + + /// + /// Creates a deep clone of the specified object, also creating clones of all + /// child objects being referenced + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A deep clone of the provided object + public static TCloned DeepFieldClone(TCloned objectToClone) { + object objectToCloneAsObject = objectToClone; + if(objectToClone == null) { + return default(TCloned); + } else { + return (TCloned)deepCloneSingleFieldBased(objectToCloneAsObject); + } + } + + /// + /// Creates a deep clone of the specified object, also creating clones of all + /// child objects being referenced + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A deep clone of the provided object + public static TCloned DeepPropertyClone(TCloned objectToClone) { + object objectToCloneAsObject = objectToClone; + if(objectToClone == null) { + return default(TCloned); + } else { + return (TCloned)deepCloneSinglePropertyBased(objectToCloneAsObject); + } + } + + /// + /// Creates a shallow clone of the specified object, reusing any referenced objects + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A shallow clone of the provided object + TCloned ICloneFactory.ShallowFieldClone(TCloned objectToClone) { + if(typeof(TCloned).IsClass || typeof(TCloned).IsArray) { + if(ReferenceEquals(objectToClone, null)) { + return default(TCloned); + } + } + return ReflectionCloner.ShallowFieldClone(objectToClone); + } + + /// + /// Creates a shallow clone of the specified object, reusing any referenced objects + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A shallow clone of the provided object + TCloned ICloneFactory.ShallowPropertyClone(TCloned objectToClone) { + if(typeof(TCloned).IsClass || typeof(TCloned).IsArray) { + if(ReferenceEquals(objectToClone, null)) { + return default(TCloned); + } + } + return ReflectionCloner.ShallowPropertyClone(objectToClone); + } + + /// + /// Creates a deep clone of the specified object, also creating clones of all + /// child objects being referenced + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A deep clone of the provided object + TCloned ICloneFactory.DeepFieldClone(TCloned objectToClone) { + return ReflectionCloner.DeepFieldClone(objectToClone); + } + + /// + /// Creates a deep clone of the specified object, also creating clones of all + /// child objects being referenced + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A deep clone of the provided object + TCloned ICloneFactory.DeepPropertyClone(TCloned objectToClone) { + return ReflectionCloner.DeepPropertyClone(objectToClone); + } + + /// Clones a complex type using field-based value transfer + /// Original instance that will be cloned + /// A clone of the original instance + private static object shallowCloneComplexFieldBased(object original) { + Type originalType = original.GetType(); + object clone = FormatterServices.GetUninitializedObject(originalType); + + FieldInfo[] fieldInfos = originalType.GetFieldInfosIncludingBaseClasses( + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance + ); + for(int index = 0; index < fieldInfos.Length; ++index) { + FieldInfo fieldInfo = fieldInfos[index]; + object originalValue = fieldInfo.GetValue(original); + if(originalValue != null) { + // Everything's just directly assigned in a shallow clone + fieldInfo.SetValue(clone, originalValue); + } + } + + return clone; + } + + /// Clones a complex type using property-based value transfer + /// Original instance that will be cloned + /// A clone of the original instance + private static object shallowCloneComplexPropertyBased(object original) { + Type originalType = original.GetType(); + object clone = Activator.CreateInstance(originalType); + + PropertyInfo[] propertyInfos = originalType.GetProperties( + BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Instance | BindingFlags.FlattenHierarchy + ); + for(int index = 0; index < propertyInfos.Length; ++index) { + PropertyInfo propertyInfo = propertyInfos[index]; + if(propertyInfo.CanRead && propertyInfo.CanWrite) { + Type propertyType = propertyInfo.PropertyType; + object originalValue = propertyInfo.GetValue(original, null); + if(originalValue != null) { + if(propertyType.IsPrimitive || (propertyType == typeof(string))) { + // Primitive types can be assigned directly + propertyInfo.SetValue(clone, originalValue, null); + } else if(propertyType.IsValueType) { + // Value types are seen as part of the original type and are thus recursed into + propertyInfo.SetValue(clone, shallowCloneComplexPropertyBased(originalValue), null); + } else if(propertyType.IsArray) { // Arrays are assigned directly in a shallow clone + propertyInfo.SetValue(clone, originalValue, null); + } else { // Complex types are directly assigned without creating a copy + propertyInfo.SetValue(clone, originalValue, null); + } + } + } + } + + return clone; + } + + /// Clones an array using field-based value transfer + /// Original array that will be cloned + /// A clone of the original array + private static object shallowCloneArray(object original) { + return ((Array)original).Clone(); + } + + /// Copies a single object using field-based value transfer + /// Original object that will be cloned + /// A clone of the original object + private static object deepCloneSingleFieldBased(object original) { + Type originalType = original.GetType(); + if(originalType.IsPrimitive || (originalType == typeof(string))) { + return original; // Creates another box, does not reference boxed primitive + } else if(originalType.IsArray) { + return deepCloneArrayFieldBased((Array)original, originalType.GetElementType()); + } else { + return deepCloneComplexFieldBased(original); + } + } + + /// Clones a complex type using field-based value transfer + /// Original instance that will be cloned + /// A clone of the original instance + private static object deepCloneComplexFieldBased(object original) { + Type originalType = original.GetType(); + object clone = FormatterServices.GetUninitializedObject(originalType); + + FieldInfo[] fieldInfos = originalType.GetFieldInfosIncludingBaseClasses( + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance + ); + for(int index = 0; index < fieldInfos.Length; ++index) { + FieldInfo fieldInfo = fieldInfos[index]; + Type fieldType = fieldInfo.FieldType; + object originalValue = fieldInfo.GetValue(original); + if(originalValue != null) { + // Primitive types can be assigned directly + if(fieldType.IsPrimitive || (fieldType == typeof(string))) { + fieldInfo.SetValue(clone, originalValue); + } else if(fieldType.IsArray) { // Arrays need to be cloned element-by-element + fieldInfo.SetValue( + clone, + deepCloneArrayFieldBased((Array)originalValue, fieldType.GetElementType()) + ); + } else { // Complex types need to be cloned member-by-member + fieldInfo.SetValue(clone, deepCloneSingleFieldBased(originalValue)); + } + } + } + + return clone; + } + + /// Clones an array using field-based value transfer + /// Original array that will be cloned + /// Type of elements the original array contains + /// A clone of the original array + private static object deepCloneArrayFieldBased(Array original, Type elementType) { + if(elementType.IsPrimitive || (elementType == typeof(string))) { + return original.Clone(); + } + + int dimensionCount = original.Rank; + + // Find out the length of each of the array's dimensions, also calculate how + // many elements there are in the array in total. + var lengths = new int[dimensionCount]; + int totalElementCount = 0; + for(int index = 0; index < dimensionCount; ++index) { + lengths[index] = original.GetLength(index); + if(index == 0) { + totalElementCount = lengths[index]; + } else { + totalElementCount *= lengths[index]; + } + } + + // Knowing the number of dimensions and the length of each dimension, we can + // create another array of the exact same sizes. + Array clone = Array.CreateInstance(elementType, lengths); + + // If this is a one-dimensional array (most common type), do an optimized copy + // directly specifying the indices + if(dimensionCount == 1) { + + // Clone each element of the array directly + for(int index = 0; index < totalElementCount; ++index) { + object originalElement = original.GetValue(index); + if(originalElement != null) { + clone.SetValue(deepCloneSingleFieldBased(originalElement), index); + } + } + + } else { // Otherwise use the generic code for multi-dimensional arrays + + var indices = new int[dimensionCount]; + for(int index = 0; index < totalElementCount; ++index) { + + // Determine the index for each of the array's dimensions + int elementIndex = index; + for(int dimensionIndex = dimensionCount - 1; dimensionIndex >= 0; --dimensionIndex) { + indices[dimensionIndex] = elementIndex % lengths[dimensionIndex]; + elementIndex /= lengths[dimensionIndex]; + } + + // Clone the current array element + object originalElement = original.GetValue(indices); + if(originalElement != null) { + clone.SetValue(deepCloneSingleFieldBased(originalElement), indices); + } + + } + + } + + return clone; + } + + /// Copies a single object using property-based value transfer + /// Original object that will be cloned + /// A clone of the original object + private static object deepCloneSinglePropertyBased(object original) { + Type originalType = original.GetType(); + if(originalType.IsPrimitive || (originalType == typeof(string))) { + return original; // Creates another box, does not reference boxed primitive + } else if(originalType.IsArray) { + return deepCloneArrayPropertyBased((Array)original, originalType.GetElementType()); + } else { + return deepCloneComplexPropertyBased(original); + } + } + + /// Clones a complex type using property-based value transfer + /// Original instance that will be cloned + /// A clone of the original instance + private static object deepCloneComplexPropertyBased(object original) { + Type originalType = original.GetType(); + object clone = Activator.CreateInstance(originalType); + + PropertyInfo[] propertyInfos = originalType.GetProperties( + BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Instance | BindingFlags.FlattenHierarchy + ); + for(int index = 0; index < propertyInfos.Length; ++index) { + PropertyInfo propertyInfo = propertyInfos[index]; + if(propertyInfo.CanRead && propertyInfo.CanWrite) { + Type propertyType = propertyInfo.PropertyType; + object originalValue = propertyInfo.GetValue(original, null); + if(originalValue != null) { + if(propertyType.IsPrimitive || (propertyType == typeof(string))) { + // Primitive types can be assigned directly + propertyInfo.SetValue(clone, originalValue, null); + } else if(propertyType.IsArray) { // Arrays need to be cloned element-by-element + propertyInfo.SetValue( + clone, + deepCloneArrayPropertyBased((Array)originalValue, propertyType.GetElementType()), + null + ); + } else { // Complex types need to be cloned member-by-member + propertyInfo.SetValue(clone, deepCloneSinglePropertyBased(originalValue), null); + } + } + } + } + + return clone; + } + + /// Clones an array using property-based value transfer + /// Original array that will be cloned + /// Type of elements the original array contains + /// A clone of the original array + private static object deepCloneArrayPropertyBased(Array original, Type elementType) { + if(elementType.IsPrimitive || (elementType == typeof(string))) { + return original.Clone(); + } + + int dimensionCount = original.Rank; + + // Find out the length of each of the array's dimensions, also calculate how + // many elements there are in the array in total. + var lengths = new int[dimensionCount]; + int totalElementCount = 0; + for(int index = 0; index < dimensionCount; ++index) { + lengths[index] = original.GetLength(index); + if(index == 0) { + totalElementCount = lengths[index]; + } else { + totalElementCount *= lengths[index]; + } + } + + // Knowing the number of dimensions and the length of each dimension, we can + // create another array of the exact same sizes. + Array clone = Array.CreateInstance(elementType, lengths); + + // If this is a one-dimensional array (most common type), do an optimized copy + // directly specifying the indices + if(dimensionCount == 1) { + + // Clone each element of the array directly + for(int index = 0; index < totalElementCount; ++index) { + object originalElement = original.GetValue(index); + if(originalElement != null) { + clone.SetValue(deepCloneSinglePropertyBased(originalElement), index); + } + } + + } else { // Otherwise use the generic code for multi-dimensional arrays + + var indices = new int[dimensionCount]; + for(int index = 0; index < totalElementCount; ++index) { + + // Determine the index for each of the array's dimensions + int elementIndex = index; + for(int dimensionIndex = dimensionCount - 1; dimensionIndex >= 0; --dimensionIndex) { + indices[dimensionIndex] = elementIndex % lengths[dimensionIndex]; + elementIndex /= lengths[dimensionIndex]; + } + + // Clone the current array element + object originalElement = original.GetValue(indices); + if(originalElement != null) { + clone.SetValue(deepCloneSinglePropertyBased(originalElement), indices); + } + + } + + } + + return clone; + } + + } + +} // namespace Nuclex.Support.Cloning diff --git a/Source/Cloning/SerializationCloner.Test.cs b/Source/Cloning/SerializationCloner.Test.cs index 798d5a9..3a42e67 100644 --- a/Source/Cloning/SerializationCloner.Test.cs +++ b/Source/Cloning/SerializationCloner.Test.cs @@ -1,146 +1,145 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.Collections.Generic; - -using NUnit.Framework; - -namespace Nuclex.Support.Cloning { - - /// Unit Test for the binary serializer-based cloner - [TestFixture] - internal class SerializationClonerTest : CloneFactoryTest { - - /// Initializes a new unit test suite for the reflection cloner - public SerializationClonerTest() { - this.cloneFactory = new SerializationCloner(); - } - - /// Verifies that cloning a null object simply returns null - [Test] - public void CloningNullYieldsNull() { - Assert.IsNull(this.cloneFactory.DeepFieldClone(null)); - Assert.IsNull(this.cloneFactory.DeepPropertyClone(null)); - } - - /// - /// Verifies that clones of objects whose class doesn't possess a default constructor - /// can be made - /// - [Test] - public void ClassWithoutDefaultConstructorCanBeCloned() { - var original = new ClassWithoutDefaultConstructor(1234); - ClassWithoutDefaultConstructor clone = this.cloneFactory.DeepFieldClone(original); - - Assert.AreNotSame(original, clone); - Assert.AreEqual(original.Dummy, clone.Dummy); - } - - /// Verifies that clones of primitive types can be created - [Test] - public void PrimitiveTypesCanBeCloned() { - int original = 12345; - int clone = this.cloneFactory.DeepFieldClone(original); - Assert.AreEqual(original, clone); - } - - /// Verifies that deep clones of arrays can be made - [Test] - public void DeepClonesOfArraysCanBeMade() { - var original = new TestReferenceType[] { - new TestReferenceType() { TestField = 123, TestProperty = 456 } - }; - TestReferenceType[] clone = this.cloneFactory.DeepFieldClone(original); - - Assert.AreNotSame(original[0], clone[0]); - Assert.AreEqual(original[0].TestField, clone[0].TestField); - Assert.AreEqual(original[0].TestProperty, clone[0].TestProperty); - } - - /// Verifies that deep clones of a generic list can be made - [Test] - public void GenericListsCanBeCloned() { - var original = new List(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }); - List clone = this.cloneFactory.DeepFieldClone(original); - - CollectionAssert.AreEqual(original, clone); - } - - /// Verifies that deep clones of a generic dictionary can be made - [Test] - public void GenericDictionariesCanBeCloned() { - var original = new Dictionary(); - original.Add(1, "one"); - Dictionary clone = this.cloneFactory.DeepFieldClone(original); - - Assert.AreEqual("one", clone[1]); - } - - /// - /// Verifies that a field-based deep clone of a value type can be performed - /// - [Test] - public void DeepFieldBasedClonesOfValueTypesCanBeMade() { - HierarchicalValueType original = CreateValueType(); - HierarchicalValueType clone = this.cloneFactory.DeepFieldClone(original); - VerifyClone(ref original, ref clone, isDeepClone: true, isPropertyBasedClone: false); - } - - /// - /// Verifies that a field-based deep clone of a reference type can be performed - /// - [Test] - public void DeepFieldBasedClonesOfReferenceTypesCanBeMade() { - HierarchicalReferenceType original = CreateReferenceType(); - HierarchicalReferenceType clone = this.cloneFactory.DeepFieldClone(original); - VerifyClone(original, clone, isDeepClone: true, isPropertyBasedClone: false); - } - - /// - /// Verifies that a property-based deep clone of a value type can be performed - /// - [Test] - public void DeepPropertyBasedClonesOfValueTypesCanBeMade() { - HierarchicalValueType original = CreateValueType(); - HierarchicalValueType clone = this.cloneFactory.DeepPropertyClone(original); - VerifyClone(ref original, ref clone, isDeepClone: true, isPropertyBasedClone: true); - } - - /// - /// Verifies that a property-based deep clone of a reference type can be performed - /// - [Test] - public void DeepPropertyBasedClonesOfReferenceTypesCanBeMade() { - HierarchicalReferenceType original = CreateReferenceType(); - HierarchicalReferenceType clone = this.cloneFactory.DeepPropertyClone(original); - VerifyClone(original, clone, isDeepClone: true, isPropertyBasedClone: true); - } - - /// Clone factory being tested - private ICloneFactory cloneFactory; - - } - -} // namespace Nuclex.Support.Cloning - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.Collections.Generic; + +using NUnit.Framework; + +namespace Nuclex.Support.Cloning { + + /// Unit Test for the binary serializer-based cloner + [TestFixture] + internal class SerializationClonerTest : CloneFactoryTest { + + /// Initializes a new unit test suite for the reflection cloner + public SerializationClonerTest() { + this.cloneFactory = new SerializationCloner(); + } + + /// Verifies that cloning a null object simply returns null + [Test] + public void CloningNullYieldsNull() { + Assert.IsNull(this.cloneFactory.DeepFieldClone(null)); + Assert.IsNull(this.cloneFactory.DeepPropertyClone(null)); + } + + /// + /// Verifies that clones of objects whose class doesn't possess a default constructor + /// can be made + /// + [Test] + public void ClassWithoutDefaultConstructorCanBeCloned() { + var original = new ClassWithoutDefaultConstructor(1234); + ClassWithoutDefaultConstructor clone = this.cloneFactory.DeepFieldClone(original); + + Assert.AreNotSame(original, clone); + Assert.AreEqual(original.Dummy, clone.Dummy); + } + + /// Verifies that clones of primitive types can be created + [Test] + public void PrimitiveTypesCanBeCloned() { + int original = 12345; + int clone = this.cloneFactory.DeepFieldClone(original); + Assert.AreEqual(original, clone); + } + + /// Verifies that deep clones of arrays can be made + [Test] + public void DeepClonesOfArraysCanBeMade() { + var original = new TestReferenceType[] { + new TestReferenceType() { TestField = 123, TestProperty = 456 } + }; + TestReferenceType[] clone = this.cloneFactory.DeepFieldClone(original); + + Assert.AreNotSame(original[0], clone[0]); + Assert.AreEqual(original[0].TestField, clone[0].TestField); + Assert.AreEqual(original[0].TestProperty, clone[0].TestProperty); + } + + /// Verifies that deep clones of a generic list can be made + [Test] + public void GenericListsCanBeCloned() { + var original = new List(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }); + List clone = this.cloneFactory.DeepFieldClone(original); + + CollectionAssert.AreEqual(original, clone); + } + + /// Verifies that deep clones of a generic dictionary can be made + [Test] + public void GenericDictionariesCanBeCloned() { + var original = new Dictionary(); + original.Add(1, "one"); + Dictionary clone = this.cloneFactory.DeepFieldClone(original); + + Assert.AreEqual("one", clone[1]); + } + + /// + /// Verifies that a field-based deep clone of a value type can be performed + /// + [Test] + public void DeepFieldBasedClonesOfValueTypesCanBeMade() { + HierarchicalValueType original = CreateValueType(); + HierarchicalValueType clone = this.cloneFactory.DeepFieldClone(original); + VerifyClone(ref original, ref clone, isDeepClone: true, isPropertyBasedClone: false); + } + + /// + /// Verifies that a field-based deep clone of a reference type can be performed + /// + [Test] + public void DeepFieldBasedClonesOfReferenceTypesCanBeMade() { + HierarchicalReferenceType original = CreateReferenceType(); + HierarchicalReferenceType clone = this.cloneFactory.DeepFieldClone(original); + VerifyClone(original, clone, isDeepClone: true, isPropertyBasedClone: false); + } + + /// + /// Verifies that a property-based deep clone of a value type can be performed + /// + [Test] + public void DeepPropertyBasedClonesOfValueTypesCanBeMade() { + HierarchicalValueType original = CreateValueType(); + HierarchicalValueType clone = this.cloneFactory.DeepPropertyClone(original); + VerifyClone(ref original, ref clone, isDeepClone: true, isPropertyBasedClone: true); + } + + /// + /// Verifies that a property-based deep clone of a reference type can be performed + /// + [Test] + public void DeepPropertyBasedClonesOfReferenceTypesCanBeMade() { + HierarchicalReferenceType original = CreateReferenceType(); + HierarchicalReferenceType clone = this.cloneFactory.DeepPropertyClone(original); + VerifyClone(original, clone, isDeepClone: true, isPropertyBasedClone: true); + } + + /// Clone factory being tested + private ICloneFactory cloneFactory; + + } + +} // namespace Nuclex.Support.Cloning + +#endif // UNITTEST diff --git a/Source/Cloning/SerializationCloner.cs b/Source/Cloning/SerializationCloner.cs index fc76284..7fa5ccd 100644 --- a/Source/Cloning/SerializationCloner.cs +++ b/Source/Cloning/SerializationCloner.cs @@ -1,328 +1,327 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.IO; -using System.Reflection; -using System.Runtime.Serialization; -using System.Runtime.Serialization.Formatters.Binary; - -namespace Nuclex.Support.Cloning { - - /// Clones objects via serialization - /// - /// - /// This type of cloning uses the binary formatter to persist the state of - /// an object and then restores it into a clone. It has the advantage of even - /// working with types that don't provide a default constructor, but is - /// terribly slow. - /// - /// - /// Inspired by the "A Generic Method for Deep Cloning in C# 3.0" article - /// on CodeProject: http://www.codeproject.com/KB/cs/generic_deep_cloning.aspx - /// - /// - public class SerializationCloner : ICloneFactory { - - #region class StaticSurrogateSelector - - /// Selects a static surrogate for any non-primitive types - private class StaticSurrogateSelector : ISurrogateSelector { - - /// Initializes a new static surrogate selector - /// Surrogate that will be selected - public StaticSurrogateSelector(ISerializationSurrogate staticSurrogate) { - this.staticSurrogate = staticSurrogate; - } - - /// - /// Sets the next selector to escalate to if this one can't provide a surrogate - /// - /// Selector to escalate to - public void ChainSelector(ISurrogateSelector selector) { - this.chainedSelector = selector; - } - - /// - /// Returns the selector this one will escalate to if it can't provide a surrogate - /// - /// The selector this one will escalate to - public ISurrogateSelector GetNextSelector() { - return this.chainedSelector; - } - - /// Attempts to provides a surrogate for the specified type - /// Type a surrogate will be provided for - /// Context - /// - /// - public ISerializationSurrogate GetSurrogate( - Type type, StreamingContext context, out ISurrogateSelector selector - ) { - if(type.IsPrimitive || type.IsArray || (type == typeof(string))) { - if(this.chainedSelector == null) { - selector = null; - return null; - } else { - return this.chainedSelector.GetSurrogate(type, context, out selector); - } - } else { - selector = this; - return this.staticSurrogate; - } - } - - /// Surrogate the that will be selected for any non-primitive types - private readonly ISerializationSurrogate staticSurrogate; - /// Surrogate selector to escalate to if no surrogate can be provided - private ISurrogateSelector chainedSelector; - - } - - #endregion // class StaticSurrogateSelector - - #region class FieldSerializationSurrogate - - /// Serializes a type based on its fields - private class FieldSerializationSurrogate : ISerializationSurrogate { - - /// Extracts the data to be serialized from an object - /// Object that is being serialized - /// Stores the serialized informations - /// - /// Provides additional informations about the serialization process - /// - public void GetObjectData( - object objectToSerialize, - SerializationInfo info, - StreamingContext context - ) { - Type originalType = objectToSerialize.GetType(); - - FieldInfo[] fieldInfos = originalType.GetFieldInfosIncludingBaseClasses( - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance - ); - for(int index = 0; index < fieldInfos.Length; ++index) { - FieldInfo fieldInfo = fieldInfos[index]; - info.AddValue(fieldInfo.Name, fieldInfo.GetValue(objectToSerialize)); - } - } - - /// Reinserts saved data into a deserializd object - /// Object the saved data will be inserted into - /// Contains the serialized informations - /// - /// Provides additional informations about the serialization process - /// - /// Surrogate selector that specified this surrogate - /// The deserialized object - public object SetObjectData( - object deserializedObject, - SerializationInfo info, - StreamingContext context, - ISurrogateSelector selector - ) { - Type originalType = deserializedObject.GetType(); - - FieldInfo[] fieldInfos = originalType.GetFieldInfosIncludingBaseClasses( - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance - ); - for(int index = 0; index < fieldInfos.Length; ++index) { - FieldInfo fieldInfo = fieldInfos[index]; - fieldInfo.SetValue(deserializedObject, info.GetValue(fieldInfo.Name, fieldInfo.FieldType)); - } - - return deserializedObject; - } - - } - - #endregion // class FieldSerializationSurrogate - - #region class PropertySerializationSurrogate - - /// Serializes a type based on its properties - private class PropertySerializationSurrogate : ISerializationSurrogate { - - /// Extracts the data to be serialized from an object - /// Object that is being serialized - /// Stores the serialized informations - /// - /// Provides additional informations about the serialization process - /// - public void GetObjectData( - object objectToSerialize, - SerializationInfo info, - StreamingContext context - ) { - Type originalType = objectToSerialize.GetType(); - - PropertyInfo[] propertyInfos = originalType.GetProperties( - BindingFlags.Public | BindingFlags.NonPublic | - BindingFlags.Instance | BindingFlags.FlattenHierarchy - ); - for(int index = 0; index < propertyInfos.Length; ++index) { - PropertyInfo propertyInfo = propertyInfos[index]; - if(propertyInfo.CanRead && propertyInfo.CanWrite) { - info.AddValue(propertyInfo.Name, propertyInfo.GetValue(objectToSerialize, null)); - } - } - } - - /// Reinserts saved data into a deserializd object - /// Object the saved data will be inserted into - /// Contains the serialized informations - /// - /// Provides additional informations about the serialization process - /// - /// Surrogate selector that specified this surrogate - /// The deserialized object - public object SetObjectData( - object deserializedObject, - SerializationInfo info, - StreamingContext context, - ISurrogateSelector selector - ) { - Type originalType = deserializedObject.GetType(); - - PropertyInfo[] propertyInfos = originalType.GetProperties( - BindingFlags.Public | BindingFlags.NonPublic | - BindingFlags.Instance | BindingFlags.FlattenHierarchy - ); - for(int index = 0; index < propertyInfos.Length; ++index) { - PropertyInfo propertyInfo = propertyInfos[index]; - if(propertyInfo.CanRead && propertyInfo.CanWrite) { - propertyInfo.SetValue( - deserializedObject, - info.GetValue(propertyInfo.Name, propertyInfo.PropertyType), - null - ); - } - } - - return deserializedObject; - } - - } - - #endregion // class PropertySerializationSurrogate - - /// Initializes the static members of the serialization-based cloner - static SerializationCloner() { - fieldBasedFormatter = new BinaryFormatter( - new StaticSurrogateSelector(new FieldSerializationSurrogate()), - new StreamingContext(StreamingContextStates.All) - ); - propertyBasedFormatter = new BinaryFormatter( - new StaticSurrogateSelector(new PropertySerializationSurrogate()), - new StreamingContext(StreamingContextStates.All) - ); - } - - /// - /// Creates a deep clone of the specified object, also creating clones of all - /// child objects being referenced - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A deep clone of the provided object - public static TCloned DeepFieldClone(TCloned objectToClone) { - if(typeof(TCloned).IsClass || typeof(TCloned).IsArray) { - if(ReferenceEquals(objectToClone, null)) { - return default(TCloned); - } - } - using(var memoryStream = new MemoryStream()) { - fieldBasedFormatter.Serialize(memoryStream, objectToClone); - memoryStream.Position = 0; - return (TCloned)fieldBasedFormatter.Deserialize(memoryStream); - } - } - - /// - /// Creates a deep clone of the specified object, also creating clones of all - /// child objects being referenced - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A deep clone of the provided object - public static TCloned DeepPropertyClone(TCloned objectToClone) { - if(typeof(TCloned).IsClass || typeof(TCloned).IsArray) { - if(ReferenceEquals(objectToClone, null)) { - return default(TCloned); - } - } - using(var memoryStream = new MemoryStream()) { - propertyBasedFormatter.Serialize(memoryStream, objectToClone); - memoryStream.Position = 0; - return (TCloned)propertyBasedFormatter.Deserialize(memoryStream); - } - } - - /// - /// Creates a shallow clone of the specified object, reusing any referenced objects - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A shallow clone of the provided object - TCloned ICloneFactory.ShallowFieldClone(TCloned objectToClone) { - throw new NotSupportedException("The serialization cloner cannot create shallow clones"); - } - - /// - /// Creates a shallow clone of the specified object, reusing any referenced objects - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A shallow clone of the provided object - TCloned ICloneFactory.ShallowPropertyClone(TCloned objectToClone) { - throw new NotSupportedException("The serialization cloner cannot create shallow clones"); - } - - /// - /// Creates a deep clone of the specified object, also creating clones of all - /// child objects being referenced - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A deep clone of the provided object - TCloned ICloneFactory.DeepFieldClone(TCloned objectToClone) { - return SerializationCloner.DeepFieldClone(objectToClone); - } - - /// - /// Creates a deep clone of the specified object, also creating clones of all - /// child objects being referenced - /// - /// Type of the object that will be cloned - /// Object that will be cloned - /// A deep clone of the provided object - TCloned ICloneFactory.DeepPropertyClone(TCloned objectToClone) { - return SerializationCloner.DeepPropertyClone(objectToClone); - } - - /// Serializes objects by storing their fields - private static readonly BinaryFormatter fieldBasedFormatter; - /// Serializes objects by storing their properties - private static readonly BinaryFormatter propertyBasedFormatter; - - } - -} // namespace Nuclex.Support.Cloning +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.IO; +using System.Reflection; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; + +namespace Nuclex.Support.Cloning { + + /// Clones objects via serialization + /// + /// + /// This type of cloning uses the binary formatter to persist the state of + /// an object and then restores it into a clone. It has the advantage of even + /// working with types that don't provide a default constructor, but is + /// terribly slow. + /// + /// + /// Inspired by the "A Generic Method for Deep Cloning in C# 3.0" article + /// on CodeProject: http://www.codeproject.com/KB/cs/generic_deep_cloning.aspx + /// + /// + public class SerializationCloner : ICloneFactory { + + #region class StaticSurrogateSelector + + /// Selects a static surrogate for any non-primitive types + private class StaticSurrogateSelector : ISurrogateSelector { + + /// Initializes a new static surrogate selector + /// Surrogate that will be selected + public StaticSurrogateSelector(ISerializationSurrogate staticSurrogate) { + this.staticSurrogate = staticSurrogate; + } + + /// + /// Sets the next selector to escalate to if this one can't provide a surrogate + /// + /// Selector to escalate to + public void ChainSelector(ISurrogateSelector selector) { + this.chainedSelector = selector; + } + + /// + /// Returns the selector this one will escalate to if it can't provide a surrogate + /// + /// The selector this one will escalate to + public ISurrogateSelector GetNextSelector() { + return this.chainedSelector; + } + + /// Attempts to provides a surrogate for the specified type + /// Type a surrogate will be provided for + /// Context + /// + /// + public ISerializationSurrogate GetSurrogate( + Type type, StreamingContext context, out ISurrogateSelector selector + ) { + if(type.IsPrimitive || type.IsArray || (type == typeof(string))) { + if(this.chainedSelector == null) { + selector = null; + return null; + } else { + return this.chainedSelector.GetSurrogate(type, context, out selector); + } + } else { + selector = this; + return this.staticSurrogate; + } + } + + /// Surrogate the that will be selected for any non-primitive types + private readonly ISerializationSurrogate staticSurrogate; + /// Surrogate selector to escalate to if no surrogate can be provided + private ISurrogateSelector chainedSelector; + + } + + #endregion // class StaticSurrogateSelector + + #region class FieldSerializationSurrogate + + /// Serializes a type based on its fields + private class FieldSerializationSurrogate : ISerializationSurrogate { + + /// Extracts the data to be serialized from an object + /// Object that is being serialized + /// Stores the serialized informations + /// + /// Provides additional informations about the serialization process + /// + public void GetObjectData( + object objectToSerialize, + SerializationInfo info, + StreamingContext context + ) { + Type originalType = objectToSerialize.GetType(); + + FieldInfo[] fieldInfos = originalType.GetFieldInfosIncludingBaseClasses( + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance + ); + for(int index = 0; index < fieldInfos.Length; ++index) { + FieldInfo fieldInfo = fieldInfos[index]; + info.AddValue(fieldInfo.Name, fieldInfo.GetValue(objectToSerialize)); + } + } + + /// Reinserts saved data into a deserializd object + /// Object the saved data will be inserted into + /// Contains the serialized informations + /// + /// Provides additional informations about the serialization process + /// + /// Surrogate selector that specified this surrogate + /// The deserialized object + public object SetObjectData( + object deserializedObject, + SerializationInfo info, + StreamingContext context, + ISurrogateSelector selector + ) { + Type originalType = deserializedObject.GetType(); + + FieldInfo[] fieldInfos = originalType.GetFieldInfosIncludingBaseClasses( + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance + ); + for(int index = 0; index < fieldInfos.Length; ++index) { + FieldInfo fieldInfo = fieldInfos[index]; + fieldInfo.SetValue(deserializedObject, info.GetValue(fieldInfo.Name, fieldInfo.FieldType)); + } + + return deserializedObject; + } + + } + + #endregion // class FieldSerializationSurrogate + + #region class PropertySerializationSurrogate + + /// Serializes a type based on its properties + private class PropertySerializationSurrogate : ISerializationSurrogate { + + /// Extracts the data to be serialized from an object + /// Object that is being serialized + /// Stores the serialized informations + /// + /// Provides additional informations about the serialization process + /// + public void GetObjectData( + object objectToSerialize, + SerializationInfo info, + StreamingContext context + ) { + Type originalType = objectToSerialize.GetType(); + + PropertyInfo[] propertyInfos = originalType.GetProperties( + BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Instance | BindingFlags.FlattenHierarchy + ); + for(int index = 0; index < propertyInfos.Length; ++index) { + PropertyInfo propertyInfo = propertyInfos[index]; + if(propertyInfo.CanRead && propertyInfo.CanWrite) { + info.AddValue(propertyInfo.Name, propertyInfo.GetValue(objectToSerialize, null)); + } + } + } + + /// Reinserts saved data into a deserializd object + /// Object the saved data will be inserted into + /// Contains the serialized informations + /// + /// Provides additional informations about the serialization process + /// + /// Surrogate selector that specified this surrogate + /// The deserialized object + public object SetObjectData( + object deserializedObject, + SerializationInfo info, + StreamingContext context, + ISurrogateSelector selector + ) { + Type originalType = deserializedObject.GetType(); + + PropertyInfo[] propertyInfos = originalType.GetProperties( + BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Instance | BindingFlags.FlattenHierarchy + ); + for(int index = 0; index < propertyInfos.Length; ++index) { + PropertyInfo propertyInfo = propertyInfos[index]; + if(propertyInfo.CanRead && propertyInfo.CanWrite) { + propertyInfo.SetValue( + deserializedObject, + info.GetValue(propertyInfo.Name, propertyInfo.PropertyType), + null + ); + } + } + + return deserializedObject; + } + + } + + #endregion // class PropertySerializationSurrogate + + /// Initializes the static members of the serialization-based cloner + static SerializationCloner() { + fieldBasedFormatter = new BinaryFormatter( + new StaticSurrogateSelector(new FieldSerializationSurrogate()), + new StreamingContext(StreamingContextStates.All) + ); + propertyBasedFormatter = new BinaryFormatter( + new StaticSurrogateSelector(new PropertySerializationSurrogate()), + new StreamingContext(StreamingContextStates.All) + ); + } + + /// + /// Creates a deep clone of the specified object, also creating clones of all + /// child objects being referenced + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A deep clone of the provided object + public static TCloned DeepFieldClone(TCloned objectToClone) { + if(typeof(TCloned).IsClass || typeof(TCloned).IsArray) { + if(ReferenceEquals(objectToClone, null)) { + return default(TCloned); + } + } + using(var memoryStream = new MemoryStream()) { + fieldBasedFormatter.Serialize(memoryStream, objectToClone); + memoryStream.Position = 0; + return (TCloned)fieldBasedFormatter.Deserialize(memoryStream); + } + } + + /// + /// Creates a deep clone of the specified object, also creating clones of all + /// child objects being referenced + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A deep clone of the provided object + public static TCloned DeepPropertyClone(TCloned objectToClone) { + if(typeof(TCloned).IsClass || typeof(TCloned).IsArray) { + if(ReferenceEquals(objectToClone, null)) { + return default(TCloned); + } + } + using(var memoryStream = new MemoryStream()) { + propertyBasedFormatter.Serialize(memoryStream, objectToClone); + memoryStream.Position = 0; + return (TCloned)propertyBasedFormatter.Deserialize(memoryStream); + } + } + + /// + /// Creates a shallow clone of the specified object, reusing any referenced objects + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A shallow clone of the provided object + TCloned ICloneFactory.ShallowFieldClone(TCloned objectToClone) { + throw new NotSupportedException("The serialization cloner cannot create shallow clones"); + } + + /// + /// Creates a shallow clone of the specified object, reusing any referenced objects + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A shallow clone of the provided object + TCloned ICloneFactory.ShallowPropertyClone(TCloned objectToClone) { + throw new NotSupportedException("The serialization cloner cannot create shallow clones"); + } + + /// + /// Creates a deep clone of the specified object, also creating clones of all + /// child objects being referenced + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A deep clone of the provided object + TCloned ICloneFactory.DeepFieldClone(TCloned objectToClone) { + return SerializationCloner.DeepFieldClone(objectToClone); + } + + /// + /// Creates a deep clone of the specified object, also creating clones of all + /// child objects being referenced + /// + /// Type of the object that will be cloned + /// Object that will be cloned + /// A deep clone of the provided object + TCloned ICloneFactory.DeepPropertyClone(TCloned objectToClone) { + return SerializationCloner.DeepPropertyClone(objectToClone); + } + + /// Serializes objects by storing their fields + private static readonly BinaryFormatter fieldBasedFormatter; + /// Serializes objects by storing their properties + private static readonly BinaryFormatter propertyBasedFormatter; + + } + +} // namespace Nuclex.Support.Cloning diff --git a/Source/Collections/Constants.Test.cs b/Source/Collections/Constants.Test.cs index e990fa5..a0ac65a 100644 --- a/Source/Collections/Constants.Test.cs +++ b/Source/Collections/Constants.Test.cs @@ -1,53 +1,52 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.Collections.Specialized; - -using NUnit.Framework; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the collection constants - [TestFixture] - internal class ConstantsTest { - -#if !NO_SPECIALIZED_COLLECTIONS - - /// - /// Verifies that the collection reset event arguments have 'reset' specified as - /// their action - /// - [Test] - public void CollectionResetEventArgsHaveResetActionSet() { - Assert.AreEqual( - NotifyCollectionChangedAction.Reset, Constants.NotifyCollectionResetEventArgs.Action - ); - } - -#endif // !NO_SPECIALIZED_COLLECTIONS - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.Collections.Specialized; + +using NUnit.Framework; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the collection constants + [TestFixture] + internal class ConstantsTest { + +#if !NO_SPECIALIZED_COLLECTIONS + + /// + /// Verifies that the collection reset event arguments have 'reset' specified as + /// their action + /// + [Test] + public void CollectionResetEventArgsHaveResetActionSet() { + Assert.AreEqual( + NotifyCollectionChangedAction.Reset, Constants.NotifyCollectionResetEventArgs.Action + ); + } + +#endif // !NO_SPECIALIZED_COLLECTIONS + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST diff --git a/Source/Collections/Constants.cs b/Source/Collections/Constants.cs index 0395bb1..5f036fe 100644 --- a/Source/Collections/Constants.cs +++ b/Source/Collections/Constants.cs @@ -1,39 +1,38 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -#if !NO_SPECIALIZED_COLLECTIONS -using System.Collections.Specialized; -#endif - -namespace Nuclex.Support.Collections { - - /// Contains fixed constants used by some collections - public static class Constants { - -#if !NO_SPECIALIZED_COLLECTIONS - /// Fixed event args used to notify that the collection has reset - public static readonly NotifyCollectionChangedEventArgs NotifyCollectionResetEventArgs = - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); -#endif - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +#if !NO_SPECIALIZED_COLLECTIONS +using System.Collections.Specialized; +#endif + +namespace Nuclex.Support.Collections { + + /// Contains fixed constants used by some collections + public static class Constants { + +#if !NO_SPECIALIZED_COLLECTIONS + /// Fixed event args used to notify that the collection has reset + public static readonly NotifyCollectionChangedEventArgs NotifyCollectionResetEventArgs = + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); +#endif + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/Deque.Insertion.cs b/Source/Collections/Deque.Insertion.cs index af78f2c..a112e63 100644 --- a/Source/Collections/Deque.Insertion.cs +++ b/Source/Collections/Deque.Insertion.cs @@ -1,211 +1,210 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; - -namespace Nuclex.Support.Collections { - - partial class Deque { - - /// Inserts an item at the beginning of the double-ended queue - /// Item that will be inserted into the queue - public void AddFirst(TItem item) { - if(this.firstBlockStartIndex > 0) { - --this.firstBlockStartIndex; - } else { // Need to allocate a new block - this.blocks.Insert(0, new TItem[this.blockSize]); - this.firstBlockStartIndex = this.blockSize - 1; - } - - this.blocks[0][this.firstBlockStartIndex] = item; - ++this.count; -#if DEBUG - ++this.version; -#endif - } - - /// Appends an item to the end of the double-ended queue - /// Item that will be appended to the queue - public void AddLast(TItem item) { - if(this.lastBlockEndIndex < this.blockSize) { - ++this.lastBlockEndIndex; - } else { // Need to allocate a new block - this.blocks.Add(new TItem[this.blockSize]); - this.lastBlockEndIndex = 1; - } - - this.blocks[this.blocks.Count - 1][this.lastBlockEndIndex - 1] = item; - ++this.count; -#if DEBUG - ++this.version; -#endif - } - - /// Inserts the item at the specified index - /// Index the item will be inserted at - /// Item that will be inserted - public void Insert(int index, TItem item) { - int distanceToRightEnd = this.count - index; - if(index < distanceToRightEnd) { // Are we closer to the left end? - shiftLeftAndInsert(index, item); - } else { // Nope, we're closer to the right end - shiftRightAndInsert(index, item); - } -#if DEBUG - ++this.version; -#endif - } - - /// - /// Shifts all items before the insertion point to the left and inserts - /// the item at the specified index - /// - /// Index the item will be inserted at - /// Item that will be inserted - private void shiftLeftAndInsert(int index, TItem item) { - if(index == 0) { - AddFirst(item); - } else { - int blockIndex, subIndex; - findIndex(index, out blockIndex, out subIndex); - - int firstBlock = 0; - int blockStart; - - // If the first block is full, we need to add another block - if(this.firstBlockStartIndex == 0) { - this.blocks.Insert(0, new TItem[this.blockSize]); - this.blocks[0][this.blockSize - 1] = this.blocks[1][0]; - this.firstBlockStartIndex = this.blockSize - 1; - - blockStart = 1; - --subIndex; - if(subIndex < 0) { - subIndex = this.blockSize - 1; - } else { - ++blockIndex; - } - ++firstBlock; - } else { - blockStart = this.firstBlockStartIndex; - --this.firstBlockStartIndex; - - --subIndex; - if(subIndex < 0) { - subIndex = this.blockSize - 1; - --blockIndex; - } - } - - // If the insertion point is not in the first block - if(blockIndex != firstBlock) { - Array.Copy( - this.blocks[firstBlock], blockStart, - this.blocks[firstBlock], blockStart - 1, - this.blockSize - blockStart - ); - this.blocks[firstBlock][this.blockSize - 1] = this.blocks[firstBlock + 1][0]; - - // Move all the blocks following the insertion point to the right by one item. - // If there are no blocks inbetween, this for loop will not run. - for(int tempIndex = firstBlock + 1; tempIndex < blockIndex; ++tempIndex) { - Array.Copy( - this.blocks[tempIndex], 1, this.blocks[tempIndex], 0, this.blockSize - 1 - ); - this.blocks[tempIndex][this.blockSize - 1] = this.blocks[tempIndex + 1][0]; - } - - blockStart = 1; - } - - // Finally, move the items in the block the insertion takes place in - Array.Copy( - this.blocks[blockIndex], blockStart, - this.blocks[blockIndex], blockStart - 1, - subIndex - blockStart + 1 - ); - - this.blocks[blockIndex][subIndex] = item; - ++this.count; - } - } - - /// - /// Shifts all items after the insertion point to the right and inserts - /// the item at the specified index - /// - /// Index the item will be inserted at - /// Item that will be inserted - private void shiftRightAndInsert(int index, TItem item) { - if(index == this.count) { - AddLast(item); - } else { - int blockIndex, subIndex; - findIndex(index, out blockIndex, out subIndex); - - int lastBlock = this.blocks.Count - 1; - int blockLength; - - // If the lastmost block is full, we need to add another block - if(this.lastBlockEndIndex == this.blockSize) { - this.blocks.Add(new TItem[this.blockSize]); - this.blocks[lastBlock + 1][0] = this.blocks[lastBlock][this.blockSize - 1]; - this.lastBlockEndIndex = 1; - - blockLength = this.blockSize - 1; - } else { - blockLength = this.lastBlockEndIndex; - ++this.lastBlockEndIndex; - } - - // If the insertion point is not in the lastmost block - if(blockIndex != lastBlock) { - Array.Copy( - this.blocks[lastBlock], 0, this.blocks[lastBlock], 1, blockLength - ); - this.blocks[lastBlock][0] = this.blocks[lastBlock - 1][this.blockSize - 1]; - - // Move all the blocks following the insertion point to the right by one item. - // If there are no blocks inbetween, this for loop will not run. - for(int tempIndex = lastBlock - 1; tempIndex > blockIndex; --tempIndex) { - Array.Copy( - this.blocks[tempIndex], 0, this.blocks[tempIndex], 1, this.blockSize - 1 - ); - this.blocks[tempIndex][0] = this.blocks[tempIndex - 1][this.blockSize - 1]; - } - - blockLength = this.blockSize - 1; - } - - // Finally, move the items in the block the insertion takes place in - Array.Copy( - this.blocks[blockIndex], subIndex, - this.blocks[blockIndex], subIndex + 1, - blockLength - subIndex - ); - - this.blocks[blockIndex][subIndex] = item; - ++this.count; - } - } - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +namespace Nuclex.Support.Collections { + + partial class Deque { + + /// Inserts an item at the beginning of the double-ended queue + /// Item that will be inserted into the queue + public void AddFirst(TItem item) { + if(this.firstBlockStartIndex > 0) { + --this.firstBlockStartIndex; + } else { // Need to allocate a new block + this.blocks.Insert(0, new TItem[this.blockSize]); + this.firstBlockStartIndex = this.blockSize - 1; + } + + this.blocks[0][this.firstBlockStartIndex] = item; + ++this.count; +#if DEBUG + ++this.version; +#endif + } + + /// Appends an item to the end of the double-ended queue + /// Item that will be appended to the queue + public void AddLast(TItem item) { + if(this.lastBlockEndIndex < this.blockSize) { + ++this.lastBlockEndIndex; + } else { // Need to allocate a new block + this.blocks.Add(new TItem[this.blockSize]); + this.lastBlockEndIndex = 1; + } + + this.blocks[this.blocks.Count - 1][this.lastBlockEndIndex - 1] = item; + ++this.count; +#if DEBUG + ++this.version; +#endif + } + + /// Inserts the item at the specified index + /// Index the item will be inserted at + /// Item that will be inserted + public void Insert(int index, TItem item) { + int distanceToRightEnd = this.count - index; + if(index < distanceToRightEnd) { // Are we closer to the left end? + shiftLeftAndInsert(index, item); + } else { // Nope, we're closer to the right end + shiftRightAndInsert(index, item); + } +#if DEBUG + ++this.version; +#endif + } + + /// + /// Shifts all items before the insertion point to the left and inserts + /// the item at the specified index + /// + /// Index the item will be inserted at + /// Item that will be inserted + private void shiftLeftAndInsert(int index, TItem item) { + if(index == 0) { + AddFirst(item); + } else { + int blockIndex, subIndex; + findIndex(index, out blockIndex, out subIndex); + + int firstBlock = 0; + int blockStart; + + // If the first block is full, we need to add another block + if(this.firstBlockStartIndex == 0) { + this.blocks.Insert(0, new TItem[this.blockSize]); + this.blocks[0][this.blockSize - 1] = this.blocks[1][0]; + this.firstBlockStartIndex = this.blockSize - 1; + + blockStart = 1; + --subIndex; + if(subIndex < 0) { + subIndex = this.blockSize - 1; + } else { + ++blockIndex; + } + ++firstBlock; + } else { + blockStart = this.firstBlockStartIndex; + --this.firstBlockStartIndex; + + --subIndex; + if(subIndex < 0) { + subIndex = this.blockSize - 1; + --blockIndex; + } + } + + // If the insertion point is not in the first block + if(blockIndex != firstBlock) { + Array.Copy( + this.blocks[firstBlock], blockStart, + this.blocks[firstBlock], blockStart - 1, + this.blockSize - blockStart + ); + this.blocks[firstBlock][this.blockSize - 1] = this.blocks[firstBlock + 1][0]; + + // Move all the blocks following the insertion point to the right by one item. + // If there are no blocks inbetween, this for loop will not run. + for(int tempIndex = firstBlock + 1; tempIndex < blockIndex; ++tempIndex) { + Array.Copy( + this.blocks[tempIndex], 1, this.blocks[tempIndex], 0, this.blockSize - 1 + ); + this.blocks[tempIndex][this.blockSize - 1] = this.blocks[tempIndex + 1][0]; + } + + blockStart = 1; + } + + // Finally, move the items in the block the insertion takes place in + Array.Copy( + this.blocks[blockIndex], blockStart, + this.blocks[blockIndex], blockStart - 1, + subIndex - blockStart + 1 + ); + + this.blocks[blockIndex][subIndex] = item; + ++this.count; + } + } + + /// + /// Shifts all items after the insertion point to the right and inserts + /// the item at the specified index + /// + /// Index the item will be inserted at + /// Item that will be inserted + private void shiftRightAndInsert(int index, TItem item) { + if(index == this.count) { + AddLast(item); + } else { + int blockIndex, subIndex; + findIndex(index, out blockIndex, out subIndex); + + int lastBlock = this.blocks.Count - 1; + int blockLength; + + // If the lastmost block is full, we need to add another block + if(this.lastBlockEndIndex == this.blockSize) { + this.blocks.Add(new TItem[this.blockSize]); + this.blocks[lastBlock + 1][0] = this.blocks[lastBlock][this.blockSize - 1]; + this.lastBlockEndIndex = 1; + + blockLength = this.blockSize - 1; + } else { + blockLength = this.lastBlockEndIndex; + ++this.lastBlockEndIndex; + } + + // If the insertion point is not in the lastmost block + if(blockIndex != lastBlock) { + Array.Copy( + this.blocks[lastBlock], 0, this.blocks[lastBlock], 1, blockLength + ); + this.blocks[lastBlock][0] = this.blocks[lastBlock - 1][this.blockSize - 1]; + + // Move all the blocks following the insertion point to the right by one item. + // If there are no blocks inbetween, this for loop will not run. + for(int tempIndex = lastBlock - 1; tempIndex > blockIndex; --tempIndex) { + Array.Copy( + this.blocks[tempIndex], 0, this.blocks[tempIndex], 1, this.blockSize - 1 + ); + this.blocks[tempIndex][0] = this.blocks[tempIndex - 1][this.blockSize - 1]; + } + + blockLength = this.blockSize - 1; + } + + // Finally, move the items in the block the insertion takes place in + Array.Copy( + this.blocks[blockIndex], subIndex, + this.blocks[blockIndex], subIndex + 1, + blockLength - subIndex + ); + + this.blocks[blockIndex][subIndex] = item; + ++this.count; + } + } + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/Deque.Interfaces.cs b/Source/Collections/Deque.Interfaces.cs index 6f2884f..bbfa26a 100644 --- a/Source/Collections/Deque.Interfaces.cs +++ b/Source/Collections/Deque.Interfaces.cs @@ -1,150 +1,149 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; -using System.Collections; - -namespace Nuclex.Support.Collections { - - partial class Deque { - - #region IEnumerable members - - /// Obtains a new enumerator for the contents of the deque - /// The new enumerator - IEnumerator IEnumerable.GetEnumerator() { - return new Enumerator(this); - } - - #endregion - - #region IList Members - - /// Adds an item to the deque - /// Item that will be added to the deque - /// The index at which the new item was added - int IList.Add(object value) { - verifyCompatibleObject(value); - - AddLast((TItem)value); - return this.count - 1; - } - - /// Checks whether the deque contains the specified item - /// Item the deque will be scanned for - /// True if the deque contained the specified item - bool IList.Contains(object value) { - return isCompatibleObject(value) && Contains((TItem)value); - } - - /// Determines the index of the item in the deque - /// Item whose index will be determined - /// The index of the specified item in the deque - int IList.IndexOf(object value) { - if(isCompatibleObject(value)) { - return IndexOf((TItem)value); - } else { - return -1; - } - } - - /// Inserts an item into the deque at the specified location - /// Index at which the item will be inserted - /// Item that will be inserted - void IList.Insert(int index, object value) { - verifyCompatibleObject(value); - Insert(index, (TItem)value); - } - - /// Whether the deque has a fixed size - bool IList.IsFixedSize { - get { return false; } - } - - /// Whether the deque is read-only - bool IList.IsReadOnly { - get { return false; } - } - - /// Removes the specified item from the deque - /// Item that will be removed from the deque - void IList.Remove(object value) { - if(isCompatibleObject(value)) { - Remove((TItem)value); - } - } - - /// Accesses an item in the deque by its index - /// Index of the item that will be accessed - /// The item at the specified index - object IList.this[int index] { - get { return this[index]; } - set { - verifyCompatibleObject(value); - this[index] = (TItem)value; - } - } - - #endregion - - #region ICollection Members - - /// Adds an item into the deque - /// Item that will be added to the deque - void ICollection.Add(TItem item) { - AddLast(item); - } - - /// Whether the collection is read-only - bool ICollection.IsReadOnly { - get { return false; } - } - - #endregion - - #region ICollection Members - - /// Copies the contents of the deque into an array - /// Array the contents of the deque will be copied into - /// Index at which writing into the array will begin - void ICollection.CopyTo(Array array, int index) { - if(!(array is TItem[])) { - throw new ArgumentException("Incompatible array type", "array"); - } - - CopyTo((TItem[])array, index); - } - - /// Whether the deque is thread-synchronized - bool ICollection.IsSynchronized { - get { return false; } - } - - /// Synchronization root of the instance - object ICollection.SyncRoot { - get { return this; } - } - - #endregion - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; +using System.Collections; + +namespace Nuclex.Support.Collections { + + partial class Deque { + + #region IEnumerable members + + /// Obtains a new enumerator for the contents of the deque + /// The new enumerator + IEnumerator IEnumerable.GetEnumerator() { + return new Enumerator(this); + } + + #endregion + + #region IList Members + + /// Adds an item to the deque + /// Item that will be added to the deque + /// The index at which the new item was added + int IList.Add(object value) { + verifyCompatibleObject(value); + + AddLast((TItem)value); + return this.count - 1; + } + + /// Checks whether the deque contains the specified item + /// Item the deque will be scanned for + /// True if the deque contained the specified item + bool IList.Contains(object value) { + return isCompatibleObject(value) && Contains((TItem)value); + } + + /// Determines the index of the item in the deque + /// Item whose index will be determined + /// The index of the specified item in the deque + int IList.IndexOf(object value) { + if(isCompatibleObject(value)) { + return IndexOf((TItem)value); + } else { + return -1; + } + } + + /// Inserts an item into the deque at the specified location + /// Index at which the item will be inserted + /// Item that will be inserted + void IList.Insert(int index, object value) { + verifyCompatibleObject(value); + Insert(index, (TItem)value); + } + + /// Whether the deque has a fixed size + bool IList.IsFixedSize { + get { return false; } + } + + /// Whether the deque is read-only + bool IList.IsReadOnly { + get { return false; } + } + + /// Removes the specified item from the deque + /// Item that will be removed from the deque + void IList.Remove(object value) { + if(isCompatibleObject(value)) { + Remove((TItem)value); + } + } + + /// Accesses an item in the deque by its index + /// Index of the item that will be accessed + /// The item at the specified index + object IList.this[int index] { + get { return this[index]; } + set { + verifyCompatibleObject(value); + this[index] = (TItem)value; + } + } + + #endregion + + #region ICollection Members + + /// Adds an item into the deque + /// Item that will be added to the deque + void ICollection.Add(TItem item) { + AddLast(item); + } + + /// Whether the collection is read-only + bool ICollection.IsReadOnly { + get { return false; } + } + + #endregion + + #region ICollection Members + + /// Copies the contents of the deque into an array + /// Array the contents of the deque will be copied into + /// Index at which writing into the array will begin + void ICollection.CopyTo(Array array, int index) { + if(!(array is TItem[])) { + throw new ArgumentException("Incompatible array type", "array"); + } + + CopyTo((TItem[])array, index); + } + + /// Whether the deque is thread-synchronized + bool ICollection.IsSynchronized { + get { return false; } + } + + /// Synchronization root of the instance + object ICollection.SyncRoot { + get { return this; } + } + + #endregion + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/Deque.Removal.cs b/Source/Collections/Deque.Removal.cs index 06cc75c..9722125 100644 --- a/Source/Collections/Deque.Removal.cs +++ b/Source/Collections/Deque.Removal.cs @@ -1,259 +1,258 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; - -namespace Nuclex.Support.Collections { - - partial class Deque { - - /// Removes all items from the deque - public void Clear() { - if(this.blocks.Count > 1) { // Are there multiple blocks? - - // Clear the items in the first block to avoid holding on to references - // in memory unreachable to the user - for(int index = this.firstBlockStartIndex; index < this.blockSize; ++index) { - this.blocks[0][index] = default(TItem); - } - - // Remove any other blocks - this.blocks.RemoveRange(1, this.blocks.Count - 1); - - } else { // Nope, only a single block exists - - // Clear the items in the block to release any reference we may be keeping alive - for( - int index = this.firstBlockStartIndex; index < this.lastBlockEndIndex; ++index - ) { - this.blocks[0][index] = default(TItem); - } - - } - - // Reset the counters to restart the deque from scratch - this.firstBlockStartIndex = 0; - this.lastBlockEndIndex = 0; - this.count = 0; -#if DEBUG - ++this.version; -#endif - } - - /// Removes the specified item from the deque - /// Item that will be removed from the deque - /// True if the item was found and removed - public bool Remove(TItem item) { - int index = IndexOf(item); - if(index == -1) { - return false; - } - - RemoveAt(index); -#if DEBUG - ++this.version; -#endif - return true; - } - - /// Removes the first item in the double-ended queue - public void RemoveFirst() { - if(this.count == 0) { - throw new InvalidOperationException("Cannot remove items from empty deque"); - } - - // This is necessary to make sure the deque doesn't hold dead objects alive - // in unreachable spaces of its memory. - this.blocks[0][this.firstBlockStartIndex] = default(TItem); - - // Cut off the item from the first block. If the block became empty and it's - // not the last remaining block, remove it as well. - ++this.firstBlockStartIndex; - if(this.firstBlockStartIndex >= this.blockSize) { // Block became empty - if(this.count > 1) { // Still more blocks in queue, remove block - this.blocks.RemoveAt(0); - this.firstBlockStartIndex = 0; - } else { // Last block - do not remove - this.firstBlockStartIndex = 0; - this.lastBlockEndIndex = 0; - } - } - --this.count; -#if DEBUG - ++this.version; -#endif - } - - /// Removes the last item in the double-ended queue - public void RemoveLast() { - if(this.count == 0) { - throw new InvalidOperationException("Cannot remove items from empty deque"); - } - - // This is necessary to make sure the deque doesn't hold dead objects alive - // in unreachable spaces of its memory. - int lastBlock = this.blocks.Count - 1; - this.blocks[lastBlock][this.lastBlockEndIndex - 1] = default(TItem); - - // Cut off the last item in the last block. If the block became empty and it's - // not the last remaining block, remove it as well. - --this.lastBlockEndIndex; - if(this.lastBlockEndIndex == 0) { // Block became empty - if(this.count > 1) { - this.blocks.RemoveAt(lastBlock); - this.lastBlockEndIndex = this.blockSize; - } else { // Last block - do not remove - this.firstBlockStartIndex = 0; - this.lastBlockEndIndex = 0; - } - } - --this.count; -#if DEBUG - ++this.version; -#endif - } - - /// Removes the item at the specified index - /// Index of the item that will be removed - public void RemoveAt(int index) { - int distanceToRightEnd = this.count - index; - if(index < distanceToRightEnd) { // Are we closer to the left end? - removeFromLeft(index); - } else { // Nope, we're closer to the right end - removeFromRight(index); - } -#if DEBUG - ++this.version; -#endif - } - - /// - /// Removes an item from the left side of the queue by shifting all items that - /// come before it to the right by one - /// - /// Index of the item that will be removed - private void removeFromLeft(int index) { - if(index == 0) { - RemoveFirst(); - } else { - int blockIndex, subIndex; - findIndex(index, out blockIndex, out subIndex); - - int firstBlock = 0; - int endIndex; - - if(blockIndex > firstBlock) { - Array.Copy( - this.blocks[blockIndex], 0, - this.blocks[blockIndex], 1, - subIndex - ); - this.blocks[blockIndex][0] = this.blocks[blockIndex - 1][this.blockSize - 1]; - - for(int tempIndex = blockIndex - 1; tempIndex > firstBlock; --tempIndex) { - Array.Copy( - this.blocks[tempIndex], 0, - this.blocks[tempIndex], 1, - this.blockSize - 1 - ); - this.blocks[tempIndex][0] = this.blocks[tempIndex - 1][this.blockSize - 1]; - } - - endIndex = this.blockSize - 1; - } else { - endIndex = subIndex; - } - - Array.Copy( - this.blocks[firstBlock], this.firstBlockStartIndex, - this.blocks[firstBlock], this.firstBlockStartIndex + 1, - endIndex - this.firstBlockStartIndex - ); - - if(this.firstBlockStartIndex == this.blockSize - 1) { - this.blocks.RemoveAt(0); - this.firstBlockStartIndex = 0; - } else { - this.blocks[0][this.firstBlockStartIndex] = default(TItem); - ++this.firstBlockStartIndex; - } - - --this.count; - } - } - - /// - /// Removes an item from the right side of the queue by shifting all items that - /// come after it to the left by one - /// - /// Index of the item that will be removed - private void removeFromRight(int index) { - if(index == this.count - 1) { - RemoveLast(); - } else { - int blockIndex, subIndex; - findIndex(index, out blockIndex, out subIndex); - - int lastBlock = this.blocks.Count - 1; - int startIndex; - - if(blockIndex < lastBlock) { - Array.Copy( - this.blocks[blockIndex], subIndex + 1, - this.blocks[blockIndex], subIndex, - this.blockSize - subIndex - 1 - ); - this.blocks[blockIndex][this.blockSize - 1] = this.blocks[blockIndex + 1][0]; - - for(int tempIndex = blockIndex + 1; tempIndex < lastBlock; ++tempIndex) { - Array.Copy( - this.blocks[tempIndex], 1, - this.blocks[tempIndex], 0, - this.blockSize - 1 - ); - this.blocks[tempIndex][this.blockSize - 1] = this.blocks[tempIndex + 1][0]; - } - - startIndex = 0; - } else { - startIndex = subIndex; - } - - Array.Copy( - this.blocks[lastBlock], startIndex + 1, - this.blocks[lastBlock], startIndex, - this.lastBlockEndIndex - startIndex - 1 - ); - - if(this.lastBlockEndIndex == 1) { - this.blocks.RemoveAt(lastBlock); - this.lastBlockEndIndex = this.blockSize; - } else { - this.blocks[lastBlock][this.lastBlockEndIndex - 1] = default(TItem); - --this.lastBlockEndIndex; - } - - --this.count; - } - } - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +namespace Nuclex.Support.Collections { + + partial class Deque { + + /// Removes all items from the deque + public void Clear() { + if(this.blocks.Count > 1) { // Are there multiple blocks? + + // Clear the items in the first block to avoid holding on to references + // in memory unreachable to the user + for(int index = this.firstBlockStartIndex; index < this.blockSize; ++index) { + this.blocks[0][index] = default(TItem); + } + + // Remove any other blocks + this.blocks.RemoveRange(1, this.blocks.Count - 1); + + } else { // Nope, only a single block exists + + // Clear the items in the block to release any reference we may be keeping alive + for( + int index = this.firstBlockStartIndex; index < this.lastBlockEndIndex; ++index + ) { + this.blocks[0][index] = default(TItem); + } + + } + + // Reset the counters to restart the deque from scratch + this.firstBlockStartIndex = 0; + this.lastBlockEndIndex = 0; + this.count = 0; +#if DEBUG + ++this.version; +#endif + } + + /// Removes the specified item from the deque + /// Item that will be removed from the deque + /// True if the item was found and removed + public bool Remove(TItem item) { + int index = IndexOf(item); + if(index == -1) { + return false; + } + + RemoveAt(index); +#if DEBUG + ++this.version; +#endif + return true; + } + + /// Removes the first item in the double-ended queue + public void RemoveFirst() { + if(this.count == 0) { + throw new InvalidOperationException("Cannot remove items from empty deque"); + } + + // This is necessary to make sure the deque doesn't hold dead objects alive + // in unreachable spaces of its memory. + this.blocks[0][this.firstBlockStartIndex] = default(TItem); + + // Cut off the item from the first block. If the block became empty and it's + // not the last remaining block, remove it as well. + ++this.firstBlockStartIndex; + if(this.firstBlockStartIndex >= this.blockSize) { // Block became empty + if(this.count > 1) { // Still more blocks in queue, remove block + this.blocks.RemoveAt(0); + this.firstBlockStartIndex = 0; + } else { // Last block - do not remove + this.firstBlockStartIndex = 0; + this.lastBlockEndIndex = 0; + } + } + --this.count; +#if DEBUG + ++this.version; +#endif + } + + /// Removes the last item in the double-ended queue + public void RemoveLast() { + if(this.count == 0) { + throw new InvalidOperationException("Cannot remove items from empty deque"); + } + + // This is necessary to make sure the deque doesn't hold dead objects alive + // in unreachable spaces of its memory. + int lastBlock = this.blocks.Count - 1; + this.blocks[lastBlock][this.lastBlockEndIndex - 1] = default(TItem); + + // Cut off the last item in the last block. If the block became empty and it's + // not the last remaining block, remove it as well. + --this.lastBlockEndIndex; + if(this.lastBlockEndIndex == 0) { // Block became empty + if(this.count > 1) { + this.blocks.RemoveAt(lastBlock); + this.lastBlockEndIndex = this.blockSize; + } else { // Last block - do not remove + this.firstBlockStartIndex = 0; + this.lastBlockEndIndex = 0; + } + } + --this.count; +#if DEBUG + ++this.version; +#endif + } + + /// Removes the item at the specified index + /// Index of the item that will be removed + public void RemoveAt(int index) { + int distanceToRightEnd = this.count - index; + if(index < distanceToRightEnd) { // Are we closer to the left end? + removeFromLeft(index); + } else { // Nope, we're closer to the right end + removeFromRight(index); + } +#if DEBUG + ++this.version; +#endif + } + + /// + /// Removes an item from the left side of the queue by shifting all items that + /// come before it to the right by one + /// + /// Index of the item that will be removed + private void removeFromLeft(int index) { + if(index == 0) { + RemoveFirst(); + } else { + int blockIndex, subIndex; + findIndex(index, out blockIndex, out subIndex); + + int firstBlock = 0; + int endIndex; + + if(blockIndex > firstBlock) { + Array.Copy( + this.blocks[blockIndex], 0, + this.blocks[blockIndex], 1, + subIndex + ); + this.blocks[blockIndex][0] = this.blocks[blockIndex - 1][this.blockSize - 1]; + + for(int tempIndex = blockIndex - 1; tempIndex > firstBlock; --tempIndex) { + Array.Copy( + this.blocks[tempIndex], 0, + this.blocks[tempIndex], 1, + this.blockSize - 1 + ); + this.blocks[tempIndex][0] = this.blocks[tempIndex - 1][this.blockSize - 1]; + } + + endIndex = this.blockSize - 1; + } else { + endIndex = subIndex; + } + + Array.Copy( + this.blocks[firstBlock], this.firstBlockStartIndex, + this.blocks[firstBlock], this.firstBlockStartIndex + 1, + endIndex - this.firstBlockStartIndex + ); + + if(this.firstBlockStartIndex == this.blockSize - 1) { + this.blocks.RemoveAt(0); + this.firstBlockStartIndex = 0; + } else { + this.blocks[0][this.firstBlockStartIndex] = default(TItem); + ++this.firstBlockStartIndex; + } + + --this.count; + } + } + + /// + /// Removes an item from the right side of the queue by shifting all items that + /// come after it to the left by one + /// + /// Index of the item that will be removed + private void removeFromRight(int index) { + if(index == this.count - 1) { + RemoveLast(); + } else { + int blockIndex, subIndex; + findIndex(index, out blockIndex, out subIndex); + + int lastBlock = this.blocks.Count - 1; + int startIndex; + + if(blockIndex < lastBlock) { + Array.Copy( + this.blocks[blockIndex], subIndex + 1, + this.blocks[blockIndex], subIndex, + this.blockSize - subIndex - 1 + ); + this.blocks[blockIndex][this.blockSize - 1] = this.blocks[blockIndex + 1][0]; + + for(int tempIndex = blockIndex + 1; tempIndex < lastBlock; ++tempIndex) { + Array.Copy( + this.blocks[tempIndex], 1, + this.blocks[tempIndex], 0, + this.blockSize - 1 + ); + this.blocks[tempIndex][this.blockSize - 1] = this.blocks[tempIndex + 1][0]; + } + + startIndex = 0; + } else { + startIndex = subIndex; + } + + Array.Copy( + this.blocks[lastBlock], startIndex + 1, + this.blocks[lastBlock], startIndex, + this.lastBlockEndIndex - startIndex - 1 + ); + + if(this.lastBlockEndIndex == 1) { + this.blocks.RemoveAt(lastBlock); + this.lastBlockEndIndex = this.blockSize; + } else { + this.blocks[lastBlock][this.lastBlockEndIndex - 1] = default(TItem); + --this.lastBlockEndIndex; + } + + --this.count; + } + } + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/Deque.Search.cs b/Source/Collections/Deque.Search.cs index 711c139..13a2904 100644 --- a/Source/Collections/Deque.Search.cs +++ b/Source/Collections/Deque.Search.cs @@ -1,84 +1,83 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; - -namespace Nuclex.Support.Collections { - - partial class Deque { - - /// - /// Determines the index of the first occurence of the specified item in the deque - /// - /// Item that will be located in the deque - /// The index of the item or -1 if it wasn't found - public int IndexOf(TItem item) { - if(this.blocks.Count == 1) { // Only one block to scan? - int length = this.lastBlockEndIndex - this.firstBlockStartIndex; - int index = Array.IndexOf( - this.blocks[0], item, this.firstBlockStartIndex, length - ); - - // If we found something, we need to adjust its index so the first item in - // the deque always appears at index 0 to the user - if(index != -1) { - return (index - this.firstBlockStartIndex); - } else { - return -1; - } - } else { // At least two blocks exist - - // Scan the first block for the item and if found, return the index - int length = this.blockSize - this.firstBlockStartIndex; - int index = Array.IndexOf( - this.blocks[0], item, this.firstBlockStartIndex, length - ); - - // If we found something, we need to adjust its index - if(index != -1) { - return (index - this.firstBlockStartIndex); - } - - int lastBlock = this.blocks.Count - 1; - for(int tempIndex = 1; tempIndex < lastBlock; ++tempIndex) { - index = Array.IndexOf( - this.blocks[tempIndex], item, 0, this.blockSize - ); - if(index != -1) { - return (index - this.firstBlockStartIndex + tempIndex * this.blockSize); - } - } - - // Nothing found, continue the search in the - index = Array.IndexOf( - this.blocks[lastBlock], item, 0, this.lastBlockEndIndex - ); - if(index == -1) { - return -1; - } else { - return (index - this.firstBlockStartIndex + lastBlock * this.blockSize); - } - - } - } - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +namespace Nuclex.Support.Collections { + + partial class Deque { + + /// + /// Determines the index of the first occurence of the specified item in the deque + /// + /// Item that will be located in the deque + /// The index of the item or -1 if it wasn't found + public int IndexOf(TItem item) { + if(this.blocks.Count == 1) { // Only one block to scan? + int length = this.lastBlockEndIndex - this.firstBlockStartIndex; + int index = Array.IndexOf( + this.blocks[0], item, this.firstBlockStartIndex, length + ); + + // If we found something, we need to adjust its index so the first item in + // the deque always appears at index 0 to the user + if(index != -1) { + return (index - this.firstBlockStartIndex); + } else { + return -1; + } + } else { // At least two blocks exist + + // Scan the first block for the item and if found, return the index + int length = this.blockSize - this.firstBlockStartIndex; + int index = Array.IndexOf( + this.blocks[0], item, this.firstBlockStartIndex, length + ); + + // If we found something, we need to adjust its index + if(index != -1) { + return (index - this.firstBlockStartIndex); + } + + int lastBlock = this.blocks.Count - 1; + for(int tempIndex = 1; tempIndex < lastBlock; ++tempIndex) { + index = Array.IndexOf( + this.blocks[tempIndex], item, 0, this.blockSize + ); + if(index != -1) { + return (index - this.firstBlockStartIndex + tempIndex * this.blockSize); + } + } + + // Nothing found, continue the search in the + index = Array.IndexOf( + this.blocks[lastBlock], item, 0, this.lastBlockEndIndex + ); + if(index == -1) { + return -1; + } else { + return (index - this.firstBlockStartIndex + lastBlock * this.blockSize); + } + + } + } + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/Deque.Test.cs b/Source/Collections/Deque.Test.cs index 77eefcd..6102643 100644 --- a/Source/Collections/Deque.Test.cs +++ b/Source/Collections/Deque.Test.cs @@ -1,711 +1,710 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.Collections; -using System.Collections.Generic; - -using NUnit.Framework; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the double ended queue - [TestFixture] - internal class DequeTest { - - /// Verifies that the AddLast() method of the deque is working - [Test] - public void TestAddLast() { - Deque intDeque = new Deque(16); - for(int item = 0; item < 48; ++item) { - intDeque.AddLast(item); - } - - for(int item = 0; item < 48; ++item) { - Assert.AreEqual(item, intDeque[item]); - } - } - - /// Verifies that the AddFirst() method of the deque is working - [Test] - public void TestAddFirst() { - Deque intDeque = new Deque(16); - for(int item = 0; item < 48; ++item) { - intDeque.AddFirst(item); - } - - for(int item = 0; item < 48; ++item) { - Assert.AreEqual(47 - item, intDeque[item]); - } - } - - /// - /// Verifies that the RemoveFirst() method of the deque is working - /// - [Test] - public void TestRemoveFirst() { - Deque intDeque = new Deque(16); - for(int item = 0; item < 48; ++item) { - intDeque.AddLast(item); - } - - for(int item = 0; item < 48; ++item) { - Assert.AreEqual(item, intDeque.First); - Assert.AreEqual(48 - item, intDeque.Count); - intDeque.RemoveFirst(); - } - } - - /// - /// Verifies that the RemoveLast() method of the deque is working - /// - [Test] - public void TestRemoveLast() { - Deque intDeque = new Deque(16); - for(int item = 0; item < 48; ++item) { - intDeque.AddLast(item); - } - - for(int item = 0; item < 48; ++item) { - Assert.AreEqual(47 - item, intDeque.Last); - Assert.AreEqual(48 - item, intDeque.Count); - intDeque.RemoveLast(); - } - } - - /// Verifies that the Insert() method works in all cases - /// - /// We have several different cases here that will be tested. The deque can - /// shift items to the left or right (depending on which end is closer to - /// the insertion point) and the insertion point may fall in an only partially - /// occupied block, requiring elaborate index calculations - /// - [Test] - public void TestInsert() { - for(int testedIndex = 0; testedIndex <= 96; ++testedIndex) { - Deque intDeque = createDeque(96); - - intDeque.Insert(testedIndex, 12345); - - Assert.AreEqual(97, intDeque.Count); - - for(int index = 0; index < testedIndex; ++index) { - Assert.AreEqual(index, intDeque[index]); - } - Assert.AreEqual(12345, intDeque[testedIndex]); - for(int index = testedIndex + 1; index < 97; ++index) { - Assert.AreEqual(index - 1, intDeque[index]); - } - } - } - - /// - /// Verifies that the Insert() method works in all cases when the deque doesn't - /// start at a block boundary - /// - [Test] - public void TestInsertNonNormalized() { - for(int testedIndex = 0; testedIndex <= 96; ++testedIndex) { - Deque intDeque = createNonNormalizedDeque(96); - - intDeque.Insert(testedIndex, 12345); - - Assert.AreEqual(97, intDeque.Count); - - for(int index = 0; index < testedIndex; ++index) { - Assert.AreEqual(index, intDeque[index]); - } - Assert.AreEqual(12345, intDeque[testedIndex]); - for(int index = testedIndex + 1; index < 97; ++index) { - Assert.AreEqual(index - 1, intDeque[index]); - } - } - } - - /// Verifies the the RemoveAt() method works in all cases - [Test] - public void TestRemoveAt() { - for(int testedIndex = 0; testedIndex < 96; ++testedIndex) { - Deque intDeque = new Deque(16); - for(int item = 0; item < 96; ++item) { - intDeque.AddLast(item); - } - - intDeque.RemoveAt(testedIndex); - - Assert.AreEqual(95, intDeque.Count); - - for(int index = 0; index < testedIndex; ++index) { - Assert.AreEqual(index, intDeque[index]); - } - for(int index = testedIndex; index < 95; ++index) { - Assert.AreEqual(index + 1, intDeque[index]); - } - } - } - - /// - /// Verifies the the RemoveAt() method works in all cases when the deque doesn't - /// start at a block boundary - /// - [Test] - public void TestRemoveAtNonNormalized() { - for(int testedIndex = 0; testedIndex < 96; ++testedIndex) { - Deque intDeque = new Deque(16); - for(int item = 4; item < 96; ++item) { - intDeque.AddLast(item); - } - intDeque.AddFirst(3); - intDeque.AddFirst(2); - intDeque.AddFirst(1); - intDeque.AddFirst(0); - - intDeque.RemoveAt(testedIndex); - - Assert.AreEqual(95, intDeque.Count); - - for(int index = 0; index < testedIndex; ++index) { - Assert.AreEqual(index, intDeque[index]); - } - for(int index = testedIndex; index < 95; ++index) { - Assert.AreEqual(index + 1, intDeque[index]); - } - } - } - - /// - /// Tests whether the RemoveAt() method keeps the state of the deque intact when - /// it has to remove a block from the left end of the deque - /// - [Test] - public void TestRemoveAtEmptiesLeftBlock() { - Deque intDeque = new Deque(16); - for(int item = 1; item <= 16; ++item) { - intDeque.AddLast(item); - } - intDeque.AddFirst(0); - intDeque.RemoveAt(3); - - Assert.AreEqual(16, intDeque.Count); - - for(int index = 0; index < 3; ++index) { - Assert.AreEqual(index, intDeque[index]); - } - for(int index = 3; index < 16; ++index) { - Assert.AreEqual(index + 1, intDeque[index]); - } - } - - /// - /// Tests whether the RemoveAt() method keeps the state of the deque intact when - /// it has to remove a block from the right end of the deque - /// - [Test] - public void TestRemoveAtEmptiesRightBlock() { - Deque intDeque = new Deque(16); - for(int item = 0; item <= 16; ++item) { - intDeque.AddLast(item); - } - intDeque.RemoveAt(13); - - Assert.AreEqual(16, intDeque.Count); - - for(int index = 0; index < 13; ++index) { - Assert.AreEqual(index, intDeque[index]); - } - for(int index = 13; index < 16; ++index) { - Assert.AreEqual(index + 1, intDeque[index]); - } - } - - /// - /// Validates that an exception is thrown if the 'First' property is accessed - /// in an empty deque - /// - [Test] - public void TestThrowOnAccessFirstInEmptyDeque() { - Deque intDeque = new Deque(); - Assert.Throws( - delegate() { Console.WriteLine(intDeque.First); } - ); - } - - /// - /// Validates that an exception is thrown if the 'Last' property is accessed - /// in an empty deque - /// - [Test] - public void TestThrowOnAccessLastInEmptyDeque() { - Deque intDeque = new Deque(); - Assert.Throws( - delegate() { Console.WriteLine(intDeque.Last); } - ); - } - - /// - /// Validates that an exception is thrown if the first item is attempted to be - /// removed from an empty deque - /// - [Test] - public void TestThrowOnRemoveFirstFromEmptyDeque() { - Deque intDeque = new Deque(); - Assert.Throws( - delegate() { intDeque.RemoveFirst(); } - ); - } - - /// - /// Validates that an exception is thrown if the last item is attempted to be - /// removed from an empty deque - /// - [Test] - public void TestThrowOnRemoveLastFromEmptyDeque() { - Deque intDeque = new Deque(); - Assert.Throws( - delegate() { intDeque.RemoveLast(); } - ); - } - - /// - /// Verifies that items can be assigned by their index - /// - [Test] - public void TestIndexAssignment() { - Deque intDeque = createDeque(32); - intDeque[16] = 12345; - intDeque[17] = 54321; - - for(int index = 0; index < 16; ++index) { - intDeque.RemoveFirst(); - } - - Assert.AreEqual(12345, intDeque.First); - intDeque.RemoveFirst(); - Assert.AreEqual(54321, intDeque.First); - } - - /// - /// Verifies that an exception is thrown if an invalid index is accessed - /// - [Test] - public void TestThrowOnInvalidIndex() { - Deque intDeque = new Deque(16); - for(int item = 0; item < 32; ++item) { - intDeque.AddLast(item); - } - - Assert.Throws( - delegate() { Console.WriteLine(intDeque[32]); } - ); - } - - /// Tests the IndexOf() method - [Test, TestCase(0), TestCase(16), TestCase(32), TestCase(48)] - public void TestIndexOf(int count) { - Deque intDeque = new Deque(16); - for(int item = 0; item < count; ++item) { - intDeque.AddLast(item); - } - - for(int item = 0; item < count; ++item) { - Assert.AreEqual(item, intDeque.IndexOf(item)); - } - Assert.AreEqual(-1, intDeque.IndexOf(count)); - } - - /// - /// Tests the IndexOf() method with the deque not starting at a block boundary - /// - [Test, TestCase(0), TestCase(16), TestCase(32), TestCase(48)] - public void TestIndexOfNonNormalized(int count) { - Deque intDeque = createNonNormalizedDeque(count); - - for(int item = 0; item < count; ++item) { - Assert.AreEqual(item, intDeque.IndexOf(item)); - } - Assert.AreEqual(-1, intDeque.IndexOf(count)); - } - - /// Verifies that the deque's enumerator works - [Test] - public void TestEnumerator() { - Deque intDeque = createNonNormalizedDeque(40); - - for(int testRun = 0; testRun < 2; ++testRun) { - using(IEnumerator enumerator = intDeque.GetEnumerator()) { - for(int index = 0; index < intDeque.Count; ++index) { - Assert.IsTrue(enumerator.MoveNext()); - Assert.AreEqual(index, enumerator.Current); - } - - Assert.IsFalse(enumerator.MoveNext()); - - enumerator.Reset(); - } - } - } - - /// Verifies that the deque's enumerator works - [Test] - public void TestObjectEnumerator() { - Deque intDeque = createNonNormalizedDeque(40); - - for(int testRun = 0; testRun < 2; ++testRun) { - IEnumerator enumerator = ((IEnumerable)intDeque).GetEnumerator(); - for(int index = 0; index < intDeque.Count; ++index) { - Assert.IsTrue(enumerator.MoveNext()); - Assert.AreEqual(index, enumerator.Current); - } - - Assert.IsFalse(enumerator.MoveNext()); - - enumerator.Reset(); - } - } - - /// - /// Verifies that an exception is thrown if the enumerator is accessed in - /// an invalid position - /// - [Test] - public void TestThrowOnInvalidEnumeratorPosition() { - Deque intDeque = createNonNormalizedDeque(40); - - using(IEnumerator enumerator = intDeque.GetEnumerator()) { - Assert.Throws( - delegate() { Console.WriteLine(enumerator.Current); } - ); - - while(enumerator.MoveNext()) { } - - Assert.Throws( - delegate() { Console.WriteLine(enumerator.Current); } - ); - } - } - - /// Tests whether a small deque can be cleared - [Test] - public void TestClearSmallDeque() { - Deque intDeque = createDeque(12); - intDeque.Clear(); - Assert.AreEqual(0, intDeque.Count); - } - - /// Tests whether a large deque can be cleared - [Test] - public void TestClearLargeDeque() { - Deque intDeque = createDeque(40); - intDeque.Clear(); - Assert.AreEqual(0, intDeque.Count); - } - - /// Verifies that the non-typesafe Add() method is working - [Test] - public void TestAddObject() { - Deque intDeque = new Deque(); - Assert.AreEqual(0, ((IList)intDeque).Add(123)); - Assert.AreEqual(1, intDeque.Count); - } - - /// - /// Tests whether an exception is thrown if the non-typesafe Add() method is - /// used to add an incompatible object into the deque - /// - [Test] - public void TestThrowOnAddIncompatibleObject() { - Deque intDeque = new Deque(); - Assert.Throws( - delegate() { ((IList)intDeque).Add("Hello World"); } - ); - } - - /// Verifies that the Add() method is working - [Test] - public void TestAdd() { - Deque intDeque = new Deque(); - ((IList)intDeque).Add(123); - Assert.AreEqual(1, intDeque.Count); - } - - /// Tests whether the Contains() method is working - [Test] - public void TestContains() { - Deque intDeque = createDeque(16); - Assert.IsTrue(intDeque.Contains(14)); - Assert.IsFalse(intDeque.Contains(16)); - } - - /// Tests the non-typesafe Contains() method - [Test] - public void TestContainsObject() { - Deque intDeque = createDeque(16); - Assert.IsTrue(((IList)intDeque).Contains(14)); - Assert.IsFalse(((IList)intDeque).Contains(16)); - Assert.IsFalse(((IList)intDeque).Contains("Hello World")); - } - - /// Tests the non-typesafe Contains() method - [Test] - public void TestIndexOfObject() { - Deque intDeque = createDeque(16); - Assert.AreEqual(14, ((IList)intDeque).IndexOf(14)); - Assert.AreEqual(-1, ((IList)intDeque).IndexOf(16)); - Assert.AreEqual(-1, ((IList)intDeque).IndexOf("Hello World")); - } - - /// Tests wether the non-typesafe Insert() method is working - [Test] - public void TestInsertObject() { - for(int testedIndex = 0; testedIndex <= 96; ++testedIndex) { - Deque intDeque = createDeque(96); - - ((IList)intDeque).Insert(testedIndex, 12345); - - Assert.AreEqual(97, intDeque.Count); - - for(int index = 0; index < testedIndex; ++index) { - Assert.AreEqual(index, intDeque[index]); - } - Assert.AreEqual(12345, intDeque[testedIndex]); - for(int index = testedIndex + 1; index < 97; ++index) { - Assert.AreEqual(index - 1, intDeque[index]); - } - } - } - - /// - /// Verifies that an exception is thrown if an incompatible object is inserted - /// into the deque - /// - [Test] - public void TestThrowOnInsertIncompatibleObject() { - Deque intDeque = createDeque(12); - Assert.Throws( - delegate() { ((IList)intDeque).Insert(8, "Hello World"); } - ); - } - - /// Validates that the IsFixedObject property is set to false - [Test] - public void TestIsFixedObject() { - Deque intDeque = new Deque(); - Assert.IsFalse(((IList)intDeque).IsFixedSize); - } - - /// Validates that the IsSynchronized property is set to false - [Test] - public void TestIsSynchronized() { - Deque intDeque = new Deque(); - Assert.IsFalse(((IList)intDeque).IsSynchronized); - } - - /// - /// Verifies that items can be assigned by their index using the non-typesafe - /// IList interface - /// - [Test] - public void TestObjectIndexAssignment() { - Deque intDeque = createDeque(32); - - ((IList)intDeque)[16] = 12345; - ((IList)intDeque)[17] = 54321; - - Assert.AreEqual(12345, ((IList)intDeque)[16]); - Assert.AreEqual(54321, ((IList)intDeque)[17]); - } - - /// - /// Tests whether an exception is thrown if an incompatible object is assigned - /// to the deque - /// - [Test] - public void TestIncompatibleObjectIndexAssignment() { - Deque intDeque = createDeque(2); - Assert.Throws( - delegate() { ((IList)intDeque)[0] = "Hello World"; } - ); - } - - /// Verifies that the Remove() method is working correctly - [Test] - public void TestRemove() { - Deque intDeque = createDeque(16); - Assert.AreEqual(16, intDeque.Count); - Assert.IsTrue(intDeque.Remove(13)); - Assert.IsFalse(intDeque.Remove(13)); - Assert.AreEqual(15, intDeque.Count); - } - - /// Tests the non-typesafe remove method - [Test] - public void TestRemoveObject() { - Deque intDeque = createDeque(10); - Assert.IsTrue(intDeque.Contains(8)); - Assert.AreEqual(10, intDeque.Count); - ((IList)intDeque).Remove(8); - Assert.IsFalse(intDeque.Contains(8)); - Assert.AreEqual(9, intDeque.Count); - } - - /// - /// Tests the non-typesafe remove method used to remove an incompatible object - /// - [Test] - public void TestRemoveIncompatibleObject() { - Deque intDeque = createDeque(10); - ((IList)intDeque).Remove("Hello World"); // should simply do nothing - Assert.AreEqual(10, intDeque.Count); - } - - /// - /// Verifies that the IsSynchronized property and the SyncRoot property are working - /// - [Test] - public void TestSynchronization() { - Deque intDeque = new Deque(); - - if(!(intDeque as ICollection).IsSynchronized) { - lock((intDeque as ICollection).SyncRoot) { - Assert.AreEqual(0, intDeque.Count); - } - } - } - - /// - /// Validates that the IsReadOnly property of the deque returns false - /// - [Test] - public void TestIsReadOnly() { - Deque intDeque = new Deque(); - Assert.IsFalse(((IList)intDeque).IsReadOnly); - Assert.IsFalse(((ICollection)intDeque).IsReadOnly); - } - - /// Tests the non-typesafe CopyTo() method - [Test] - public void TestCopyToObjectArray() { - Deque intDeque = createNonNormalizedDeque(40); - - int[] array = new int[40]; - ((ICollection)intDeque).CopyTo(array, 0); - - Assert.AreEqual(intDeque, array); - } - - /// Tests the CopyTo() method - [Test] - public void TestCopyToArray() { - Deque intDeque = createDeque(12); - intDeque.RemoveFirst(); - intDeque.RemoveFirst(); - - int[] array = new int[14]; - intDeque.CopyTo(array, 4); - - Assert.AreEqual( - new int[] { 0, 0, 0, 0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }, - array - ); - } - - /// - /// Verifies that the non-typesafe CopyTo() method throws an exception if - /// the array is of an incompatible type - /// - [Test] - public void TestThrowOnCopyToIncompatibleObjectArray() { - Deque intDeque = createDeque(4); - - short[] array = new short[4]; - Assert.Throws( - delegate() { ((ICollection)intDeque).CopyTo(array, 4); } - ); - } - - /// - /// Verifies that the CopyTo() method throws an exception if the target array - /// is too small - /// - [Test] - public void TestThrowOnCopyToTooSmallArray() { - Deque intDeque = createDeque(8); - Assert.Throws( - delegate() { intDeque.CopyTo(new int[7], 0); } - ); - } - -#if DEBUG - /// - /// Tests whether the deque enumerator detects when it runs out of sync - /// - [Test] - public void TestInvalidatedEnumeratorDetection() { - Deque intDeque = createDeque(8); - using(IEnumerator enumerator = intDeque.GetEnumerator()) { - Assert.IsTrue(enumerator.MoveNext()); - intDeque.AddFirst(12345); - Assert.Throws( - delegate() { enumerator.MoveNext(); } - ); - } - } -#endif - - /// - /// Creates a deque whose first element does not coincide with a block boundary - /// - /// Number of items the deque will be filled with - /// The newly created deque - private static Deque createNonNormalizedDeque(int count) { - Deque intDeque = new Deque(16); - - for(int item = 4; item < count; ++item) { - intDeque.AddLast(item); - } - if(count > 3) { intDeque.AddFirst(3); } - if(count > 2) { intDeque.AddFirst(2); } - if(count > 1) { intDeque.AddFirst(1); } - if(count > 0) { intDeque.AddFirst(0); } - - return intDeque; - } - - /// Creates a deque filled with the specified number of items - /// - /// Number of items the deque will be filled with - /// The newly created deque - private static Deque createDeque(int count) { - Deque intDeque = new Deque(16); - - for(int item = 0; item < count; ++item) { - intDeque.AddLast(item); - } - - return intDeque; - } - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.Collections; +using System.Collections.Generic; + +using NUnit.Framework; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the double ended queue + [TestFixture] + internal class DequeTest { + + /// Verifies that the AddLast() method of the deque is working + [Test] + public void TestAddLast() { + Deque intDeque = new Deque(16); + for(int item = 0; item < 48; ++item) { + intDeque.AddLast(item); + } + + for(int item = 0; item < 48; ++item) { + Assert.AreEqual(item, intDeque[item]); + } + } + + /// Verifies that the AddFirst() method of the deque is working + [Test] + public void TestAddFirst() { + Deque intDeque = new Deque(16); + for(int item = 0; item < 48; ++item) { + intDeque.AddFirst(item); + } + + for(int item = 0; item < 48; ++item) { + Assert.AreEqual(47 - item, intDeque[item]); + } + } + + /// + /// Verifies that the RemoveFirst() method of the deque is working + /// + [Test] + public void TestRemoveFirst() { + Deque intDeque = new Deque(16); + for(int item = 0; item < 48; ++item) { + intDeque.AddLast(item); + } + + for(int item = 0; item < 48; ++item) { + Assert.AreEqual(item, intDeque.First); + Assert.AreEqual(48 - item, intDeque.Count); + intDeque.RemoveFirst(); + } + } + + /// + /// Verifies that the RemoveLast() method of the deque is working + /// + [Test] + public void TestRemoveLast() { + Deque intDeque = new Deque(16); + for(int item = 0; item < 48; ++item) { + intDeque.AddLast(item); + } + + for(int item = 0; item < 48; ++item) { + Assert.AreEqual(47 - item, intDeque.Last); + Assert.AreEqual(48 - item, intDeque.Count); + intDeque.RemoveLast(); + } + } + + /// Verifies that the Insert() method works in all cases + /// + /// We have several different cases here that will be tested. The deque can + /// shift items to the left or right (depending on which end is closer to + /// the insertion point) and the insertion point may fall in an only partially + /// occupied block, requiring elaborate index calculations + /// + [Test] + public void TestInsert() { + for(int testedIndex = 0; testedIndex <= 96; ++testedIndex) { + Deque intDeque = createDeque(96); + + intDeque.Insert(testedIndex, 12345); + + Assert.AreEqual(97, intDeque.Count); + + for(int index = 0; index < testedIndex; ++index) { + Assert.AreEqual(index, intDeque[index]); + } + Assert.AreEqual(12345, intDeque[testedIndex]); + for(int index = testedIndex + 1; index < 97; ++index) { + Assert.AreEqual(index - 1, intDeque[index]); + } + } + } + + /// + /// Verifies that the Insert() method works in all cases when the deque doesn't + /// start at a block boundary + /// + [Test] + public void TestInsertNonNormalized() { + for(int testedIndex = 0; testedIndex <= 96; ++testedIndex) { + Deque intDeque = createNonNormalizedDeque(96); + + intDeque.Insert(testedIndex, 12345); + + Assert.AreEqual(97, intDeque.Count); + + for(int index = 0; index < testedIndex; ++index) { + Assert.AreEqual(index, intDeque[index]); + } + Assert.AreEqual(12345, intDeque[testedIndex]); + for(int index = testedIndex + 1; index < 97; ++index) { + Assert.AreEqual(index - 1, intDeque[index]); + } + } + } + + /// Verifies the the RemoveAt() method works in all cases + [Test] + public void TestRemoveAt() { + for(int testedIndex = 0; testedIndex < 96; ++testedIndex) { + Deque intDeque = new Deque(16); + for(int item = 0; item < 96; ++item) { + intDeque.AddLast(item); + } + + intDeque.RemoveAt(testedIndex); + + Assert.AreEqual(95, intDeque.Count); + + for(int index = 0; index < testedIndex; ++index) { + Assert.AreEqual(index, intDeque[index]); + } + for(int index = testedIndex; index < 95; ++index) { + Assert.AreEqual(index + 1, intDeque[index]); + } + } + } + + /// + /// Verifies the the RemoveAt() method works in all cases when the deque doesn't + /// start at a block boundary + /// + [Test] + public void TestRemoveAtNonNormalized() { + for(int testedIndex = 0; testedIndex < 96; ++testedIndex) { + Deque intDeque = new Deque(16); + for(int item = 4; item < 96; ++item) { + intDeque.AddLast(item); + } + intDeque.AddFirst(3); + intDeque.AddFirst(2); + intDeque.AddFirst(1); + intDeque.AddFirst(0); + + intDeque.RemoveAt(testedIndex); + + Assert.AreEqual(95, intDeque.Count); + + for(int index = 0; index < testedIndex; ++index) { + Assert.AreEqual(index, intDeque[index]); + } + for(int index = testedIndex; index < 95; ++index) { + Assert.AreEqual(index + 1, intDeque[index]); + } + } + } + + /// + /// Tests whether the RemoveAt() method keeps the state of the deque intact when + /// it has to remove a block from the left end of the deque + /// + [Test] + public void TestRemoveAtEmptiesLeftBlock() { + Deque intDeque = new Deque(16); + for(int item = 1; item <= 16; ++item) { + intDeque.AddLast(item); + } + intDeque.AddFirst(0); + intDeque.RemoveAt(3); + + Assert.AreEqual(16, intDeque.Count); + + for(int index = 0; index < 3; ++index) { + Assert.AreEqual(index, intDeque[index]); + } + for(int index = 3; index < 16; ++index) { + Assert.AreEqual(index + 1, intDeque[index]); + } + } + + /// + /// Tests whether the RemoveAt() method keeps the state of the deque intact when + /// it has to remove a block from the right end of the deque + /// + [Test] + public void TestRemoveAtEmptiesRightBlock() { + Deque intDeque = new Deque(16); + for(int item = 0; item <= 16; ++item) { + intDeque.AddLast(item); + } + intDeque.RemoveAt(13); + + Assert.AreEqual(16, intDeque.Count); + + for(int index = 0; index < 13; ++index) { + Assert.AreEqual(index, intDeque[index]); + } + for(int index = 13; index < 16; ++index) { + Assert.AreEqual(index + 1, intDeque[index]); + } + } + + /// + /// Validates that an exception is thrown if the 'First' property is accessed + /// in an empty deque + /// + [Test] + public void TestThrowOnAccessFirstInEmptyDeque() { + Deque intDeque = new Deque(); + Assert.Throws( + delegate() { Console.WriteLine(intDeque.First); } + ); + } + + /// + /// Validates that an exception is thrown if the 'Last' property is accessed + /// in an empty deque + /// + [Test] + public void TestThrowOnAccessLastInEmptyDeque() { + Deque intDeque = new Deque(); + Assert.Throws( + delegate() { Console.WriteLine(intDeque.Last); } + ); + } + + /// + /// Validates that an exception is thrown if the first item is attempted to be + /// removed from an empty deque + /// + [Test] + public void TestThrowOnRemoveFirstFromEmptyDeque() { + Deque intDeque = new Deque(); + Assert.Throws( + delegate() { intDeque.RemoveFirst(); } + ); + } + + /// + /// Validates that an exception is thrown if the last item is attempted to be + /// removed from an empty deque + /// + [Test] + public void TestThrowOnRemoveLastFromEmptyDeque() { + Deque intDeque = new Deque(); + Assert.Throws( + delegate() { intDeque.RemoveLast(); } + ); + } + + /// + /// Verifies that items can be assigned by their index + /// + [Test] + public void TestIndexAssignment() { + Deque intDeque = createDeque(32); + intDeque[16] = 12345; + intDeque[17] = 54321; + + for(int index = 0; index < 16; ++index) { + intDeque.RemoveFirst(); + } + + Assert.AreEqual(12345, intDeque.First); + intDeque.RemoveFirst(); + Assert.AreEqual(54321, intDeque.First); + } + + /// + /// Verifies that an exception is thrown if an invalid index is accessed + /// + [Test] + public void TestThrowOnInvalidIndex() { + Deque intDeque = new Deque(16); + for(int item = 0; item < 32; ++item) { + intDeque.AddLast(item); + } + + Assert.Throws( + delegate() { Console.WriteLine(intDeque[32]); } + ); + } + + /// Tests the IndexOf() method + [Test, TestCase(0), TestCase(16), TestCase(32), TestCase(48)] + public void TestIndexOf(int count) { + Deque intDeque = new Deque(16); + for(int item = 0; item < count; ++item) { + intDeque.AddLast(item); + } + + for(int item = 0; item < count; ++item) { + Assert.AreEqual(item, intDeque.IndexOf(item)); + } + Assert.AreEqual(-1, intDeque.IndexOf(count)); + } + + /// + /// Tests the IndexOf() method with the deque not starting at a block boundary + /// + [Test, TestCase(0), TestCase(16), TestCase(32), TestCase(48)] + public void TestIndexOfNonNormalized(int count) { + Deque intDeque = createNonNormalizedDeque(count); + + for(int item = 0; item < count; ++item) { + Assert.AreEqual(item, intDeque.IndexOf(item)); + } + Assert.AreEqual(-1, intDeque.IndexOf(count)); + } + + /// Verifies that the deque's enumerator works + [Test] + public void TestEnumerator() { + Deque intDeque = createNonNormalizedDeque(40); + + for(int testRun = 0; testRun < 2; ++testRun) { + using(IEnumerator enumerator = intDeque.GetEnumerator()) { + for(int index = 0; index < intDeque.Count; ++index) { + Assert.IsTrue(enumerator.MoveNext()); + Assert.AreEqual(index, enumerator.Current); + } + + Assert.IsFalse(enumerator.MoveNext()); + + enumerator.Reset(); + } + } + } + + /// Verifies that the deque's enumerator works + [Test] + public void TestObjectEnumerator() { + Deque intDeque = createNonNormalizedDeque(40); + + for(int testRun = 0; testRun < 2; ++testRun) { + IEnumerator enumerator = ((IEnumerable)intDeque).GetEnumerator(); + for(int index = 0; index < intDeque.Count; ++index) { + Assert.IsTrue(enumerator.MoveNext()); + Assert.AreEqual(index, enumerator.Current); + } + + Assert.IsFalse(enumerator.MoveNext()); + + enumerator.Reset(); + } + } + + /// + /// Verifies that an exception is thrown if the enumerator is accessed in + /// an invalid position + /// + [Test] + public void TestThrowOnInvalidEnumeratorPosition() { + Deque intDeque = createNonNormalizedDeque(40); + + using(IEnumerator enumerator = intDeque.GetEnumerator()) { + Assert.Throws( + delegate() { Console.WriteLine(enumerator.Current); } + ); + + while(enumerator.MoveNext()) { } + + Assert.Throws( + delegate() { Console.WriteLine(enumerator.Current); } + ); + } + } + + /// Tests whether a small deque can be cleared + [Test] + public void TestClearSmallDeque() { + Deque intDeque = createDeque(12); + intDeque.Clear(); + Assert.AreEqual(0, intDeque.Count); + } + + /// Tests whether a large deque can be cleared + [Test] + public void TestClearLargeDeque() { + Deque intDeque = createDeque(40); + intDeque.Clear(); + Assert.AreEqual(0, intDeque.Count); + } + + /// Verifies that the non-typesafe Add() method is working + [Test] + public void TestAddObject() { + Deque intDeque = new Deque(); + Assert.AreEqual(0, ((IList)intDeque).Add(123)); + Assert.AreEqual(1, intDeque.Count); + } + + /// + /// Tests whether an exception is thrown if the non-typesafe Add() method is + /// used to add an incompatible object into the deque + /// + [Test] + public void TestThrowOnAddIncompatibleObject() { + Deque intDeque = new Deque(); + Assert.Throws( + delegate() { ((IList)intDeque).Add("Hello World"); } + ); + } + + /// Verifies that the Add() method is working + [Test] + public void TestAdd() { + Deque intDeque = new Deque(); + ((IList)intDeque).Add(123); + Assert.AreEqual(1, intDeque.Count); + } + + /// Tests whether the Contains() method is working + [Test] + public void TestContains() { + Deque intDeque = createDeque(16); + Assert.IsTrue(intDeque.Contains(14)); + Assert.IsFalse(intDeque.Contains(16)); + } + + /// Tests the non-typesafe Contains() method + [Test] + public void TestContainsObject() { + Deque intDeque = createDeque(16); + Assert.IsTrue(((IList)intDeque).Contains(14)); + Assert.IsFalse(((IList)intDeque).Contains(16)); + Assert.IsFalse(((IList)intDeque).Contains("Hello World")); + } + + /// Tests the non-typesafe Contains() method + [Test] + public void TestIndexOfObject() { + Deque intDeque = createDeque(16); + Assert.AreEqual(14, ((IList)intDeque).IndexOf(14)); + Assert.AreEqual(-1, ((IList)intDeque).IndexOf(16)); + Assert.AreEqual(-1, ((IList)intDeque).IndexOf("Hello World")); + } + + /// Tests wether the non-typesafe Insert() method is working + [Test] + public void TestInsertObject() { + for(int testedIndex = 0; testedIndex <= 96; ++testedIndex) { + Deque intDeque = createDeque(96); + + ((IList)intDeque).Insert(testedIndex, 12345); + + Assert.AreEqual(97, intDeque.Count); + + for(int index = 0; index < testedIndex; ++index) { + Assert.AreEqual(index, intDeque[index]); + } + Assert.AreEqual(12345, intDeque[testedIndex]); + for(int index = testedIndex + 1; index < 97; ++index) { + Assert.AreEqual(index - 1, intDeque[index]); + } + } + } + + /// + /// Verifies that an exception is thrown if an incompatible object is inserted + /// into the deque + /// + [Test] + public void TestThrowOnInsertIncompatibleObject() { + Deque intDeque = createDeque(12); + Assert.Throws( + delegate() { ((IList)intDeque).Insert(8, "Hello World"); } + ); + } + + /// Validates that the IsFixedObject property is set to false + [Test] + public void TestIsFixedObject() { + Deque intDeque = new Deque(); + Assert.IsFalse(((IList)intDeque).IsFixedSize); + } + + /// Validates that the IsSynchronized property is set to false + [Test] + public void TestIsSynchronized() { + Deque intDeque = new Deque(); + Assert.IsFalse(((IList)intDeque).IsSynchronized); + } + + /// + /// Verifies that items can be assigned by their index using the non-typesafe + /// IList interface + /// + [Test] + public void TestObjectIndexAssignment() { + Deque intDeque = createDeque(32); + + ((IList)intDeque)[16] = 12345; + ((IList)intDeque)[17] = 54321; + + Assert.AreEqual(12345, ((IList)intDeque)[16]); + Assert.AreEqual(54321, ((IList)intDeque)[17]); + } + + /// + /// Tests whether an exception is thrown if an incompatible object is assigned + /// to the deque + /// + [Test] + public void TestIncompatibleObjectIndexAssignment() { + Deque intDeque = createDeque(2); + Assert.Throws( + delegate() { ((IList)intDeque)[0] = "Hello World"; } + ); + } + + /// Verifies that the Remove() method is working correctly + [Test] + public void TestRemove() { + Deque intDeque = createDeque(16); + Assert.AreEqual(16, intDeque.Count); + Assert.IsTrue(intDeque.Remove(13)); + Assert.IsFalse(intDeque.Remove(13)); + Assert.AreEqual(15, intDeque.Count); + } + + /// Tests the non-typesafe remove method + [Test] + public void TestRemoveObject() { + Deque intDeque = createDeque(10); + Assert.IsTrue(intDeque.Contains(8)); + Assert.AreEqual(10, intDeque.Count); + ((IList)intDeque).Remove(8); + Assert.IsFalse(intDeque.Contains(8)); + Assert.AreEqual(9, intDeque.Count); + } + + /// + /// Tests the non-typesafe remove method used to remove an incompatible object + /// + [Test] + public void TestRemoveIncompatibleObject() { + Deque intDeque = createDeque(10); + ((IList)intDeque).Remove("Hello World"); // should simply do nothing + Assert.AreEqual(10, intDeque.Count); + } + + /// + /// Verifies that the IsSynchronized property and the SyncRoot property are working + /// + [Test] + public void TestSynchronization() { + Deque intDeque = new Deque(); + + if(!(intDeque as ICollection).IsSynchronized) { + lock((intDeque as ICollection).SyncRoot) { + Assert.AreEqual(0, intDeque.Count); + } + } + } + + /// + /// Validates that the IsReadOnly property of the deque returns false + /// + [Test] + public void TestIsReadOnly() { + Deque intDeque = new Deque(); + Assert.IsFalse(((IList)intDeque).IsReadOnly); + Assert.IsFalse(((ICollection)intDeque).IsReadOnly); + } + + /// Tests the non-typesafe CopyTo() method + [Test] + public void TestCopyToObjectArray() { + Deque intDeque = createNonNormalizedDeque(40); + + int[] array = new int[40]; + ((ICollection)intDeque).CopyTo(array, 0); + + Assert.AreEqual(intDeque, array); + } + + /// Tests the CopyTo() method + [Test] + public void TestCopyToArray() { + Deque intDeque = createDeque(12); + intDeque.RemoveFirst(); + intDeque.RemoveFirst(); + + int[] array = new int[14]; + intDeque.CopyTo(array, 4); + + Assert.AreEqual( + new int[] { 0, 0, 0, 0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }, + array + ); + } + + /// + /// Verifies that the non-typesafe CopyTo() method throws an exception if + /// the array is of an incompatible type + /// + [Test] + public void TestThrowOnCopyToIncompatibleObjectArray() { + Deque intDeque = createDeque(4); + + short[] array = new short[4]; + Assert.Throws( + delegate() { ((ICollection)intDeque).CopyTo(array, 4); } + ); + } + + /// + /// Verifies that the CopyTo() method throws an exception if the target array + /// is too small + /// + [Test] + public void TestThrowOnCopyToTooSmallArray() { + Deque intDeque = createDeque(8); + Assert.Throws( + delegate() { intDeque.CopyTo(new int[7], 0); } + ); + } + +#if DEBUG + /// + /// Tests whether the deque enumerator detects when it runs out of sync + /// + [Test] + public void TestInvalidatedEnumeratorDetection() { + Deque intDeque = createDeque(8); + using(IEnumerator enumerator = intDeque.GetEnumerator()) { + Assert.IsTrue(enumerator.MoveNext()); + intDeque.AddFirst(12345); + Assert.Throws( + delegate() { enumerator.MoveNext(); } + ); + } + } +#endif + + /// + /// Creates a deque whose first element does not coincide with a block boundary + /// + /// Number of items the deque will be filled with + /// The newly created deque + private static Deque createNonNormalizedDeque(int count) { + Deque intDeque = new Deque(16); + + for(int item = 4; item < count; ++item) { + intDeque.AddLast(item); + } + if(count > 3) { intDeque.AddFirst(3); } + if(count > 2) { intDeque.AddFirst(2); } + if(count > 1) { intDeque.AddFirst(1); } + if(count > 0) { intDeque.AddFirst(0); } + + return intDeque; + } + + /// Creates a deque filled with the specified number of items + /// + /// Number of items the deque will be filled with + /// The newly created deque + private static Deque createDeque(int count) { + Deque intDeque = new Deque(16); + + for(int item = 0; item < count; ++item) { + intDeque.AddLast(item); + } + + return intDeque; + } + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST diff --git a/Source/Collections/Deque.cs b/Source/Collections/Deque.cs index 68f24dd..fcb3af9 100644 --- a/Source/Collections/Deque.cs +++ b/Source/Collections/Deque.cs @@ -1,334 +1,333 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; -using System.Collections; - -namespace Nuclex.Support.Collections { - - /// A double-ended queue that allocates memory in blocks - /// Type of the items being stored in the queue - /// - /// - /// The double-ended queue allows items to be appended to either side of the queue - /// without a hefty toll on performance. Like its namesake in C++, it is implemented - /// using multiple arrays. - /// - /// - /// Therefore, it's not only good at coping with lists that are modified at their - /// beginning, but also at handling huge data sets since enlarging the deque doesn't - /// require items to be copied around and it still can be accessed by index. - /// - /// - public partial class Deque : IList, IList { - - #region class Enumerator - - /// Enumerates over the items in a deque - private class Enumerator : IEnumerator, IEnumerator { - - /// Initializes a new deque enumerator - /// Deque whose items will be enumerated - public Enumerator(Deque deque) { - this.deque = deque; - this.blockSize = this.deque.blockSize; - this.lastBlock = this.deque.blocks.Count - 1; - this.lastBlockEndIndex = this.deque.lastBlockEndIndex - 1; - - Reset(); - } - - /// Immediately releases all resources owned by the instance - public void Dispose() { - this.deque = null; - this.currentBlock = null; - } - - /// The item at the enumerator's current position - public TItem Current { - get { -#if DEBUG - checkVersion(); -#endif - if (this.currentBlock == null) { - throw new InvalidOperationException("Enumerator is not on a valid position"); - } - - return this.currentBlock[this.subIndex]; - } - } - - /// Advances the enumerator to the next item - /// True if there was a next item - public bool MoveNext() { - -#if DEBUG - checkVersion(); -#endif - - // If we haven't reached the last block yet - if (this.currentBlockIndex < this.lastBlock) { - - // Advance to the next item. If the end of the current block is reached, - // go to the next block's first item - ++this.subIndex; - if (this.subIndex >= this.blockSize) { - ++this.currentBlockIndex; - this.currentBlock = this.deque.blocks[this.currentBlockIndex]; - if (this.currentBlockIndex == 0) { - this.subIndex = this.deque.firstBlockStartIndex; - } else { - this.subIndex = 0; - } - } - - // Item found. If the current block wasn't the last block, an item *has* - // to follow since otherwise, no further blocks would exist! - return true; - - } else { // We in or beyond the last block - - // Are there any items left to advance to? - if (this.subIndex < this.lastBlockEndIndex) { - ++this.subIndex; - return true; - } else { // Nope, we've reached the end of the deque - this.currentBlock = null; - return false; - } - - } - - } - - /// Resets the enumerator to its initial position - public void Reset() { - this.currentBlock = null; - this.currentBlockIndex = -1; - this.subIndex = this.deque.blockSize - 1; -#if DEBUG - this.expectedVersion = this.deque.version; -#endif - } - - /// The item at the enumerator's current position - object IEnumerator.Current { - get { return Current; } - } - -#if DEBUG - /// Ensures that the deque has not changed - private void checkVersion() { - if(this.expectedVersion != this.deque.version) - throw new InvalidOperationException("Deque has been modified"); - } -#endif - - /// Deque the enumerator belongs to - private Deque deque; - /// Size of the blocks in the deque - private int blockSize; - /// Index of the last block in the deque - private int lastBlock; - /// End index of the items in the deque's last block - private int lastBlockEndIndex; - - /// Index of the block the enumerator currently is in - private int currentBlockIndex; - /// Reference to the block being enumerated - private TItem[] currentBlock; - /// Index in the current block - private int subIndex; -#if DEBUG - /// Version the deque is expected to have - private int expectedVersion; -#endif - } - - #endregion // class Enumerator - - /// Initializes a new deque - public Deque() : this(512) { } - - /// Initializes a new deque using the specified block size - /// Size of the individual memory blocks used - public Deque(int blockSize) { - this.blockSize = blockSize; - - this.blocks = new List(); - this.blocks.Add(new TItem[this.blockSize]); - } - - /// Number of items contained in the double ended queue - public int Count { - get { return this.count; } - } - - /// Accesses an item by its index - /// Index of the item that will be accessed - /// The item at the specified index - public TItem this[int index] { - get { - int blockIndex, subIndex; - findIndex(index, out blockIndex, out subIndex); - - return this.blocks[blockIndex][subIndex]; - } - set { - int blockIndex, subIndex; - findIndex(index, out blockIndex, out subIndex); - - this.blocks[blockIndex][subIndex] = value; - } - } - - /// The first item in the double-ended queue - public TItem First { - get { - if (this.count == 0) { - throw new InvalidOperationException("The deque is empty"); - } - return this.blocks[0][this.firstBlockStartIndex]; - } - } - - /// The last item in the double-ended queue - public TItem Last { - get { - if (this.count == 0) { - throw new InvalidOperationException("The deque is empty"); - } - return this.blocks[this.blocks.Count - 1][this.lastBlockEndIndex - 1]; - } - } - - /// Determines whether the deque contains the specified item - /// Item the deque will be scanned for - /// True if the deque contains the item, false otherwise - public bool Contains(TItem item) { - return (IndexOf(item) != -1); - } - - /// Copies the contents of the deque into an array - /// Array the contents of the deque will be copied into - /// Array index the deque contents will begin at - public void CopyTo(TItem[] array, int arrayIndex) { - if (this.count > (array.Length - arrayIndex)) { - throw new ArgumentException( - "Array too small to hold the collection items starting at the specified index" - ); - } - - if (this.blocks.Count == 1) { // Does only one block exist? - - // Copy the one and only block there is - Array.Copy( - this.blocks[0], this.firstBlockStartIndex, - array, arrayIndex, - this.lastBlockEndIndex - this.firstBlockStartIndex - ); - - } else { // Multiple blocks exist - - // Copy the first block which is filled from the start index to its end - int length = this.blockSize - this.firstBlockStartIndex; - Array.Copy( - this.blocks[0], this.firstBlockStartIndex, - array, arrayIndex, - length - ); - arrayIndex += length; - - // Copy all intermediate blocks (if there are any). These are completely filled - int lastBlock = this.blocks.Count - 1; - for (int index = 1; index < lastBlock; ++index) { - Array.Copy( - this.blocks[index], 0, - array, arrayIndex, - this.blockSize - ); - arrayIndex += this.blockSize; - } - - // Copy the final block which is filled from the beginning to the end index - Array.Copy( - this.blocks[lastBlock], 0, - array, arrayIndex, - this.lastBlockEndIndex - ); - - } - } - - /// Obtains a new enumerator for the contents of the deque - /// The new enumerator - public IEnumerator GetEnumerator() { - return new Enumerator(this); - } - - /// Calculates the block index and local sub index of an entry - /// Index of the entry that will be located - /// Index of the block the entry is contained in - /// Local sub index of the entry within the block - private void findIndex(int index, out int blockIndex, out int subIndex) { - if ((index < 0) || (index >= this.count)) { - throw new ArgumentOutOfRangeException("Index out of range", "index"); - } - - index += this.firstBlockStartIndex; - blockIndex = Math.DivRem(index, this.blockSize, out subIndex); - } - - /// - /// Determines whether the provided object can be placed in the deque - /// - /// Value that will be checked for compatibility - /// True if the value can be placed in the deque - private static bool isCompatibleObject(object value) { - return ((value is TItem) || ((value == null) && !typeof(TItem).IsValueType)); - } - - /// Verifies that the provided object matches the deque's type - /// Value that will be checked for compatibility - private static void verifyCompatibleObject(object value) { - if (!isCompatibleObject(value)) { - throw new ArgumentException("Value does not match the deque's type", "value"); - } - } - - /// Number if items currently stored in the deque - private int count; - /// Size of a single deque block - private int blockSize; - /// Memory blocks being used to store the deque's data - private List blocks; - /// Starting index of data in the first block - private int firstBlockStartIndex; - /// End index of data in the last block - private int lastBlockEndIndex; -#if DEBUG - /// Used to detect when enumerators go out of sync - private int version; -#endif - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; +using System.Collections; + +namespace Nuclex.Support.Collections { + + /// A double-ended queue that allocates memory in blocks + /// Type of the items being stored in the queue + /// + /// + /// The double-ended queue allows items to be appended to either side of the queue + /// without a hefty toll on performance. Like its namesake in C++, it is implemented + /// using multiple arrays. + /// + /// + /// Therefore, it's not only good at coping with lists that are modified at their + /// beginning, but also at handling huge data sets since enlarging the deque doesn't + /// require items to be copied around and it still can be accessed by index. + /// + /// + public partial class Deque : IList, IList { + + #region class Enumerator + + /// Enumerates over the items in a deque + private class Enumerator : IEnumerator, IEnumerator { + + /// Initializes a new deque enumerator + /// Deque whose items will be enumerated + public Enumerator(Deque deque) { + this.deque = deque; + this.blockSize = this.deque.blockSize; + this.lastBlock = this.deque.blocks.Count - 1; + this.lastBlockEndIndex = this.deque.lastBlockEndIndex - 1; + + Reset(); + } + + /// Immediately releases all resources owned by the instance + public void Dispose() { + this.deque = null; + this.currentBlock = null; + } + + /// The item at the enumerator's current position + public TItem Current { + get { +#if DEBUG + checkVersion(); +#endif + if (this.currentBlock == null) { + throw new InvalidOperationException("Enumerator is not on a valid position"); + } + + return this.currentBlock[this.subIndex]; + } + } + + /// Advances the enumerator to the next item + /// True if there was a next item + public bool MoveNext() { + +#if DEBUG + checkVersion(); +#endif + + // If we haven't reached the last block yet + if (this.currentBlockIndex < this.lastBlock) { + + // Advance to the next item. If the end of the current block is reached, + // go to the next block's first item + ++this.subIndex; + if (this.subIndex >= this.blockSize) { + ++this.currentBlockIndex; + this.currentBlock = this.deque.blocks[this.currentBlockIndex]; + if (this.currentBlockIndex == 0) { + this.subIndex = this.deque.firstBlockStartIndex; + } else { + this.subIndex = 0; + } + } + + // Item found. If the current block wasn't the last block, an item *has* + // to follow since otherwise, no further blocks would exist! + return true; + + } else { // We in or beyond the last block + + // Are there any items left to advance to? + if (this.subIndex < this.lastBlockEndIndex) { + ++this.subIndex; + return true; + } else { // Nope, we've reached the end of the deque + this.currentBlock = null; + return false; + } + + } + + } + + /// Resets the enumerator to its initial position + public void Reset() { + this.currentBlock = null; + this.currentBlockIndex = -1; + this.subIndex = this.deque.blockSize - 1; +#if DEBUG + this.expectedVersion = this.deque.version; +#endif + } + + /// The item at the enumerator's current position + object IEnumerator.Current { + get { return Current; } + } + +#if DEBUG + /// Ensures that the deque has not changed + private void checkVersion() { + if(this.expectedVersion != this.deque.version) + throw new InvalidOperationException("Deque has been modified"); + } +#endif + + /// Deque the enumerator belongs to + private Deque deque; + /// Size of the blocks in the deque + private int blockSize; + /// Index of the last block in the deque + private int lastBlock; + /// End index of the items in the deque's last block + private int lastBlockEndIndex; + + /// Index of the block the enumerator currently is in + private int currentBlockIndex; + /// Reference to the block being enumerated + private TItem[] currentBlock; + /// Index in the current block + private int subIndex; +#if DEBUG + /// Version the deque is expected to have + private int expectedVersion; +#endif + } + + #endregion // class Enumerator + + /// Initializes a new deque + public Deque() : this(512) { } + + /// Initializes a new deque using the specified block size + /// Size of the individual memory blocks used + public Deque(int blockSize) { + this.blockSize = blockSize; + + this.blocks = new List(); + this.blocks.Add(new TItem[this.blockSize]); + } + + /// Number of items contained in the double ended queue + public int Count { + get { return this.count; } + } + + /// Accesses an item by its index + /// Index of the item that will be accessed + /// The item at the specified index + public TItem this[int index] { + get { + int blockIndex, subIndex; + findIndex(index, out blockIndex, out subIndex); + + return this.blocks[blockIndex][subIndex]; + } + set { + int blockIndex, subIndex; + findIndex(index, out blockIndex, out subIndex); + + this.blocks[blockIndex][subIndex] = value; + } + } + + /// The first item in the double-ended queue + public TItem First { + get { + if (this.count == 0) { + throw new InvalidOperationException("The deque is empty"); + } + return this.blocks[0][this.firstBlockStartIndex]; + } + } + + /// The last item in the double-ended queue + public TItem Last { + get { + if (this.count == 0) { + throw new InvalidOperationException("The deque is empty"); + } + return this.blocks[this.blocks.Count - 1][this.lastBlockEndIndex - 1]; + } + } + + /// Determines whether the deque contains the specified item + /// Item the deque will be scanned for + /// True if the deque contains the item, false otherwise + public bool Contains(TItem item) { + return (IndexOf(item) != -1); + } + + /// Copies the contents of the deque into an array + /// Array the contents of the deque will be copied into + /// Array index the deque contents will begin at + public void CopyTo(TItem[] array, int arrayIndex) { + if (this.count > (array.Length - arrayIndex)) { + throw new ArgumentException( + "Array too small to hold the collection items starting at the specified index" + ); + } + + if (this.blocks.Count == 1) { // Does only one block exist? + + // Copy the one and only block there is + Array.Copy( + this.blocks[0], this.firstBlockStartIndex, + array, arrayIndex, + this.lastBlockEndIndex - this.firstBlockStartIndex + ); + + } else { // Multiple blocks exist + + // Copy the first block which is filled from the start index to its end + int length = this.blockSize - this.firstBlockStartIndex; + Array.Copy( + this.blocks[0], this.firstBlockStartIndex, + array, arrayIndex, + length + ); + arrayIndex += length; + + // Copy all intermediate blocks (if there are any). These are completely filled + int lastBlock = this.blocks.Count - 1; + for (int index = 1; index < lastBlock; ++index) { + Array.Copy( + this.blocks[index], 0, + array, arrayIndex, + this.blockSize + ); + arrayIndex += this.blockSize; + } + + // Copy the final block which is filled from the beginning to the end index + Array.Copy( + this.blocks[lastBlock], 0, + array, arrayIndex, + this.lastBlockEndIndex + ); + + } + } + + /// Obtains a new enumerator for the contents of the deque + /// The new enumerator + public IEnumerator GetEnumerator() { + return new Enumerator(this); + } + + /// Calculates the block index and local sub index of an entry + /// Index of the entry that will be located + /// Index of the block the entry is contained in + /// Local sub index of the entry within the block + private void findIndex(int index, out int blockIndex, out int subIndex) { + if ((index < 0) || (index >= this.count)) { + throw new ArgumentOutOfRangeException("Index out of range", "index"); + } + + index += this.firstBlockStartIndex; + blockIndex = Math.DivRem(index, this.blockSize, out subIndex); + } + + /// + /// Determines whether the provided object can be placed in the deque + /// + /// Value that will be checked for compatibility + /// True if the value can be placed in the deque + private static bool isCompatibleObject(object value) { + return ((value is TItem) || ((value == null) && !typeof(TItem).IsValueType)); + } + + /// Verifies that the provided object matches the deque's type + /// Value that will be checked for compatibility + private static void verifyCompatibleObject(object value) { + if (!isCompatibleObject(value)) { + throw new ArgumentException("Value does not match the deque's type", "value"); + } + } + + /// Number if items currently stored in the deque + private int count; + /// Size of a single deque block + private int blockSize; + /// Memory blocks being used to store the deque's data + private List blocks; + /// Starting index of data in the first block + private int firstBlockStartIndex; + /// End index of data in the last block + private int lastBlockEndIndex; +#if DEBUG + /// Used to detect when enumerators go out of sync + private int version; +#endif + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/IListExtensions.Test.cs b/Source/Collections/IListExtensions.Test.cs index 7bd8240..6ca66e4 100644 --- a/Source/Collections/IListExtensions.Test.cs +++ b/Source/Collections/IListExtensions.Test.cs @@ -1,137 +1,136 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; -using System.IO; - -#if UNITTEST - -using NUnit.Framework; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the IList extension methods - [TestFixture] - internal class IListExtensionsTest { - - /// Tests whether the insertion sort algorithm sorts a list correctly - [Test] - public void InsertionSortCanSortWholeList() { - var testList = new List(capacity: 5) { 1, 5, 2, 4, 3 }; - var testListAsIList = (IList)testList; - - testListAsIList.InsertionSort(); - - CollectionAssert.AreEqual( - new List(capacity: 5) { 1, 2, 3, 4, 5 }, - testList - ); - } - - /// Tests whether the insertion sort algorithm works on big lists - [Test] - public void InsertionSortCanSortBigList() { - const int ListSize = 16384; - - var testList = new List(capacity: ListSize); - { - var random = new Random(); - for(int index = 0; index < ListSize; ++index) { - testList.Add(random.Next()); - } - } - - var testListAsIList = (IList)testList; - testListAsIList.InsertionSort(); - - for(int index = 1; index < ListSize; ++index) { - Assert.LessOrEqual(testListAsIList[index - 1], testListAsIList[index]); - } - } - - /// Tests whether the insertion sort algorithm respects custom boundaries - [Test] - public void InsertionSortCanSortListSegment() { - var testList = new List(capacity: 7) { 9, 1, 5, 2, 4, 3, 0 }; - var testListAsIList = (IList)testList; - - testListAsIList.InsertionSort(1, 5, Comparer.Default); - - CollectionAssert.AreEqual( - new List(capacity: 7) { 9, 1, 2, 3, 4, 5, 0 }, - testList - ); - } - - /// Tests whether the quicksort algorithm sorts a list correctly - [Test] - public void QuickSortCanSortWholeList() { - var testList = new List(capacity: 5) { 1, 5, 2, 4, 3 }; - var testListAsIList = (IList)testList; - - testListAsIList.QuickSort(); - - CollectionAssert.AreEqual( - new List(capacity: 5) { 1, 2, 3, 4, 5 }, - testList - ); - } - - /// Tests whether the quicksort algorithm works on big lists - [Test] - public void QuickSortCanSortBigList() { - const int ListSize = 16384; - - var testList = new List(capacity: ListSize); - { - var random = new Random(); - for(int index = 0; index < ListSize; ++index) { - testList.Add(random.Next()); - } - } - - var testListAsIList = (IList)testList; - testListAsIList.QuickSort(); - - for(int index = 1; index < ListSize; ++index) { - Assert.LessOrEqual(testListAsIList[index - 1], testListAsIList[index]); - } - } - - /// Tests whether the quicksort algorithm respects custom boundaries - [Test] - public void QuickSortCanSortListSegment() { - var testList = new List(capacity: 7) { 9, 1, 5, 2, 4, 3, 0 }; - var testListAsIList = (IList)testList; - - testListAsIList.QuickSort(1, 5, Comparer.Default); - - CollectionAssert.AreEqual( - new List(capacity: 7) { 9, 1, 2, 3, 4, 5, 0 }, - testList - ); - } - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; +using System.IO; + +#if UNITTEST + +using NUnit.Framework; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the IList extension methods + [TestFixture] + internal class IListExtensionsTest { + + /// Tests whether the insertion sort algorithm sorts a list correctly + [Test] + public void InsertionSortCanSortWholeList() { + var testList = new List(capacity: 5) { 1, 5, 2, 4, 3 }; + var testListAsIList = (IList)testList; + + testListAsIList.InsertionSort(); + + CollectionAssert.AreEqual( + new List(capacity: 5) { 1, 2, 3, 4, 5 }, + testList + ); + } + + /// Tests whether the insertion sort algorithm works on big lists + [Test] + public void InsertionSortCanSortBigList() { + const int ListSize = 16384; + + var testList = new List(capacity: ListSize); + { + var random = new Random(); + for(int index = 0; index < ListSize; ++index) { + testList.Add(random.Next()); + } + } + + var testListAsIList = (IList)testList; + testListAsIList.InsertionSort(); + + for(int index = 1; index < ListSize; ++index) { + Assert.LessOrEqual(testListAsIList[index - 1], testListAsIList[index]); + } + } + + /// Tests whether the insertion sort algorithm respects custom boundaries + [Test] + public void InsertionSortCanSortListSegment() { + var testList = new List(capacity: 7) { 9, 1, 5, 2, 4, 3, 0 }; + var testListAsIList = (IList)testList; + + testListAsIList.InsertionSort(1, 5, Comparer.Default); + + CollectionAssert.AreEqual( + new List(capacity: 7) { 9, 1, 2, 3, 4, 5, 0 }, + testList + ); + } + + /// Tests whether the quicksort algorithm sorts a list correctly + [Test] + public void QuickSortCanSortWholeList() { + var testList = new List(capacity: 5) { 1, 5, 2, 4, 3 }; + var testListAsIList = (IList)testList; + + testListAsIList.QuickSort(); + + CollectionAssert.AreEqual( + new List(capacity: 5) { 1, 2, 3, 4, 5 }, + testList + ); + } + + /// Tests whether the quicksort algorithm works on big lists + [Test] + public void QuickSortCanSortBigList() { + const int ListSize = 16384; + + var testList = new List(capacity: ListSize); + { + var random = new Random(); + for(int index = 0; index < ListSize; ++index) { + testList.Add(random.Next()); + } + } + + var testListAsIList = (IList)testList; + testListAsIList.QuickSort(); + + for(int index = 1; index < ListSize; ++index) { + Assert.LessOrEqual(testListAsIList[index - 1], testListAsIList[index]); + } + } + + /// Tests whether the quicksort algorithm respects custom boundaries + [Test] + public void QuickSortCanSortListSegment() { + var testList = new List(capacity: 7) { 9, 1, 5, 2, 4, 3, 0 }; + var testListAsIList = (IList)testList; + + testListAsIList.QuickSort(1, 5, Comparer.Default); + + CollectionAssert.AreEqual( + new List(capacity: 7) { 9, 1, 2, 3, 4, 5, 0 }, + testList + ); + } + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST diff --git a/Source/Collections/IListExtensions.cs b/Source/Collections/IListExtensions.cs index 5cbe824..06d693f 100644 --- a/Source/Collections/IListExtensions.cs +++ b/Source/Collections/IListExtensions.cs @@ -1,219 +1,218 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; - -namespace Nuclex.Support.Collections { - - /// Extension methods for the IList interface - public static class ListExtensions { - - #region struct Partition - - /// - /// Stores the left and right index of a partition for the quicksort algorithm - /// - private struct Partition { - - /// - /// Initializes a new partition using the specified left and right index - /// - /// Index of the leftmost element in the partition - /// Index of the rightmost element in the partition - public Partition(int leftmostIndex, int rightmostIndex) { - this.LeftmostIndex = leftmostIndex; - this.RightmostIndex = rightmostIndex; - } - - /// Index of the leftmost element in the partition - public int LeftmostIndex; - /// Index of the rightmost element in the partition - public int RightmostIndex; - - } - - #endregion // struct Partition - - /// - /// Sorts a subset of the elements in an IList<T> using the insertion sort algorithm - /// - /// Type of elements the list contains - /// List in which a subset will be sorted - /// Index at which the sorting process will begin - /// Index one past the last element that will be sorted - /// Comparison function to use for comparing list elements - public static void InsertionSort( - this IList list, int startIndex, int count, IComparer comparer - ) { - int rightIndex = startIndex; - - int lastIndex = startIndex + count - 1; - for(int index = startIndex + 1; index <= lastIndex; ++index) { - TElement temp = list[index]; - - while(rightIndex >= startIndex) { - if(comparer.Compare(list[rightIndex], temp) < 0) { - break; - } - - list[rightIndex + 1] = list[rightIndex]; - --rightIndex; - } - - list[rightIndex + 1] = temp; - - rightIndex = index; - } - } - - /// - /// Sorts all the elements in an IList<T> using the insertion sort algorithm - /// - /// Type of elements the list contains - /// List in which a subset will be sorted - /// Comparison function to use for comparing list elements - public static void InsertionSort( - this IList list, IComparer comparer - ) { - InsertionSort(list, 0, list.Count, comparer); - } - - /// - /// Sorts all the elements in an IList<T> using the insertion sort algorithm - /// - /// Type of elements the list contains - /// List in which a subset will be sorted - public static void InsertionSort(this IList list) { - InsertionSort(list, 0, list.Count, Comparer.Default); - } - - /// - /// Sorts all the elements in an IList<T> using the quicksort algorithm - /// - /// Type of elements the list contains - /// List in which a subset will be sorted - /// Index at which the sorting process will begin - /// Index one past the last element that will be sorted - /// Comparison function to use for comparing list elements - public static void QuickSort( - this IList list, int startIndex, int count, IComparer comparer - ) { - var remainingPartitions = new Stack(); - - int lastIndex = startIndex + count - 1; - for(; ; ) { - int pivotIndex = quicksortPartition(list, startIndex, lastIndex, comparer); - - // This block just queues the next partitions left of the pivot point and right - // of the pivot point (if they contain at least 2 elements). It's fattened up - // a bit by trying to forego the stack and adjusting the startIndex/lastIndex - // directly where it's clear the next loop can process these partitions. - if(pivotIndex - 1 > startIndex) { // Are the elements to sort right of the pivot? - if(pivotIndex + 1 < lastIndex) { // Are the elements left of the pivot as well? - remainingPartitions.Push(new Partition(startIndex, pivotIndex - 1)); - startIndex = pivotIndex + 1; - } else { // Elements to sort are only right of the pivot - lastIndex = pivotIndex - 1; - } - } else if(pivotIndex + 1 < lastIndex) { // Are elements to sort only left of the pivot? - startIndex = pivotIndex + 1; - } else { // Partition was fully sorted - - // Did we process all queued partitions? If so, the list is sorted - if(remainingPartitions.Count == 0) { - return; - } - - // Pull the next partition that needs to be sorted from the stack - Partition current = remainingPartitions.Pop(); - startIndex = current.LeftmostIndex; - lastIndex = current.RightmostIndex; - - } // if sortable sub-partitions exist left/right/nowhere - } // for ever (termination inside loop) - } - - /// - /// Sorts all the elements in an IList<T> using the insertion sort algorithm - /// - /// Type of elements the list contains - /// List in which a subset will be sorted - /// Comparison function to use for comparing list elements - public static void QuickSort( - this IList list, IComparer comparer - ) { - QuickSort(list, 0, list.Count, comparer); - } - - /// - /// Sorts all the elements in an IList<T> using the insertion sort algorithm - /// - /// Type of elements the list contains - /// List in which a subset will be sorted - public static void QuickSort(this IList list) { - QuickSort(list, 0, list.Count, Comparer.Default); - } - - /// - /// Moves an element downward over all elements that precede it in the sort order - /// - /// Type of elements stored in the sorted list - /// List that is being sorted - /// Index of the first element in the partition - /// Index of hte last element in the partition - /// - /// Comparison function that decides the ordering of elements - /// - /// The index of the next pivot element - private static int quicksortPartition( - IList list, int firstIndex, int lastIndex, IComparer comparer - ) { - - // Step through all elements in the partition and accumulate those that are smaller - // than the last element on the left (by swapping). At the end 'firstIndex' will be - // the new pivot point, left of which are all elements smaller than the element at - // 'lastIndex' and right of it will be all elements which are larger. - for(int index = firstIndex; index < lastIndex; ++index) { - if(comparer.Compare(list[index], list[lastIndex]) < 0) { - TElement temp = list[firstIndex]; - list[firstIndex] = list[index]; - list[index] = temp; - - ++firstIndex; - } - } - - // The element at 'lastIndex' as a sort value that's in the middle of the two sides, - // so we'll have to swap it, too, putting it in the middle and making it the new pivot. - { - TElement temp = list[firstIndex]; - list[firstIndex] = list[lastIndex]; - list[lastIndex] = temp; - } - - // Return the index of the new pivot position - return firstIndex; - - } - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; + +namespace Nuclex.Support.Collections { + + /// Extension methods for the IList interface + public static class ListExtensions { + + #region struct Partition + + /// + /// Stores the left and right index of a partition for the quicksort algorithm + /// + private struct Partition { + + /// + /// Initializes a new partition using the specified left and right index + /// + /// Index of the leftmost element in the partition + /// Index of the rightmost element in the partition + public Partition(int leftmostIndex, int rightmostIndex) { + this.LeftmostIndex = leftmostIndex; + this.RightmostIndex = rightmostIndex; + } + + /// Index of the leftmost element in the partition + public int LeftmostIndex; + /// Index of the rightmost element in the partition + public int RightmostIndex; + + } + + #endregion // struct Partition + + /// + /// Sorts a subset of the elements in an IList<T> using the insertion sort algorithm + /// + /// Type of elements the list contains + /// List in which a subset will be sorted + /// Index at which the sorting process will begin + /// Index one past the last element that will be sorted + /// Comparison function to use for comparing list elements + public static void InsertionSort( + this IList list, int startIndex, int count, IComparer comparer + ) { + int rightIndex = startIndex; + + int lastIndex = startIndex + count - 1; + for(int index = startIndex + 1; index <= lastIndex; ++index) { + TElement temp = list[index]; + + while(rightIndex >= startIndex) { + if(comparer.Compare(list[rightIndex], temp) < 0) { + break; + } + + list[rightIndex + 1] = list[rightIndex]; + --rightIndex; + } + + list[rightIndex + 1] = temp; + + rightIndex = index; + } + } + + /// + /// Sorts all the elements in an IList<T> using the insertion sort algorithm + /// + /// Type of elements the list contains + /// List in which a subset will be sorted + /// Comparison function to use for comparing list elements + public static void InsertionSort( + this IList list, IComparer comparer + ) { + InsertionSort(list, 0, list.Count, comparer); + } + + /// + /// Sorts all the elements in an IList<T> using the insertion sort algorithm + /// + /// Type of elements the list contains + /// List in which a subset will be sorted + public static void InsertionSort(this IList list) { + InsertionSort(list, 0, list.Count, Comparer.Default); + } + + /// + /// Sorts all the elements in an IList<T> using the quicksort algorithm + /// + /// Type of elements the list contains + /// List in which a subset will be sorted + /// Index at which the sorting process will begin + /// Index one past the last element that will be sorted + /// Comparison function to use for comparing list elements + public static void QuickSort( + this IList list, int startIndex, int count, IComparer comparer + ) { + var remainingPartitions = new Stack(); + + int lastIndex = startIndex + count - 1; + for(; ; ) { + int pivotIndex = quicksortPartition(list, startIndex, lastIndex, comparer); + + // This block just queues the next partitions left of the pivot point and right + // of the pivot point (if they contain at least 2 elements). It's fattened up + // a bit by trying to forego the stack and adjusting the startIndex/lastIndex + // directly where it's clear the next loop can process these partitions. + if(pivotIndex - 1 > startIndex) { // Are the elements to sort right of the pivot? + if(pivotIndex + 1 < lastIndex) { // Are the elements left of the pivot as well? + remainingPartitions.Push(new Partition(startIndex, pivotIndex - 1)); + startIndex = pivotIndex + 1; + } else { // Elements to sort are only right of the pivot + lastIndex = pivotIndex - 1; + } + } else if(pivotIndex + 1 < lastIndex) { // Are elements to sort only left of the pivot? + startIndex = pivotIndex + 1; + } else { // Partition was fully sorted + + // Did we process all queued partitions? If so, the list is sorted + if(remainingPartitions.Count == 0) { + return; + } + + // Pull the next partition that needs to be sorted from the stack + Partition current = remainingPartitions.Pop(); + startIndex = current.LeftmostIndex; + lastIndex = current.RightmostIndex; + + } // if sortable sub-partitions exist left/right/nowhere + } // for ever (termination inside loop) + } + + /// + /// Sorts all the elements in an IList<T> using the insertion sort algorithm + /// + /// Type of elements the list contains + /// List in which a subset will be sorted + /// Comparison function to use for comparing list elements + public static void QuickSort( + this IList list, IComparer comparer + ) { + QuickSort(list, 0, list.Count, comparer); + } + + /// + /// Sorts all the elements in an IList<T> using the insertion sort algorithm + /// + /// Type of elements the list contains + /// List in which a subset will be sorted + public static void QuickSort(this IList list) { + QuickSort(list, 0, list.Count, Comparer.Default); + } + + /// + /// Moves an element downward over all elements that precede it in the sort order + /// + /// Type of elements stored in the sorted list + /// List that is being sorted + /// Index of the first element in the partition + /// Index of hte last element in the partition + /// + /// Comparison function that decides the ordering of elements + /// + /// The index of the next pivot element + private static int quicksortPartition( + IList list, int firstIndex, int lastIndex, IComparer comparer + ) { + + // Step through all elements in the partition and accumulate those that are smaller + // than the last element on the left (by swapping). At the end 'firstIndex' will be + // the new pivot point, left of which are all elements smaller than the element at + // 'lastIndex' and right of it will be all elements which are larger. + for(int index = firstIndex; index < lastIndex; ++index) { + if(comparer.Compare(list[index], list[lastIndex]) < 0) { + TElement temp = list[firstIndex]; + list[firstIndex] = list[index]; + list[index] = temp; + + ++firstIndex; + } + } + + // The element at 'lastIndex' as a sort value that's in the middle of the two sides, + // so we'll have to swap it, too, putting it in the middle and making it the new pivot. + { + TElement temp = list[firstIndex]; + list[firstIndex] = list[lastIndex]; + list[lastIndex] = temp; + } + + // Return the index of the new pivot position + return firstIndex; + + } + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/IMultiDictionary.cs b/Source/Collections/IMultiDictionary.cs index b5d495e..87408cd 100644 --- a/Source/Collections/IMultiDictionary.cs +++ b/Source/Collections/IMultiDictionary.cs @@ -1,68 +1,67 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Nuclex.Support.Collections { - - /// - /// Associative collection that can store several values under one key and vice versa - /// - /// Type of keys used within the dictionary - /// Type of values stored in the dictionary - public interface IMultiDictionary : - IDictionary>, - IDictionary, - ICollection>, - IEnumerable>, - IEnumerable { - - /// Adds a value into the dictionary under the provided key - /// Key the value will be stored under - /// Value that will be stored under the specified key - void Add(TKey key, TValue value); - - /// Determines the number of values stored under the specified key - /// Key whose values will be counted - /// The number of values stored under the specified key - int CountValues(TKey key); - - /// - /// Removes the item with the specified key and value from the dictionary - /// - /// Key of the item that will be removed - /// Value of the item that will be removed - /// - /// True if the specified item was contained in the dictionary and was removed - /// - /// If the dictionary is read-only - bool Remove(TKey key, TValue value); - - /// Removes all items with the specified key from the dictionary - /// Key of the item that will be removed - /// The number of items that have been removed from the dictionary - /// If the dictionary is read-only - int RemoveKey(TKey key); - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Nuclex.Support.Collections { + + /// + /// Associative collection that can store several values under one key and vice versa + /// + /// Type of keys used within the dictionary + /// Type of values stored in the dictionary + public interface IMultiDictionary : + IDictionary>, + IDictionary, + ICollection>, + IEnumerable>, + IEnumerable { + + /// Adds a value into the dictionary under the provided key + /// Key the value will be stored under + /// Value that will be stored under the specified key + void Add(TKey key, TValue value); + + /// Determines the number of values stored under the specified key + /// Key whose values will be counted + /// The number of values stored under the specified key + int CountValues(TKey key); + + /// + /// Removes the item with the specified key and value from the dictionary + /// + /// Key of the item that will be removed + /// Value of the item that will be removed + /// + /// True if the specified item was contained in the dictionary and was removed + /// + /// If the dictionary is read-only + bool Remove(TKey key, TValue value); + + /// Removes all items with the specified key from the dictionary + /// Key of the item that will be removed + /// The number of items that have been removed from the dictionary + /// If the dictionary is read-only + int RemoveKey(TKey key); + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/IObservableCollection.cs b/Source/Collections/IObservableCollection.cs index e583d25..6a19bc0 100644 --- a/Source/Collections/IObservableCollection.cs +++ b/Source/Collections/IObservableCollection.cs @@ -1,51 +1,50 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; - -namespace Nuclex.Support.Collections { - - /// Interface for collections that can be observed - /// Type of items managed in the collection - public interface IObservableCollection { - - /// Raised when an item has been added to the collection - event EventHandler> ItemAdded; - - /// Raised when an item is removed from the collection - event EventHandler> ItemRemoved; - - /// Raised when an item is replaced in the collection - event EventHandler> ItemReplaced; - - /// Raised when the collection is about to be cleared - /// - /// This could be covered by calling ItemRemoved for each item currently - /// contained in the collection, but it is often simpler and more efficient - /// to process the clearing of the entire collection as a special operation. - /// - event EventHandler Clearing; - - /// Raised when the collection has been cleared of its items - event EventHandler Cleared; - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +namespace Nuclex.Support.Collections { + + /// Interface for collections that can be observed + /// Type of items managed in the collection + public interface IObservableCollection { + + /// Raised when an item has been added to the collection + event EventHandler> ItemAdded; + + /// Raised when an item is removed from the collection + event EventHandler> ItemRemoved; + + /// Raised when an item is replaced in the collection + event EventHandler> ItemReplaced; + + /// Raised when the collection is about to be cleared + /// + /// This could be covered by calling ItemRemoved for each item currently + /// contained in the collection, but it is often simpler and more efficient + /// to process the clearing of the entire collection as a special operation. + /// + event EventHandler Clearing; + + /// Raised when the collection has been cleared of its items + event EventHandler Cleared; + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/IRecyclable.cs b/Source/Collections/IRecyclable.cs index 370183f..cafbdba 100644 --- a/Source/Collections/IRecyclable.cs +++ b/Source/Collections/IRecyclable.cs @@ -1,45 +1,44 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; - -namespace Nuclex.Support.Collections { - - /// Allows an object to be returned to its initial state - /// - /// - /// This interface is typically implemented by objects which can be recycled - /// in order to avoid the construction overhead of a heavyweight class and to - /// eliminate garbage by reusing instances. - /// - /// - /// Recyclable objects should have a parameterless constructor and calling - /// their Recycle() method should bring them back into the state they were - /// in right after they had been constructed. - /// - /// - public interface IRecyclable { - - /// Returns the object to its initial state - void Recycle(); - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +namespace Nuclex.Support.Collections { + + /// Allows an object to be returned to its initial state + /// + /// + /// This interface is typically implemented by objects which can be recycled + /// in order to avoid the construction overhead of a heavyweight class and to + /// eliminate garbage by reusing instances. + /// + /// + /// Recyclable objects should have a parameterless constructor and calling + /// their Recycle() method should bring them back into the state they were + /// in right after they had been constructed. + /// + /// + public interface IRecyclable { + + /// Returns the object to its initial state + void Recycle(); + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/ItemEventArgs.Test.cs b/Source/Collections/ItemEventArgs.Test.cs index 66dbf54..62f4f9f 100644 --- a/Source/Collections/ItemEventArgs.Test.cs +++ b/Source/Collections/ItemEventArgs.Test.cs @@ -1,56 +1,55 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.Collections.Generic; - -using NUnit.Framework; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the item event argument container - [TestFixture] - internal class ItemEventArgsTest { - - /// - /// Tests whether an integer argument can be stored in the argument container - /// - [Test] - public void IntegersCanBeCarried() { - var test = new ItemEventArgs(12345); - Assert.AreEqual(12345, test.Item); - } - - /// - /// Tests whether a string argument can be stored in the argument container - /// - [Test] - public void StringsCanBeCarried() { - var test = new ItemEventArgs("hello world"); - Assert.AreEqual("hello world", test.Item); - } - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.Collections.Generic; + +using NUnit.Framework; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the item event argument container + [TestFixture] + internal class ItemEventArgsTest { + + /// + /// Tests whether an integer argument can be stored in the argument container + /// + [Test] + public void IntegersCanBeCarried() { + var test = new ItemEventArgs(12345); + Assert.AreEqual(12345, test.Item); + } + + /// + /// Tests whether a string argument can be stored in the argument container + /// + [Test] + public void StringsCanBeCarried() { + var test = new ItemEventArgs("hello world"); + Assert.AreEqual("hello world", test.Item); + } + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST diff --git a/Source/Collections/ItemEventArgs.cs b/Source/Collections/ItemEventArgs.cs index d485faf..67ad733 100644 --- a/Source/Collections/ItemEventArgs.cs +++ b/Source/Collections/ItemEventArgs.cs @@ -1,46 +1,45 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; - -namespace Nuclex.Support.Collections { - - /// - /// Argument container used by collections to notify about changed items - /// - public class ItemEventArgs : EventArgs { - - /// Initializes a new event arguments supplier - /// Item to be supplied to the event handler - public ItemEventArgs(TItem item) { - this.item = item; - } - - /// Obtains the collection item the event arguments are carrying - public TItem Item { - get { return this.item; } - } - - /// Item to be passed to the event handler - private TItem item; - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +namespace Nuclex.Support.Collections { + + /// + /// Argument container used by collections to notify about changed items + /// + public class ItemEventArgs : EventArgs { + + /// Initializes a new event arguments supplier + /// Item to be supplied to the event handler + public ItemEventArgs(TItem item) { + this.item = item; + } + + /// Obtains the collection item the event arguments are carrying + public TItem Item { + get { return this.item; } + } + + /// Item to be passed to the event handler + private TItem item; + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/ItemReplaceEventArgs.Test.cs b/Source/Collections/ItemReplaceEventArgs.Test.cs index 9e47e5f..2cdaf32 100644 --- a/Source/Collections/ItemReplaceEventArgs.Test.cs +++ b/Source/Collections/ItemReplaceEventArgs.Test.cs @@ -1,58 +1,57 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.Collections.Generic; - -using NUnit.Framework; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the item event argument container - [TestFixture] - internal class ItemReplaceEventArgsTest { - - /// - /// Tests whether an integer argument can be stored in the argument container - /// - [Test] - public void IntegersCanBeCarried() { - var test = new ItemReplaceEventArgs(12345, 54321); - Assert.AreEqual(12345, test.OldItem); - Assert.AreEqual(54321, test.NewItem); - } - - /// - /// Tests whether a string argument can be stored in the argument container - /// - [Test] - public void StringsCanBeCarried() { - var test = new ItemReplaceEventArgs("hello", "world"); - Assert.AreEqual("hello", test.OldItem); - Assert.AreEqual("world", test.NewItem); - } - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.Collections.Generic; + +using NUnit.Framework; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the item event argument container + [TestFixture] + internal class ItemReplaceEventArgsTest { + + /// + /// Tests whether an integer argument can be stored in the argument container + /// + [Test] + public void IntegersCanBeCarried() { + var test = new ItemReplaceEventArgs(12345, 54321); + Assert.AreEqual(12345, test.OldItem); + Assert.AreEqual(54321, test.NewItem); + } + + /// + /// Tests whether a string argument can be stored in the argument container + /// + [Test] + public void StringsCanBeCarried() { + var test = new ItemReplaceEventArgs("hello", "world"); + Assert.AreEqual("hello", test.OldItem); + Assert.AreEqual("world", test.NewItem); + } + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST diff --git a/Source/Collections/ItemReplaceEventArgs.cs b/Source/Collections/ItemReplaceEventArgs.cs index 5a8fba7..d0b1d6e 100644 --- a/Source/Collections/ItemReplaceEventArgs.cs +++ b/Source/Collections/ItemReplaceEventArgs.cs @@ -1,55 +1,54 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; - -namespace Nuclex.Support.Collections { - - /// - /// Argument container used by collections to notify about replaced items - /// - public class ItemReplaceEventArgs : EventArgs { - - /// Initializes a new event arguments supplier - /// Item that has been replaced by another item - /// Replacement item that is now part of the collection - public ItemReplaceEventArgs(TItem oldItem, TItem newItem) { - this.oldItem = oldItem; - this.newItem = newItem; - } - - /// Item that has been replaced by another item - public TItem OldItem { - get { return this.oldItem; } - } - - /// Replacement item that is now part of the collection - public TItem NewItem { - get { return this.newItem; } - } - - /// Item that was removed from the collection - private TItem oldItem; - /// Item that was added to the collection - private TItem newItem; - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +namespace Nuclex.Support.Collections { + + /// + /// Argument container used by collections to notify about replaced items + /// + public class ItemReplaceEventArgs : EventArgs { + + /// Initializes a new event arguments supplier + /// Item that has been replaced by another item + /// Replacement item that is now part of the collection + public ItemReplaceEventArgs(TItem oldItem, TItem newItem) { + this.oldItem = oldItem; + this.newItem = newItem; + } + + /// Item that has been replaced by another item + public TItem OldItem { + get { return this.oldItem; } + } + + /// Replacement item that is now part of the collection + public TItem NewItem { + get { return this.newItem; } + } + + /// Item that was removed from the collection + private TItem oldItem; + /// Item that was added to the collection + private TItem newItem; + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/ListSegment.Test.cs b/Source/Collections/ListSegment.Test.cs index d7da67b..04b2c7b 100644 --- a/Source/Collections/ListSegment.Test.cs +++ b/Source/Collections/ListSegment.Test.cs @@ -1,257 +1,256 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; -using System.IO; - -#if UNITTEST - -using NUnit.Framework; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the list segment class - [TestFixture] - internal class ListSegmentTest { - - /// - /// Tests whether the default constructor of the ListSegment class throws the - /// right exception when being passed 'null' instead of a list - /// - [Test] - public void SimpleConstructorThrowsWhenListIsNull() { - Assert.Throws( - delegate() { new ListSegment(null); } - ); - } - - /// - /// Tests whether the simple constructor of the ListSegment class accepts - /// an empty list - /// - [Test] - public void SimpleConstructorAcceptsEmptyList() { - new ListSegment(new List()); - } - - /// - /// Tests whether the full constructor of the ListSegment class throws the - /// right exception when being passed 'null' instead of a string - /// - [Test] - public void ConstructorThrowsWhenListIsNull() { - Assert.Throws( - delegate() { new ListSegment(null, 0, 0); } - ); - } - - /// - /// Tests whether the full constructor of the ListSegment class accepts - /// an empty string - /// - [Test] - public void ConstructorAcceptsEmptyList() { - new ListSegment(new List(), 0, 0); - } - - /// - /// Tests whether the full constructor of the ListSegment class throws the - /// right exception when being passed an invalid start offset - /// - [Test] - public void ConstructorThrowsOnInvalidOffset() { - Assert.Throws( - delegate() { new ListSegment(new List(), -1, 0); } - ); - } - - /// - /// Tests whether the full constructor of the ListSegment class throws the - /// right exception when being passed an invalid element count - /// - [Test] - public void ConstructorThrowsOnInvalidCount() { - Assert.Throws( - delegate() { new ListSegment(new List(), 0, -1); } - ); - } - - /// - /// Tests whether the full constructor of the ListSegment class throws the - /// right exception when being passed a string length that's too large - /// - [Test] - public void ConstructorThrowsOnListOverrun() { - var testList = new List(capacity: 5) { 1, 2, 3, 4, 5 }; - Assert.Throws( - delegate() { new ListSegment(testList, 3, 3); } - ); - } - - /// Tests whether the 'Text' property works as expected - [Test] - public void ListPropertyStoresOriginalList() { - var testList = new List(capacity: 5) { 1, 2, 3, 4, 5 }; - ListSegment testSegment = new ListSegment(testList, 1, 3); - Assert.AreSame(testList, testSegment.List); - } - - /// Tests whether the 'Offset' property works as expected - [Test] - public void OffsetPropertyIsStored() { - var testList = new List(capacity: 5) { 1, 2, 3, 4, 5 }; - ListSegment testSegment = new ListSegment(testList, 1, 3); - Assert.AreEqual(1, testSegment.Offset); - } - - /// Tests whether the 'Count' property works as expected - [Test] - public void CountPropertyIsStored() { - var testList = new List(capacity: 5) { 1, 2, 3, 4, 5 }; - ListSegment testSegment = new ListSegment(testList, 1, 3); - Assert.AreEqual(3, testSegment.Count); - } - - /// - /// Tests whether two differing instances produce different hash codes - /// - [Test] - public void DifferentInstancesHaveDifferentHashCodes_Usually() { - var forwardCountSegment = new ListSegment( - new List(capacity: 9) { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 2, 7 - ); - var reverseCountSegment = new ListSegment( - new List(capacity: 9) { 9, 8, 7, 6, 5, 4, 3, 2, 1 }, 1, 8 - ); - - Assert.AreNotEqual( - forwardCountSegment.GetHashCode(), reverseCountSegment.GetHashCode() - ); - } - - /// - /// Tests whether two equivalent instances produce an identical hash code - /// - [Test] - public void EquivalentInstancesHaveSameHashcode() { - var testSegment = new ListSegment( - new List(capacity: 9) { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 2, 7 - ); - var identicalSegment = new ListSegment( - new List(capacity: 9) { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 2, 7 - ); - - Assert.AreEqual( - testSegment.GetHashCode(), identicalSegment.GetHashCode() - ); - } - - /// Tests the equals method performing a comparison against null - [Test] - public void EqualsAgainstNullIsAlwaysFalse() { - var testList = new List(capacity: 5) { 1, 2, 3, 4, 5 }; - ListSegment testSegment = new ListSegment(testList, 1, 3); - - Assert.IsFalse( - testSegment.Equals(null) - ); - } - - /// Tests the equality operator with differing instances - [Test] - public void DifferingInstancesAreNotEqual() { - var forwardCountSegment = new ListSegment( - new List(capacity: 9) { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 2, 7 - ); - var reverseCountSegment = new ListSegment( - new List(capacity: 9) { 9, 8, 7, 6, 5, 4, 3, 2, 1 }, 1, 8 - ); - - Assert.IsFalse(forwardCountSegment == reverseCountSegment); - } - - /// Tests the equality operator with equivalent instances - [Test] - public void EquivalentInstancesAreEqual() { - var testSegment = new ListSegment( - new List(capacity: 9) { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 2, 7 - ); - var identicalSegment = new ListSegment( - new List(capacity: 9) { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 2, 7 - ); - - Assert.IsTrue(testSegment == identicalSegment); - } - - /// Tests the inequality operator with differing instances - [Test] - public void DifferingInstancesAreUnequal() { - var forwardCountSegment = new ListSegment( - new List(capacity: 9) { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 2, 7 - ); - var reverseCountSegment = new ListSegment( - new List(capacity: 9) { 9, 8, 7, 6, 5, 4, 3, 2, 1 }, 1, 8 - ); - - Assert.IsTrue(forwardCountSegment != reverseCountSegment); - } - - /// Tests the inequality operator with equivalent instances - [Test] - public void EquivalentInstancesAreNotUnequal() { - var testSegment = new ListSegment( - new List(capacity: 9) { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 2, 7 - ); - var identicalSegment = new ListSegment( - new List(capacity: 9) { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 2, 7 - ); - - Assert.IsFalse(testSegment != identicalSegment); - } - - /// Tests the ToString() method of the string segment - [Test] - public void TestToString() { - var testList = new List(capacity: 6) { 1, 2, 3, 4, 5, 6 }; - ListSegment testSegment = new ListSegment(testList, 2, 2); - - string stringRepresentation = testSegment.ToString(); - StringAssert.Contains("3, 4", stringRepresentation); - StringAssert.DoesNotContain("2", stringRepresentation); - StringAssert.DoesNotContain("5", stringRepresentation); - } - - /// Tests whether the 'Text' property works as expected - [Test] - public void ToListReturnsSubset() { - var testList = new List(capacity: 5) { 1, 2, 3, 4, 5 }; - ListSegment testSegment = new ListSegment(testList, 1, 3); - CollectionAssert.AreEqual( - new List(capacity: 3) { 2, 3, 4 }, - testSegment.ToList() - ); - } - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; +using System.IO; + +#if UNITTEST + +using NUnit.Framework; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the list segment class + [TestFixture] + internal class ListSegmentTest { + + /// + /// Tests whether the default constructor of the ListSegment class throws the + /// right exception when being passed 'null' instead of a list + /// + [Test] + public void SimpleConstructorThrowsWhenListIsNull() { + Assert.Throws( + delegate() { new ListSegment(null); } + ); + } + + /// + /// Tests whether the simple constructor of the ListSegment class accepts + /// an empty list + /// + [Test] + public void SimpleConstructorAcceptsEmptyList() { + new ListSegment(new List()); + } + + /// + /// Tests whether the full constructor of the ListSegment class throws the + /// right exception when being passed 'null' instead of a string + /// + [Test] + public void ConstructorThrowsWhenListIsNull() { + Assert.Throws( + delegate() { new ListSegment(null, 0, 0); } + ); + } + + /// + /// Tests whether the full constructor of the ListSegment class accepts + /// an empty string + /// + [Test] + public void ConstructorAcceptsEmptyList() { + new ListSegment(new List(), 0, 0); + } + + /// + /// Tests whether the full constructor of the ListSegment class throws the + /// right exception when being passed an invalid start offset + /// + [Test] + public void ConstructorThrowsOnInvalidOffset() { + Assert.Throws( + delegate() { new ListSegment(new List(), -1, 0); } + ); + } + + /// + /// Tests whether the full constructor of the ListSegment class throws the + /// right exception when being passed an invalid element count + /// + [Test] + public void ConstructorThrowsOnInvalidCount() { + Assert.Throws( + delegate() { new ListSegment(new List(), 0, -1); } + ); + } + + /// + /// Tests whether the full constructor of the ListSegment class throws the + /// right exception when being passed a string length that's too large + /// + [Test] + public void ConstructorThrowsOnListOverrun() { + var testList = new List(capacity: 5) { 1, 2, 3, 4, 5 }; + Assert.Throws( + delegate() { new ListSegment(testList, 3, 3); } + ); + } + + /// Tests whether the 'Text' property works as expected + [Test] + public void ListPropertyStoresOriginalList() { + var testList = new List(capacity: 5) { 1, 2, 3, 4, 5 }; + ListSegment testSegment = new ListSegment(testList, 1, 3); + Assert.AreSame(testList, testSegment.List); + } + + /// Tests whether the 'Offset' property works as expected + [Test] + public void OffsetPropertyIsStored() { + var testList = new List(capacity: 5) { 1, 2, 3, 4, 5 }; + ListSegment testSegment = new ListSegment(testList, 1, 3); + Assert.AreEqual(1, testSegment.Offset); + } + + /// Tests whether the 'Count' property works as expected + [Test] + public void CountPropertyIsStored() { + var testList = new List(capacity: 5) { 1, 2, 3, 4, 5 }; + ListSegment testSegment = new ListSegment(testList, 1, 3); + Assert.AreEqual(3, testSegment.Count); + } + + /// + /// Tests whether two differing instances produce different hash codes + /// + [Test] + public void DifferentInstancesHaveDifferentHashCodes_Usually() { + var forwardCountSegment = new ListSegment( + new List(capacity: 9) { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 2, 7 + ); + var reverseCountSegment = new ListSegment( + new List(capacity: 9) { 9, 8, 7, 6, 5, 4, 3, 2, 1 }, 1, 8 + ); + + Assert.AreNotEqual( + forwardCountSegment.GetHashCode(), reverseCountSegment.GetHashCode() + ); + } + + /// + /// Tests whether two equivalent instances produce an identical hash code + /// + [Test] + public void EquivalentInstancesHaveSameHashcode() { + var testSegment = new ListSegment( + new List(capacity: 9) { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 2, 7 + ); + var identicalSegment = new ListSegment( + new List(capacity: 9) { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 2, 7 + ); + + Assert.AreEqual( + testSegment.GetHashCode(), identicalSegment.GetHashCode() + ); + } + + /// Tests the equals method performing a comparison against null + [Test] + public void EqualsAgainstNullIsAlwaysFalse() { + var testList = new List(capacity: 5) { 1, 2, 3, 4, 5 }; + ListSegment testSegment = new ListSegment(testList, 1, 3); + + Assert.IsFalse( + testSegment.Equals(null) + ); + } + + /// Tests the equality operator with differing instances + [Test] + public void DifferingInstancesAreNotEqual() { + var forwardCountSegment = new ListSegment( + new List(capacity: 9) { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 2, 7 + ); + var reverseCountSegment = new ListSegment( + new List(capacity: 9) { 9, 8, 7, 6, 5, 4, 3, 2, 1 }, 1, 8 + ); + + Assert.IsFalse(forwardCountSegment == reverseCountSegment); + } + + /// Tests the equality operator with equivalent instances + [Test] + public void EquivalentInstancesAreEqual() { + var testSegment = new ListSegment( + new List(capacity: 9) { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 2, 7 + ); + var identicalSegment = new ListSegment( + new List(capacity: 9) { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 2, 7 + ); + + Assert.IsTrue(testSegment == identicalSegment); + } + + /// Tests the inequality operator with differing instances + [Test] + public void DifferingInstancesAreUnequal() { + var forwardCountSegment = new ListSegment( + new List(capacity: 9) { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 2, 7 + ); + var reverseCountSegment = new ListSegment( + new List(capacity: 9) { 9, 8, 7, 6, 5, 4, 3, 2, 1 }, 1, 8 + ); + + Assert.IsTrue(forwardCountSegment != reverseCountSegment); + } + + /// Tests the inequality operator with equivalent instances + [Test] + public void EquivalentInstancesAreNotUnequal() { + var testSegment = new ListSegment( + new List(capacity: 9) { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 2, 7 + ); + var identicalSegment = new ListSegment( + new List(capacity: 9) { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 2, 7 + ); + + Assert.IsFalse(testSegment != identicalSegment); + } + + /// Tests the ToString() method of the string segment + [Test] + public void TestToString() { + var testList = new List(capacity: 6) { 1, 2, 3, 4, 5, 6 }; + ListSegment testSegment = new ListSegment(testList, 2, 2); + + string stringRepresentation = testSegment.ToString(); + StringAssert.Contains("3, 4", stringRepresentation); + StringAssert.DoesNotContain("2", stringRepresentation); + StringAssert.DoesNotContain("5", stringRepresentation); + } + + /// Tests whether the 'Text' property works as expected + [Test] + public void ToListReturnsSubset() { + var testList = new List(capacity: 5) { 1, 2, 3, 4, 5 }; + ListSegment testSegment = new ListSegment(testList, 1, 3); + CollectionAssert.AreEqual( + new List(capacity: 3) { 2, 3, 4 }, + testSegment.ToList() + ); + } + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST diff --git a/Source/Collections/ListSegment.cs b/Source/Collections/ListSegment.cs index ca47b4b..54e1f3c 100644 --- a/Source/Collections/ListSegment.cs +++ b/Source/Collections/ListSegment.cs @@ -1,281 +1,280 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; - -namespace Nuclex.Support.Collections { - - /// View into a section of an IList<T> without copying said string - /// - /// Type of elements that are stored in the list the segment references - /// - /// - /// - /// The design of this class pretty much mirrors that of the - /// class found in the .NET framework, but is - /// specialized to be used for IList<T>, which can not be cast to arrays - /// directly (and loses type safety). - /// - /// - /// In certain situations, passing a ListSegment instead of storing the selected - /// elements in a new list is useful. For example, the caller might want to know - /// from which index of the original list the section was taken. When the original - /// list needs to be modified, for example in a sorting algorithm, the list segment - /// can be used to specify a region for the algorithm to work on while still accessing - /// the original list. - /// - /// -#if !NO_SERIALIZATION - [Serializable, StructLayout(LayoutKind.Sequential)] -#endif - public struct ListSegment { - - /// - /// Initializes a new instance of the class - /// that delimits all the elements in the specified string - /// - /// List that will be wrapped - /// String is null - public ListSegment(IList list) { - if(list == null) { // questionable, but matches behavior of ArraySegment class - throw new ArgumentNullException("text", "Text must not be null"); - } - - this.list = list; - this.offset = 0; - this.count = list.Count; - } - - /// - /// Initializes a new instance of the class - /// that delimits the specified range of the elements in the specified string - /// - /// The list containing the range of elements to delimit - /// The zero-based index of the first element in the range - /// The number of elements in the range - /// - /// Offset or count is less than 0 - /// - /// - /// Offset and count do not specify a valid range in array - /// - /// String is null - public ListSegment(IList list, int offset, int count) { - if(list == null) { // questionable, but matches behavior of ArraySegment class - throw new ArgumentNullException("list"); - } - if(offset < 0) { - throw new ArgumentOutOfRangeException( - "offset", "Argument out of range, non-negative number required" - ); - } - if(count < 0) { - throw new ArgumentOutOfRangeException( - "count", "Argument out of range, non-negative number required" - ); - } - if(count > (list.Count - offset)) { - throw new ArgumentException( - "Invalid argument, specified offset and count exceed list size" - ); - } - - this.list = list; - this.offset = offset; - this.count = count; - } - - /// - /// Gets the original list containing the range of elements that the list - /// segment delimits - /// - /// - /// The original list that was passed to the constructor, and that contains the range - /// delimited by the - /// - public IList List { - get { return this.list; } - } - - /// - /// Gets the position of the first element in the range delimited by the list segment, - /// relative to the start of the original list - /// - /// - /// The position of the first element in the range delimited by the - /// , relative to the start of the original list - /// - public int Offset { - get { return this.offset; } - } - - /// - /// Gets the number of elements in the range delimited by the list segment - /// - /// - /// The number of elements in the range delimited by - /// the - /// - public int Count { - get { return this.count; } - } - - /// Returns the hash code for the current instance - /// A 32-bit signed integer hash code - public override int GetHashCode() { - int hashCode = this.offset ^ this.count; - for(int index = 0; index < this.count; ++index) { - hashCode ^= this.list[index + this.offset].GetHashCode(); - } - return hashCode; - } - - /// - /// Determines whether the specified object is equal to the current instance - /// - /// - /// True if the specified object is a structure - /// and is equal to the current instance; otherwise, false - /// - /// The object to be compared with the current instance - public override bool Equals(object other) { - return - (other is ListSegment) && - this.Equals((ListSegment)other); - } - - /// - /// Determines whether the specified - /// structure is equal to the current instance - /// - /// - /// True if the specified structure is equal - /// to the current instance; otherwise, false - /// - /// - /// The structure to be compared with - /// the current instance - /// - public bool Equals(ListSegment other) { - if(other.count != this.count) { - return false; - } - - if(ReferenceEquals(other.list, this.list)) { - return (other.offset == this.offset); - } else { - var comparer = Comparer.Default; - for(int index = 0; index < this.count; ++index) { - int difference = comparer.Compare( - other.list[index + other.offset], this.list[index + this.offset] - ); - if(difference != 0) { - return false; - } - } - } - return true; - } - - /// - /// Indicates whether two structures are equal - /// - /// True if a is equal to b; otherwise, false - /// - /// The structure on the left side of - /// the equality operator - /// - /// - /// The structure on the right side of - /// the equality operator - /// - public static bool operator ==(ListSegment left, ListSegment right) { - return left.Equals(right); - } - - /// - /// Indicates whether two structures are unequal - /// - /// True if a is not equal to b; otherwise, false - /// - /// The structure on the left side of - /// the inequality operator - /// - /// - /// The structure on the right side of - /// the inequality operator - /// - public static bool operator !=(ListSegment left, ListSegment right) { - return !(left == right); - } - - /// Returns a string representation of the list segment - /// The string representation of the list segment - public override string ToString() { - var builder = new System.Text.StringBuilder(); - builder.Append("ListSegment {"); - for(int index = 0; index < Math.Min(this.count, 10); ++index) { - if(index == 0) { - builder.Append(" "); - } else { - builder.Append(", "); - } - builder.Append(this.list[index + this.offset].ToString()); - } - - if(this.count >= 11) { - builder.Append(", ... }"); - } else { - builder.Append(" }"); - } - - return builder.ToString(); - } - - /// Returns a new list containing only the elements in the list segment - /// A new list containing only the elements in the list segment - public List ToList() { - if(this.count == 0) { - return new List(capacity: 0); - } else { - var newList = new List(capacity: this.count); - { - int endIndex = this.offset + this.count; - for(int index = this.offset; index < endIndex; ++index) { - newList.Add(this.list[index]); - } - } - - return newList; - } - } - - /// List wrapped by the list segment - private IList list; - /// Offset in the original list the segment begins at - private int offset; - /// Number of elements in the segment - private int count; - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace Nuclex.Support.Collections { + + /// View into a section of an IList<T> without copying said string + /// + /// Type of elements that are stored in the list the segment references + /// + /// + /// + /// The design of this class pretty much mirrors that of the + /// class found in the .NET framework, but is + /// specialized to be used for IList<T>, which can not be cast to arrays + /// directly (and loses type safety). + /// + /// + /// In certain situations, passing a ListSegment instead of storing the selected + /// elements in a new list is useful. For example, the caller might want to know + /// from which index of the original list the section was taken. When the original + /// list needs to be modified, for example in a sorting algorithm, the list segment + /// can be used to specify a region for the algorithm to work on while still accessing + /// the original list. + /// + /// +#if !NO_SERIALIZATION + [Serializable, StructLayout(LayoutKind.Sequential)] +#endif + public struct ListSegment { + + /// + /// Initializes a new instance of the class + /// that delimits all the elements in the specified string + /// + /// List that will be wrapped + /// String is null + public ListSegment(IList list) { + if(list == null) { // questionable, but matches behavior of ArraySegment class + throw new ArgumentNullException("text", "Text must not be null"); + } + + this.list = list; + this.offset = 0; + this.count = list.Count; + } + + /// + /// Initializes a new instance of the class + /// that delimits the specified range of the elements in the specified string + /// + /// The list containing the range of elements to delimit + /// The zero-based index of the first element in the range + /// The number of elements in the range + /// + /// Offset or count is less than 0 + /// + /// + /// Offset and count do not specify a valid range in array + /// + /// String is null + public ListSegment(IList list, int offset, int count) { + if(list == null) { // questionable, but matches behavior of ArraySegment class + throw new ArgumentNullException("list"); + } + if(offset < 0) { + throw new ArgumentOutOfRangeException( + "offset", "Argument out of range, non-negative number required" + ); + } + if(count < 0) { + throw new ArgumentOutOfRangeException( + "count", "Argument out of range, non-negative number required" + ); + } + if(count > (list.Count - offset)) { + throw new ArgumentException( + "Invalid argument, specified offset and count exceed list size" + ); + } + + this.list = list; + this.offset = offset; + this.count = count; + } + + /// + /// Gets the original list containing the range of elements that the list + /// segment delimits + /// + /// + /// The original list that was passed to the constructor, and that contains the range + /// delimited by the + /// + public IList List { + get { return this.list; } + } + + /// + /// Gets the position of the first element in the range delimited by the list segment, + /// relative to the start of the original list + /// + /// + /// The position of the first element in the range delimited by the + /// , relative to the start of the original list + /// + public int Offset { + get { return this.offset; } + } + + /// + /// Gets the number of elements in the range delimited by the list segment + /// + /// + /// The number of elements in the range delimited by + /// the + /// + public int Count { + get { return this.count; } + } + + /// Returns the hash code for the current instance + /// A 32-bit signed integer hash code + public override int GetHashCode() { + int hashCode = this.offset ^ this.count; + for(int index = 0; index < this.count; ++index) { + hashCode ^= this.list[index + this.offset].GetHashCode(); + } + return hashCode; + } + + /// + /// Determines whether the specified object is equal to the current instance + /// + /// + /// True if the specified object is a structure + /// and is equal to the current instance; otherwise, false + /// + /// The object to be compared with the current instance + public override bool Equals(object other) { + return + (other is ListSegment) && + this.Equals((ListSegment)other); + } + + /// + /// Determines whether the specified + /// structure is equal to the current instance + /// + /// + /// True if the specified structure is equal + /// to the current instance; otherwise, false + /// + /// + /// The structure to be compared with + /// the current instance + /// + public bool Equals(ListSegment other) { + if(other.count != this.count) { + return false; + } + + if(ReferenceEquals(other.list, this.list)) { + return (other.offset == this.offset); + } else { + var comparer = Comparer.Default; + for(int index = 0; index < this.count; ++index) { + int difference = comparer.Compare( + other.list[index + other.offset], this.list[index + this.offset] + ); + if(difference != 0) { + return false; + } + } + } + return true; + } + + /// + /// Indicates whether two structures are equal + /// + /// True if a is equal to b; otherwise, false + /// + /// The structure on the left side of + /// the equality operator + /// + /// + /// The structure on the right side of + /// the equality operator + /// + public static bool operator ==(ListSegment left, ListSegment right) { + return left.Equals(right); + } + + /// + /// Indicates whether two structures are unequal + /// + /// True if a is not equal to b; otherwise, false + /// + /// The structure on the left side of + /// the inequality operator + /// + /// + /// The structure on the right side of + /// the inequality operator + /// + public static bool operator !=(ListSegment left, ListSegment right) { + return !(left == right); + } + + /// Returns a string representation of the list segment + /// The string representation of the list segment + public override string ToString() { + var builder = new System.Text.StringBuilder(); + builder.Append("ListSegment {"); + for(int index = 0; index < Math.Min(this.count, 10); ++index) { + if(index == 0) { + builder.Append(" "); + } else { + builder.Append(", "); + } + builder.Append(this.list[index + this.offset].ToString()); + } + + if(this.count >= 11) { + builder.Append(", ... }"); + } else { + builder.Append(" }"); + } + + return builder.ToString(); + } + + /// Returns a new list containing only the elements in the list segment + /// A new list containing only the elements in the list segment + public List ToList() { + if(this.count == 0) { + return new List(capacity: 0); + } else { + var newList = new List(capacity: this.count); + { + int endIndex = this.offset + this.count; + for(int index = this.offset; index < endIndex; ++index) { + newList.Add(this.list[index]); + } + } + + return newList; + } + } + + /// List wrapped by the list segment + private IList list; + /// Offset in the original list the segment begins at + private int offset; + /// Number of elements in the segment + private int count; + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/MultiDictionary.Interfaces.cs b/Source/Collections/MultiDictionary.Interfaces.cs index 2b76067..0139391 100644 --- a/Source/Collections/MultiDictionary.Interfaces.cs +++ b/Source/Collections/MultiDictionary.Interfaces.cs @@ -1,273 +1,272 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Nuclex.Support.Collections { - - partial class MultiDictionary { - - #region IEnumerable implementation - - /// Returns a new object enumerator for the Dictionary - /// The new object enumerator - IEnumerator IEnumerable.GetEnumerator() { - return GetEnumerator(); - } - - #endregion - - #region IDictionary implementation - - /// Adds an item into the dictionary - /// Key under which the item will be added - /// Item that will be added - void IDictionary.Add(object key, object value) { - Add((TKey)key, (TValue)value); - } - - /// Determines whether the specified key exists in the dictionary - /// Key that will be checked for - /// True if an item with the specified key exists in the dictionary - bool IDictionary.Contains(object key) { - return this.objectDictionary.Contains(key); - } - - /// Whether the size of the dictionary is fixed - bool IDictionary.IsFixedSize { - get { return this.objectDictionary.IsFixedSize; } - } - - /// Returns a collection of all keys in the dictionary - ICollection IDictionary.Keys { - get { return this.objectDictionary.Keys; } - } - - /// Returns a collection of all values stored in the dictionary - ICollection IDictionary.Values { - get { - if(this.valueCollection == null) { - this.valueCollection = new ValueCollection(this); - } - - return this.valueCollection; - } - } - - /// Removes an item from the dictionary - /// Key of the item that will be removed - void IDictionary.Remove(object key) { - RemoveKey((TKey)key); - } - - /// Accesses an item in the dictionary by its key - /// Key of the item that will be accessed - /// The item with the specified key - object IDictionary.this[object key] { - get { return this.objectDictionary[key]; } - set { this[(TKey)key] = (ICollection)value; } - } - - #endregion - - #region IDictionaryEnumerator implementation - - /// Returns a new entry enumerator for the dictionary - /// The new entry enumerator - IDictionaryEnumerator IDictionary.GetEnumerator() { - return new Enumerator(this); - } - - #endregion // IDictionaryEnumerator implementation - - #region ICollection> implementation - - /// Inserts an already prepared element into the dictionary - /// Prepared element that will be added to the dictionary - void ICollection>.Add( - KeyValuePair item - ) { - Add(item.Key, item.Value); - } - - /// Removes all items from the dictionary - /// Item that will be removed from the dictionary - bool ICollection>.Remove( - KeyValuePair itemToRemove - ) { - ICollection values; - if(!this.typedDictionary.TryGetValue(itemToRemove.Key, out values)) { - return false; - } - - if(values.Remove(itemToRemove.Value)) { - if(values.Count == 0) { - this.typedDictionary.Remove(itemToRemove.Key); - } - return true; - } else { - return false; - } - } - - #endregion - - #region ICollection implementation - - /// Copies the contents of the Dictionary into an array - /// Array the Dictionary contents will be copied into - /// - /// Starting index at which to begin filling the destination array - /// - void ICollection.CopyTo(Array array, int arrayIndex) { - foreach(KeyValuePair> item in this.typedDictionary) { - foreach(TValue value in item.Value) { - array.SetValue(new KeyValuePair(item.Key, value), arrayIndex); - ++arrayIndex; - } - } - } - - /// Whether the Dictionary is synchronized for multi-threaded usage - bool ICollection.IsSynchronized { - get { return this.objectDictionary.IsSynchronized; } - } - - /// Synchronization root on which the Dictionary locks - object ICollection.SyncRoot { - get { return this.objectDictionary.SyncRoot; } - } - - #endregion - - #region IDictionary> implementation - - /// Adds a series of values to a dictionary - /// Key under which the values will be added - /// Values that will be added to the dictionary - void IDictionary>.Add(TKey key, ICollection values) { - ICollection currentValues; - if(!this.typedDictionary.TryGetValue(key, out currentValues)) { - currentValues = new ValueList(this); - } - - foreach(TValue value in values) { - currentValues.Add(value); - } - } - - /// Removes all values with the specified key - /// Key whose associated entries will be removed - /// True if at least one entry has been removed from the dictionary - bool IDictionary>.Remove(TKey key) { - return (RemoveKey(key) > 0); - } - - /// Returns a collection of value collections - ICollection> IDictionary>.Values { - get { return this.typedDictionary.Values; } - } - - #endregion // IDictionary> implementation - - #region ICollection>> implementation - - /// Adds a series of values to a dictionary - /// Entry containing the values that will be added - void ICollection>>.Add( - KeyValuePair> item - ) { - ICollection currentValues; - if(!this.typedDictionary.TryGetValue(item.Key, out currentValues)) { - currentValues = new ValueList(this); - } - - foreach(TValue value in item.Value) { - currentValues.Add(value); - } - } - - /// - /// Checks whether the dictionary contains the specified key/value pair - /// - /// Key/value pair for which the dictionary will be checked - /// True if the dictionary contains the specified key/value pair - bool ICollection>>.Contains( - KeyValuePair> item - ) { - return this.typedDictionary.Contains(item); - } - - /// Copies the contents of the dictionary into an array - /// - /// - void ICollection>>.CopyTo( - KeyValuePair>[] array, int arrayIndex - ) { - this.typedDictionary.CopyTo(array, arrayIndex); - } - - /// Removes the specified key/value pair from the dictionary - /// Key/value pair that will be removed - /// True if the key/value pair was contained in the dictionary - bool ICollection>>.Remove( - KeyValuePair> item - ) { - return this.typedDictionary.Remove(item); - } - - /// Returns an enumerator for the dictionary - /// An enumerator for the key/value pairs in the dictionary - IEnumerator>> IEnumerable< - KeyValuePair> - >.GetEnumerator() { - return this.typedDictionary.GetEnumerator(); - } - - /// Number of unique keys in the dictionary - /// - /// - /// This Count property returns a different value from the main interface of - /// the multi dictionary to stay consistent with the implemented interfaces. - /// - /// - /// If you cast a multi dictionary to a collection of collections, the count - /// property of the outer collection should, of course, be the number of inner - /// collections it contains (and not the sum of the items contained in all of - /// the inner collections). - /// - /// - /// If you use the count property in the main interface of the multi dictionary, - /// the value collections are hidden (it behaves as if the key was in the - /// dictionary multiple times), so now the sum of all key-value pairs should - /// be returned. - /// - /// - int ICollection>>.Count { - get { return this.typedDictionary.Count; } - } - - #endregion // ICollection>> implementation - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Nuclex.Support.Collections { + + partial class MultiDictionary { + + #region IEnumerable implementation + + /// Returns a new object enumerator for the Dictionary + /// The new object enumerator + IEnumerator IEnumerable.GetEnumerator() { + return GetEnumerator(); + } + + #endregion + + #region IDictionary implementation + + /// Adds an item into the dictionary + /// Key under which the item will be added + /// Item that will be added + void IDictionary.Add(object key, object value) { + Add((TKey)key, (TValue)value); + } + + /// Determines whether the specified key exists in the dictionary + /// Key that will be checked for + /// True if an item with the specified key exists in the dictionary + bool IDictionary.Contains(object key) { + return this.objectDictionary.Contains(key); + } + + /// Whether the size of the dictionary is fixed + bool IDictionary.IsFixedSize { + get { return this.objectDictionary.IsFixedSize; } + } + + /// Returns a collection of all keys in the dictionary + ICollection IDictionary.Keys { + get { return this.objectDictionary.Keys; } + } + + /// Returns a collection of all values stored in the dictionary + ICollection IDictionary.Values { + get { + if(this.valueCollection == null) { + this.valueCollection = new ValueCollection(this); + } + + return this.valueCollection; + } + } + + /// Removes an item from the dictionary + /// Key of the item that will be removed + void IDictionary.Remove(object key) { + RemoveKey((TKey)key); + } + + /// Accesses an item in the dictionary by its key + /// Key of the item that will be accessed + /// The item with the specified key + object IDictionary.this[object key] { + get { return this.objectDictionary[key]; } + set { this[(TKey)key] = (ICollection)value; } + } + + #endregion + + #region IDictionaryEnumerator implementation + + /// Returns a new entry enumerator for the dictionary + /// The new entry enumerator + IDictionaryEnumerator IDictionary.GetEnumerator() { + return new Enumerator(this); + } + + #endregion // IDictionaryEnumerator implementation + + #region ICollection> implementation + + /// Inserts an already prepared element into the dictionary + /// Prepared element that will be added to the dictionary + void ICollection>.Add( + KeyValuePair item + ) { + Add(item.Key, item.Value); + } + + /// Removes all items from the dictionary + /// Item that will be removed from the dictionary + bool ICollection>.Remove( + KeyValuePair itemToRemove + ) { + ICollection values; + if(!this.typedDictionary.TryGetValue(itemToRemove.Key, out values)) { + return false; + } + + if(values.Remove(itemToRemove.Value)) { + if(values.Count == 0) { + this.typedDictionary.Remove(itemToRemove.Key); + } + return true; + } else { + return false; + } + } + + #endregion + + #region ICollection implementation + + /// Copies the contents of the Dictionary into an array + /// Array the Dictionary contents will be copied into + /// + /// Starting index at which to begin filling the destination array + /// + void ICollection.CopyTo(Array array, int arrayIndex) { + foreach(KeyValuePair> item in this.typedDictionary) { + foreach(TValue value in item.Value) { + array.SetValue(new KeyValuePair(item.Key, value), arrayIndex); + ++arrayIndex; + } + } + } + + /// Whether the Dictionary is synchronized for multi-threaded usage + bool ICollection.IsSynchronized { + get { return this.objectDictionary.IsSynchronized; } + } + + /// Synchronization root on which the Dictionary locks + object ICollection.SyncRoot { + get { return this.objectDictionary.SyncRoot; } + } + + #endregion + + #region IDictionary> implementation + + /// Adds a series of values to a dictionary + /// Key under which the values will be added + /// Values that will be added to the dictionary + void IDictionary>.Add(TKey key, ICollection values) { + ICollection currentValues; + if(!this.typedDictionary.TryGetValue(key, out currentValues)) { + currentValues = new ValueList(this); + } + + foreach(TValue value in values) { + currentValues.Add(value); + } + } + + /// Removes all values with the specified key + /// Key whose associated entries will be removed + /// True if at least one entry has been removed from the dictionary + bool IDictionary>.Remove(TKey key) { + return (RemoveKey(key) > 0); + } + + /// Returns a collection of value collections + ICollection> IDictionary>.Values { + get { return this.typedDictionary.Values; } + } + + #endregion // IDictionary> implementation + + #region ICollection>> implementation + + /// Adds a series of values to a dictionary + /// Entry containing the values that will be added + void ICollection>>.Add( + KeyValuePair> item + ) { + ICollection currentValues; + if(!this.typedDictionary.TryGetValue(item.Key, out currentValues)) { + currentValues = new ValueList(this); + } + + foreach(TValue value in item.Value) { + currentValues.Add(value); + } + } + + /// + /// Checks whether the dictionary contains the specified key/value pair + /// + /// Key/value pair for which the dictionary will be checked + /// True if the dictionary contains the specified key/value pair + bool ICollection>>.Contains( + KeyValuePair> item + ) { + return this.typedDictionary.Contains(item); + } + + /// Copies the contents of the dictionary into an array + /// + /// + void ICollection>>.CopyTo( + KeyValuePair>[] array, int arrayIndex + ) { + this.typedDictionary.CopyTo(array, arrayIndex); + } + + /// Removes the specified key/value pair from the dictionary + /// Key/value pair that will be removed + /// True if the key/value pair was contained in the dictionary + bool ICollection>>.Remove( + KeyValuePair> item + ) { + return this.typedDictionary.Remove(item); + } + + /// Returns an enumerator for the dictionary + /// An enumerator for the key/value pairs in the dictionary + IEnumerator>> IEnumerable< + KeyValuePair> + >.GetEnumerator() { + return this.typedDictionary.GetEnumerator(); + } + + /// Number of unique keys in the dictionary + /// + /// + /// This Count property returns a different value from the main interface of + /// the multi dictionary to stay consistent with the implemented interfaces. + /// + /// + /// If you cast a multi dictionary to a collection of collections, the count + /// property of the outer collection should, of course, be the number of inner + /// collections it contains (and not the sum of the items contained in all of + /// the inner collections). + /// + /// + /// If you use the count property in the main interface of the multi dictionary, + /// the value collections are hidden (it behaves as if the key was in the + /// dictionary multiple times), so now the sum of all key-value pairs should + /// be returned. + /// + /// + int ICollection>>.Count { + get { return this.typedDictionary.Count; } + } + + #endregion // ICollection>> implementation + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/MultiDictionary.Test.cs b/Source/Collections/MultiDictionary.Test.cs index 010f45c..0c72383 100644 --- a/Source/Collections/MultiDictionary.Test.cs +++ b/Source/Collections/MultiDictionary.Test.cs @@ -1,391 +1,390 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.Collections.Generic; - -using NUnit.Framework; -using NMock; - -namespace Nuclex.Support.Collections { - - /// Unit tests for the multi dictionary - [TestFixture] - internal class MultiDictionaryTest { - - /// - /// Verifies that new instances of the multi dictionary can be created - /// - [Test] - public void CanConstructNewDictionary() { - var dictionary = new MultiDictionary(); - Assert.IsNotNull(dictionary); // nonsense, prevents compiler warning - } - - /// - /// Verifies that the count is initialized correctly when building - /// a multi dictionary from a dictionary of value collections. - /// - [Test] - public void CountIsCalculatedIfInitializedFromDictionary() { - var contents = new Dictionary>(); - contents.Add(1, new List(new string[] { "one", "eins" })); - contents.Add(2, new List(new string[] { "two", "zwei" })); - - var multiDictionary = new MultiDictionary(contents); - Assert.AreEqual(4, multiDictionary.Count); - } - - /// - /// Verifies that a new multi dictionary based on a read-only dictionary is - /// also read-only - /// - [Test] - public void IsReadOnlyWhenBasedOnReadOnlyContainer() { - var readOnly = new ReadOnlyDictionary>( - new Dictionary>() - ); - var dictionary = new MultiDictionary(readOnly); - - Assert.IsTrue(dictionary.IsReadOnly); - } - - /// - /// Ensures that the multi dictionary can contain the same key multiple times - /// (or in other words, multiple values on the same key) - /// - [Test] - public void CanContainKeyMultipleTimes() { - var dictionary = new MultiDictionary(); - dictionary.Add(123, "one two three"); - dictionary.Add(123, "eins zwei drei"); - - Assert.AreEqual(2, dictionary.Count); - - CollectionAssert.AreEquivalent( - new KeyValuePair[] { - new KeyValuePair(123, "one two three"), - new KeyValuePair(123, "eins zwei drei") - }, - dictionary - ); - } - - /// - /// Verifies that adding values through the indexer still updates the item count - /// - [Test] - public void AddingValuesFromIndexerUpdatesCount() { - var dictionary = new MultiDictionary(); - dictionary.Add(42, "the answer to everything"); - dictionary[42].Add("21x2"); - - Assert.AreEqual(2, dictionary.Count); - - CollectionAssert.AreEquivalent( - new KeyValuePair[] { - new KeyValuePair(42, "the answer to everything"), - new KeyValuePair(42, "21x2") - }, - dictionary - ); - } - - /// - /// Tests whether the collection can count the number of values stored - /// under a key - /// - [Test] - public void ValuesWithSameKeyCanBeCounted() { - var dictionary = new MultiDictionary(); - dictionary.Add(10, "ten"); - dictionary.Add(20, "twenty"); - dictionary.Add(30, "thirty"); - dictionary.Add(10, "zehn"); - dictionary.Add(20, "zwanzig"); - dictionary.Add(10, "dix"); - - Assert.AreEqual(6, dictionary.Count); - Assert.AreEqual(3, dictionary.CountValues(10)); - Assert.AreEqual(2, dictionary.CountValues(20)); - Assert.AreEqual(1, dictionary.CountValues(30)); - } - - /// - /// Verifies that counting the values of a non-existing key returns 0 - /// - [Test] - public void CountingValuesOfNonExistentKeyReturnsNull() { - var dictionary = new MultiDictionary(); - Assert.AreEqual(0, dictionary.CountValues(1)); - } - - /// - /// Ensures that its possible to remove values individually without affecting - /// other values stored under the same key - /// - [Test] - public void ValuesCanBeRemovedIndividually() { - var dictionary = new MultiDictionary(); - dictionary.Add(10, "ten"); - dictionary.Add(10, "zehn"); - dictionary.Add(10, "dix"); - - dictionary.Remove(10, "zehn"); - - Assert.AreEqual(2, dictionary.Count); - CollectionAssert.AreEquivalent( - new KeyValuePair[] { - new KeyValuePair(10, "ten"), - new KeyValuePair(10, "dix") - }, - dictionary - ); - } - - /// - /// Verifies that the Count property returns the number of unique keys if it is called - /// on the collection-of-collections interface implemented by the multi dictionary - /// - [Test] - public void CollectionOfCollectionCountIsUniqueKeyCount() { - var dictionary = new MultiDictionary(); - dictionary.Add(10, "ten"); - dictionary.Add(10, "zehn"); - - Assert.AreEqual(2, dictionary.Count); - var collectionOfCollections = - (ICollection>>)dictionary; - Assert.AreEqual(1, collectionOfCollections.Count); - } - - /// - /// Verifies that the multi dictionary can be tested for containment of a specific value - /// - [Test] - public void ContainmentCanBeTested() { - var dictionary = new MultiDictionary(); - dictionary.Add(10, "ten"); - dictionary.Add(10, "zehn"); - - Assert.IsTrue(dictionary.Contains(new KeyValuePair(10, "ten"))); - Assert.IsTrue(dictionary.Contains(new KeyValuePair(10, "zehn"))); - Assert.IsFalse(dictionary.Contains(new KeyValuePair(10, "dix"))); - Assert.IsFalse(dictionary.Contains(new KeyValuePair(20, "ten"))); - } - - /// - /// Verifies that the multi dictionary can be tested for containment of a specific key - /// - [Test] - public void KeyContainmentCanBeTested() { - var dictionary = new MultiDictionary(); - dictionary.Add(10, "ten"); - dictionary.Add(10, "zehn"); - - Assert.IsTrue(dictionary.ContainsKey(10)); - Assert.IsFalse(dictionary.ContainsKey(20)); - } - - /// - /// Verifies that the key collection can be retrieved from the dictionary - /// - [Test] - public void KeyCollectionCanBeRetrieved() { - var dictionary = new MultiDictionary(); - dictionary.Add(10, "ten"); - dictionary.Add(10, "zehn"); - - ICollection keys = dictionary.Keys; - Assert.IsNotNull(keys); - Assert.AreEqual(1, keys.Count); - } - - /// - /// Verifies that the key collection can be retrieved from the dictionary - /// - [Test] - public void ValueCollectionCanBeRetrieved() { - var dictionary = new MultiDictionary(); - dictionary.Add(10, "ten"); - dictionary.Add(10, "zehn"); - dictionary.Add(20, "twenty"); - - ICollection values = dictionary.Values; - Assert.IsNotNull(values); - Assert.AreEqual(3, values.Count); - } - - /// - /// Verifies that TryGetValue() returns false and doesn't throw if a key - /// is not found in the collection - /// - [Test] - public void TryGetValueReturnsFalseOnMissingKey() { - var dictionary = new MultiDictionary(); - ICollection values; - Assert.IsFalse(dictionary.TryGetValue(123, out values)); - } - - /// Verifies that keys can be looked up via TryGetValue() - [Test] - public void TryGetValueCanLookUpValues() { - var dictionary = new MultiDictionary(); - dictionary.Add(10, "ten"); - dictionary.Add(10, "zehn"); - ICollection values; - Assert.IsTrue(dictionary.TryGetValue(10, out values)); - Assert.AreEqual(2, values.Count); - } - - /// - /// Verifies that assigning null to a key deletes all the values stored - /// under it - /// - [Test] - public void AssigningNullToKeyRemovesAllValues() { - var dictionary = new MultiDictionary(); - dictionary.Add(10, "ten"); - dictionary.Add(10, "zehn"); - dictionary.Add(20, "twenty"); - - Assert.AreEqual(3, dictionary.Count); - dictionary[10] = null; - Assert.AreEqual(1, dictionary.Count); - Assert.IsFalse(dictionary.ContainsKey(10)); - } - - /// - /// Verifies that assigning null to a key deletes all the values stored - /// under it - /// - [Test] - public void ValueListCanBeAssignedToNewKey() { - var dictionary = new MultiDictionary(); - dictionary[3] = new List() { "three", "drei" }; - - Assert.AreEqual(2, dictionary.Count); - Assert.IsTrue(dictionary.Contains(new KeyValuePair(3, "three"))); - } - - /// - /// Verifies that assigning null to a key deletes all the values stored - /// under it - /// - [Test] - public void ValueListCanOverwriteExistingKey() { - var dictionary = new MultiDictionary(); - dictionary.Add(10, "dix"); - - Assert.AreEqual(1, dictionary.Count); - - dictionary[10] = new List() { "ten", "zehn" }; - - Assert.AreEqual(2, dictionary.Count); - Assert.IsFalse(dictionary.Contains(new KeyValuePair(10, "dix"))); - Assert.IsTrue(dictionary.Contains(new KeyValuePair(10, "ten"))); - } - - /// - /// Verifies that nothing bad happens when a key is removed from the dictionary - /// that it doesn't contain - /// - [Test] - public void NonExistingKeyCanBeRemoved() { - var dictionary = new MultiDictionary(); - Assert.AreEqual(0, dictionary.RemoveKey(123)); - } - - /// - /// Verifies that the remove method returns the number of values that have - /// been removed from the dictionary - /// - [Test] - public void RemoveReturnsNumberOfValuesRemoved() { - var dictionary = new MultiDictionary(); - dictionary.Add(10, "ten"); - dictionary.Add(10, "zehn"); - Assert.AreEqual(2, dictionary.RemoveKey(10)); - } - - /// - /// Verifies that the dictionary becomes empty after clearing it - /// - [Test] - public void DictionaryIsEmptyAfterClear() { - var dictionary = new MultiDictionary(); - dictionary.Add(10, "ten"); - dictionary.Add(10, "zehn"); - dictionary.Add(20, "twenty"); - Assert.AreEqual(3, dictionary.Count); - dictionary.Clear(); - Assert.AreEqual(0, dictionary.Count); - } - - /// - /// Verifies that non-existing values can be removed from the dictionary - /// - [Test] - public void NonExistingValueCanBeRemoved() { - var dictionary = new MultiDictionary(); - Assert.IsFalse(dictionary.Remove(123, "test")); - } - - /// - /// Verifies that nothing bad happens when the last value under a key is removed - /// - [Test] - public void LastValueOfKeyCanBeRemoved() { - var dictionary = new MultiDictionary(); - dictionary.Add(123, "test"); - dictionary.Remove(123, "test"); - Assert.AreEqual(0, dictionary.CountValues(123)); - } - - /// - /// Verifies that the dictionary can be copied into an array - /// - [Test] - public void DictionaryCanBeCopiedIntoArray() { - var expected = new List>() { - new KeyValuePair(1, "one"), - new KeyValuePair(1, "eins"), - new KeyValuePair(2, "two"), - new KeyValuePair(2, "zwei") - }; - - var dictionary = new MultiDictionary(); - foreach(KeyValuePair entry in expected) { - dictionary.Add(entry.Key, entry.Value); - } - - var actual = new KeyValuePair[4]; - dictionary.CopyTo(actual, 0); - - CollectionAssert.AreEquivalent(expected, actual); - } - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.Collections.Generic; + +using NUnit.Framework; +using NMock; + +namespace Nuclex.Support.Collections { + + /// Unit tests for the multi dictionary + [TestFixture] + internal class MultiDictionaryTest { + + /// + /// Verifies that new instances of the multi dictionary can be created + /// + [Test] + public void CanConstructNewDictionary() { + var dictionary = new MultiDictionary(); + Assert.IsNotNull(dictionary); // nonsense, prevents compiler warning + } + + /// + /// Verifies that the count is initialized correctly when building + /// a multi dictionary from a dictionary of value collections. + /// + [Test] + public void CountIsCalculatedIfInitializedFromDictionary() { + var contents = new Dictionary>(); + contents.Add(1, new List(new string[] { "one", "eins" })); + contents.Add(2, new List(new string[] { "two", "zwei" })); + + var multiDictionary = new MultiDictionary(contents); + Assert.AreEqual(4, multiDictionary.Count); + } + + /// + /// Verifies that a new multi dictionary based on a read-only dictionary is + /// also read-only + /// + [Test] + public void IsReadOnlyWhenBasedOnReadOnlyContainer() { + var readOnly = new ReadOnlyDictionary>( + new Dictionary>() + ); + var dictionary = new MultiDictionary(readOnly); + + Assert.IsTrue(dictionary.IsReadOnly); + } + + /// + /// Ensures that the multi dictionary can contain the same key multiple times + /// (or in other words, multiple values on the same key) + /// + [Test] + public void CanContainKeyMultipleTimes() { + var dictionary = new MultiDictionary(); + dictionary.Add(123, "one two three"); + dictionary.Add(123, "eins zwei drei"); + + Assert.AreEqual(2, dictionary.Count); + + CollectionAssert.AreEquivalent( + new KeyValuePair[] { + new KeyValuePair(123, "one two three"), + new KeyValuePair(123, "eins zwei drei") + }, + dictionary + ); + } + + /// + /// Verifies that adding values through the indexer still updates the item count + /// + [Test] + public void AddingValuesFromIndexerUpdatesCount() { + var dictionary = new MultiDictionary(); + dictionary.Add(42, "the answer to everything"); + dictionary[42].Add("21x2"); + + Assert.AreEqual(2, dictionary.Count); + + CollectionAssert.AreEquivalent( + new KeyValuePair[] { + new KeyValuePair(42, "the answer to everything"), + new KeyValuePair(42, "21x2") + }, + dictionary + ); + } + + /// + /// Tests whether the collection can count the number of values stored + /// under a key + /// + [Test] + public void ValuesWithSameKeyCanBeCounted() { + var dictionary = new MultiDictionary(); + dictionary.Add(10, "ten"); + dictionary.Add(20, "twenty"); + dictionary.Add(30, "thirty"); + dictionary.Add(10, "zehn"); + dictionary.Add(20, "zwanzig"); + dictionary.Add(10, "dix"); + + Assert.AreEqual(6, dictionary.Count); + Assert.AreEqual(3, dictionary.CountValues(10)); + Assert.AreEqual(2, dictionary.CountValues(20)); + Assert.AreEqual(1, dictionary.CountValues(30)); + } + + /// + /// Verifies that counting the values of a non-existing key returns 0 + /// + [Test] + public void CountingValuesOfNonExistentKeyReturnsNull() { + var dictionary = new MultiDictionary(); + Assert.AreEqual(0, dictionary.CountValues(1)); + } + + /// + /// Ensures that its possible to remove values individually without affecting + /// other values stored under the same key + /// + [Test] + public void ValuesCanBeRemovedIndividually() { + var dictionary = new MultiDictionary(); + dictionary.Add(10, "ten"); + dictionary.Add(10, "zehn"); + dictionary.Add(10, "dix"); + + dictionary.Remove(10, "zehn"); + + Assert.AreEqual(2, dictionary.Count); + CollectionAssert.AreEquivalent( + new KeyValuePair[] { + new KeyValuePair(10, "ten"), + new KeyValuePair(10, "dix") + }, + dictionary + ); + } + + /// + /// Verifies that the Count property returns the number of unique keys if it is called + /// on the collection-of-collections interface implemented by the multi dictionary + /// + [Test] + public void CollectionOfCollectionCountIsUniqueKeyCount() { + var dictionary = new MultiDictionary(); + dictionary.Add(10, "ten"); + dictionary.Add(10, "zehn"); + + Assert.AreEqual(2, dictionary.Count); + var collectionOfCollections = + (ICollection>>)dictionary; + Assert.AreEqual(1, collectionOfCollections.Count); + } + + /// + /// Verifies that the multi dictionary can be tested for containment of a specific value + /// + [Test] + public void ContainmentCanBeTested() { + var dictionary = new MultiDictionary(); + dictionary.Add(10, "ten"); + dictionary.Add(10, "zehn"); + + Assert.IsTrue(dictionary.Contains(new KeyValuePair(10, "ten"))); + Assert.IsTrue(dictionary.Contains(new KeyValuePair(10, "zehn"))); + Assert.IsFalse(dictionary.Contains(new KeyValuePair(10, "dix"))); + Assert.IsFalse(dictionary.Contains(new KeyValuePair(20, "ten"))); + } + + /// + /// Verifies that the multi dictionary can be tested for containment of a specific key + /// + [Test] + public void KeyContainmentCanBeTested() { + var dictionary = new MultiDictionary(); + dictionary.Add(10, "ten"); + dictionary.Add(10, "zehn"); + + Assert.IsTrue(dictionary.ContainsKey(10)); + Assert.IsFalse(dictionary.ContainsKey(20)); + } + + /// + /// Verifies that the key collection can be retrieved from the dictionary + /// + [Test] + public void KeyCollectionCanBeRetrieved() { + var dictionary = new MultiDictionary(); + dictionary.Add(10, "ten"); + dictionary.Add(10, "zehn"); + + ICollection keys = dictionary.Keys; + Assert.IsNotNull(keys); + Assert.AreEqual(1, keys.Count); + } + + /// + /// Verifies that the key collection can be retrieved from the dictionary + /// + [Test] + public void ValueCollectionCanBeRetrieved() { + var dictionary = new MultiDictionary(); + dictionary.Add(10, "ten"); + dictionary.Add(10, "zehn"); + dictionary.Add(20, "twenty"); + + ICollection values = dictionary.Values; + Assert.IsNotNull(values); + Assert.AreEqual(3, values.Count); + } + + /// + /// Verifies that TryGetValue() returns false and doesn't throw if a key + /// is not found in the collection + /// + [Test] + public void TryGetValueReturnsFalseOnMissingKey() { + var dictionary = new MultiDictionary(); + ICollection values; + Assert.IsFalse(dictionary.TryGetValue(123, out values)); + } + + /// Verifies that keys can be looked up via TryGetValue() + [Test] + public void TryGetValueCanLookUpValues() { + var dictionary = new MultiDictionary(); + dictionary.Add(10, "ten"); + dictionary.Add(10, "zehn"); + ICollection values; + Assert.IsTrue(dictionary.TryGetValue(10, out values)); + Assert.AreEqual(2, values.Count); + } + + /// + /// Verifies that assigning null to a key deletes all the values stored + /// under it + /// + [Test] + public void AssigningNullToKeyRemovesAllValues() { + var dictionary = new MultiDictionary(); + dictionary.Add(10, "ten"); + dictionary.Add(10, "zehn"); + dictionary.Add(20, "twenty"); + + Assert.AreEqual(3, dictionary.Count); + dictionary[10] = null; + Assert.AreEqual(1, dictionary.Count); + Assert.IsFalse(dictionary.ContainsKey(10)); + } + + /// + /// Verifies that assigning null to a key deletes all the values stored + /// under it + /// + [Test] + public void ValueListCanBeAssignedToNewKey() { + var dictionary = new MultiDictionary(); + dictionary[3] = new List() { "three", "drei" }; + + Assert.AreEqual(2, dictionary.Count); + Assert.IsTrue(dictionary.Contains(new KeyValuePair(3, "three"))); + } + + /// + /// Verifies that assigning null to a key deletes all the values stored + /// under it + /// + [Test] + public void ValueListCanOverwriteExistingKey() { + var dictionary = new MultiDictionary(); + dictionary.Add(10, "dix"); + + Assert.AreEqual(1, dictionary.Count); + + dictionary[10] = new List() { "ten", "zehn" }; + + Assert.AreEqual(2, dictionary.Count); + Assert.IsFalse(dictionary.Contains(new KeyValuePair(10, "dix"))); + Assert.IsTrue(dictionary.Contains(new KeyValuePair(10, "ten"))); + } + + /// + /// Verifies that nothing bad happens when a key is removed from the dictionary + /// that it doesn't contain + /// + [Test] + public void NonExistingKeyCanBeRemoved() { + var dictionary = new MultiDictionary(); + Assert.AreEqual(0, dictionary.RemoveKey(123)); + } + + /// + /// Verifies that the remove method returns the number of values that have + /// been removed from the dictionary + /// + [Test] + public void RemoveReturnsNumberOfValuesRemoved() { + var dictionary = new MultiDictionary(); + dictionary.Add(10, "ten"); + dictionary.Add(10, "zehn"); + Assert.AreEqual(2, dictionary.RemoveKey(10)); + } + + /// + /// Verifies that the dictionary becomes empty after clearing it + /// + [Test] + public void DictionaryIsEmptyAfterClear() { + var dictionary = new MultiDictionary(); + dictionary.Add(10, "ten"); + dictionary.Add(10, "zehn"); + dictionary.Add(20, "twenty"); + Assert.AreEqual(3, dictionary.Count); + dictionary.Clear(); + Assert.AreEqual(0, dictionary.Count); + } + + /// + /// Verifies that non-existing values can be removed from the dictionary + /// + [Test] + public void NonExistingValueCanBeRemoved() { + var dictionary = new MultiDictionary(); + Assert.IsFalse(dictionary.Remove(123, "test")); + } + + /// + /// Verifies that nothing bad happens when the last value under a key is removed + /// + [Test] + public void LastValueOfKeyCanBeRemoved() { + var dictionary = new MultiDictionary(); + dictionary.Add(123, "test"); + dictionary.Remove(123, "test"); + Assert.AreEqual(0, dictionary.CountValues(123)); + } + + /// + /// Verifies that the dictionary can be copied into an array + /// + [Test] + public void DictionaryCanBeCopiedIntoArray() { + var expected = new List>() { + new KeyValuePair(1, "one"), + new KeyValuePair(1, "eins"), + new KeyValuePair(2, "two"), + new KeyValuePair(2, "zwei") + }; + + var dictionary = new MultiDictionary(); + foreach(KeyValuePair entry in expected) { + dictionary.Add(entry.Key, entry.Value); + } + + var actual = new KeyValuePair[4]; + dictionary.CopyTo(actual, 0); + + CollectionAssert.AreEquivalent(expected, actual); + } + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST diff --git a/Source/Collections/MultiDictionary.ValueCollection.cs b/Source/Collections/MultiDictionary.ValueCollection.cs index f560b56..95e4a07 100644 --- a/Source/Collections/MultiDictionary.ValueCollection.cs +++ b/Source/Collections/MultiDictionary.ValueCollection.cs @@ -1,269 +1,268 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; -using System.Collections; - -namespace Nuclex.Support.Collections { - - partial class MultiDictionary { - - /// - /// Provides access to the values stored in a multi dictionary as a collection - /// - private class ValueCollection : ICollection, ICollection { - - #region class Enumerator - - /// Enumerates the values stored in a multi dictionary - private class Enumerator : IEnumerator { - - /// Initializes a new enumerator - /// Value collections being enumerated - public Enumerator(ICollection> valueCollections) { - this.valueCollections = valueCollections; - - Reset(); - } - - /// Immediately releases all resources owned by the instance - public void Dispose() { - if(this.currentValue != null) { - this.currentValue.Dispose(); - this.currentValue = null; - } - if(this.currentCollection != null) { - this.currentCollection.Dispose(); - this.currentCollection = null; - } - } - - /// The current value the enumerator is pointing at - public TValue Current { - get { - if(this.currentValue == null) { - throw new InvalidOperationException("Enumerator is not on a valid position"); - } - - return this.currentValue.Current; - } - } - - /// Advances the enumerator to the next item - /// - /// True if there was a next item, false if the enumerator reached the end - /// - public bool MoveNext() { - if(this.currentCollection == null) { - return false; - } - - for(; ; ) { - - // Try to move the enumerator in the current key's list to the next item - if(this.currentValue != null) { - if(this.currentValue.MoveNext()) { - return true; // We found the next item - } else { - this.currentValue.Dispose(); - } - } - - // Enumerator for the current key's list reached the end, go to the next key - if(this.currentCollection.MoveNext()) { - this.currentValue = this.currentCollection.Current.GetEnumerator(); - } else { - this.currentValue = null; // Guaranteed to be disposed already - this.currentCollection.Dispose(); - this.currentCollection = null; - return false; - } - - } - } - - /// Resets the enumerator to its initial position - public void Reset() { - if(this.currentValue != null) { - this.currentValue.Dispose(); - this.currentValue = null; - } - if(this.currentCollection != null) { - this.currentCollection.Dispose(); - } - this.currentCollection = valueCollections.GetEnumerator(); - } - - #region IEnumerator implementation - - /// The current entry the enumerator is pointing at - object IEnumerator.Current { - get { return Current; } - } - - #endregion // IEnumerator implementation - - /// Value collections being enumerated - private ICollection> valueCollections; - /// The current value collection the enumerator is in - private IEnumerator> currentCollection; - /// Current value in the collection the enumerator is in - private IEnumerator currentValue; - - } - - #endregion // class Enumerator - - /// Initializes a new multi dictionary value collection - /// Dictionary whose values the collection represents - public ValueCollection(MultiDictionary dictionary) { - this.dictionary = dictionary; - this.dictionaryAsICollection = (ICollection)dictionary; - } - - /// Determines whether the collection contains a specific value - /// Value for which the collection will be checked - /// True if the collection contains the specified value - public bool Contains(TValue item) { - foreach(ICollection values in this.dictionary.Values) { - if(values.Contains(item)) { - return true; - } - } - - return false; - } - - /// Copies the contents of the collection into an array - /// Array the collection contents will be copied into - /// - /// Starting index in the array where writing will begin - /// - public void CopyTo(TValue[] array, int arrayIndex) { - foreach(ICollection values in this.dictionary.Values) { - foreach(TValue value in values) { - array[arrayIndex] = value; - ++arrayIndex; - } - } - } - - /// The number of values in the collection - public int Count { - get { return this.dictionary.count; } - } - - /// Always true since the value collection is read-only - public bool IsReadOnly { - get { return true; } - } - - /// Returns a new enumerator for the value collection - /// A new enumerator for the value collection - public IEnumerator GetEnumerator() { - return new Enumerator(this.dictionary.typedDictionary.Values); - } - - #region IEnumerator implementation - - /// Returns a non-typesafe enumerator for the collection - /// The non-typesafe collection enumerator - IEnumerator IEnumerable.GetEnumerator() { - return GetEnumerator(); - } - - #endregion // IEnumerator implementation - - #region ICollection<> implementation - - /// Throws a NotSupportedException - /// Not used - void ICollection.Add(TValue item) { - throw new NotSupportedException( - "Items cannot be added to a dictionary through its values collection" - ); - } - - /// Throws a NotSupportedException - void ICollection.Clear() { - throw new NotSupportedException( - "The values collection of a dictionary cannot be cleared" - ); - } - - /// Throws a NotSupportedException - /// Not used - /// Nothing, since the method always throws an exception - bool ICollection.Contains(TValue item) { - throw new NotImplementedException(); - } - - /// Not supported - /// Item that will not be removed - /// Nothing because the method throws an exception - bool ICollection.Remove(TValue item) { - throw new NotSupportedException( - "Items cannot be removed from a dictionary through its values collection" - ); - } - - #endregion ICollection<> implementation - - #region ICollection implementation - - /// Copies the contents of the collection into an array - /// Array the collection's contents are copied into - /// - /// Starting index in the array where writing will begin - /// - void ICollection.CopyTo(Array array, int arrayIndex) { - foreach(ICollection values in this.dictionary.Values) { - foreach(TValue value in values) { - array.SetValue(value, arrayIndex); - ++arrayIndex; - } - } - } - - /// Whether the dictionary is thread-safe - bool ICollection.IsSynchronized { - get { return this.dictionaryAsICollection.IsSynchronized; } - } - - /// - /// The synchronization root used by the dictionary for thread synchronization - /// - object ICollection.SyncRoot { - get { return this.dictionaryAsICollection.SyncRoot; } - } - - #endregion // ICollection implementation - - /// Dictionary whose values the collection represents - private MultiDictionary dictionary; - /// The dictionary under its ICollection interface - private ICollection dictionaryAsICollection; - - } - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; +using System.Collections; + +namespace Nuclex.Support.Collections { + + partial class MultiDictionary { + + /// + /// Provides access to the values stored in a multi dictionary as a collection + /// + private class ValueCollection : ICollection, ICollection { + + #region class Enumerator + + /// Enumerates the values stored in a multi dictionary + private class Enumerator : IEnumerator { + + /// Initializes a new enumerator + /// Value collections being enumerated + public Enumerator(ICollection> valueCollections) { + this.valueCollections = valueCollections; + + Reset(); + } + + /// Immediately releases all resources owned by the instance + public void Dispose() { + if(this.currentValue != null) { + this.currentValue.Dispose(); + this.currentValue = null; + } + if(this.currentCollection != null) { + this.currentCollection.Dispose(); + this.currentCollection = null; + } + } + + /// The current value the enumerator is pointing at + public TValue Current { + get { + if(this.currentValue == null) { + throw new InvalidOperationException("Enumerator is not on a valid position"); + } + + return this.currentValue.Current; + } + } + + /// Advances the enumerator to the next item + /// + /// True if there was a next item, false if the enumerator reached the end + /// + public bool MoveNext() { + if(this.currentCollection == null) { + return false; + } + + for(; ; ) { + + // Try to move the enumerator in the current key's list to the next item + if(this.currentValue != null) { + if(this.currentValue.MoveNext()) { + return true; // We found the next item + } else { + this.currentValue.Dispose(); + } + } + + // Enumerator for the current key's list reached the end, go to the next key + if(this.currentCollection.MoveNext()) { + this.currentValue = this.currentCollection.Current.GetEnumerator(); + } else { + this.currentValue = null; // Guaranteed to be disposed already + this.currentCollection.Dispose(); + this.currentCollection = null; + return false; + } + + } + } + + /// Resets the enumerator to its initial position + public void Reset() { + if(this.currentValue != null) { + this.currentValue.Dispose(); + this.currentValue = null; + } + if(this.currentCollection != null) { + this.currentCollection.Dispose(); + } + this.currentCollection = valueCollections.GetEnumerator(); + } + + #region IEnumerator implementation + + /// The current entry the enumerator is pointing at + object IEnumerator.Current { + get { return Current; } + } + + #endregion // IEnumerator implementation + + /// Value collections being enumerated + private ICollection> valueCollections; + /// The current value collection the enumerator is in + private IEnumerator> currentCollection; + /// Current value in the collection the enumerator is in + private IEnumerator currentValue; + + } + + #endregion // class Enumerator + + /// Initializes a new multi dictionary value collection + /// Dictionary whose values the collection represents + public ValueCollection(MultiDictionary dictionary) { + this.dictionary = dictionary; + this.dictionaryAsICollection = (ICollection)dictionary; + } + + /// Determines whether the collection contains a specific value + /// Value for which the collection will be checked + /// True if the collection contains the specified value + public bool Contains(TValue item) { + foreach(ICollection values in this.dictionary.Values) { + if(values.Contains(item)) { + return true; + } + } + + return false; + } + + /// Copies the contents of the collection into an array + /// Array the collection contents will be copied into + /// + /// Starting index in the array where writing will begin + /// + public void CopyTo(TValue[] array, int arrayIndex) { + foreach(ICollection values in this.dictionary.Values) { + foreach(TValue value in values) { + array[arrayIndex] = value; + ++arrayIndex; + } + } + } + + /// The number of values in the collection + public int Count { + get { return this.dictionary.count; } + } + + /// Always true since the value collection is read-only + public bool IsReadOnly { + get { return true; } + } + + /// Returns a new enumerator for the value collection + /// A new enumerator for the value collection + public IEnumerator GetEnumerator() { + return new Enumerator(this.dictionary.typedDictionary.Values); + } + + #region IEnumerator implementation + + /// Returns a non-typesafe enumerator for the collection + /// The non-typesafe collection enumerator + IEnumerator IEnumerable.GetEnumerator() { + return GetEnumerator(); + } + + #endregion // IEnumerator implementation + + #region ICollection<> implementation + + /// Throws a NotSupportedException + /// Not used + void ICollection.Add(TValue item) { + throw new NotSupportedException( + "Items cannot be added to a dictionary through its values collection" + ); + } + + /// Throws a NotSupportedException + void ICollection.Clear() { + throw new NotSupportedException( + "The values collection of a dictionary cannot be cleared" + ); + } + + /// Throws a NotSupportedException + /// Not used + /// Nothing, since the method always throws an exception + bool ICollection.Contains(TValue item) { + throw new NotImplementedException(); + } + + /// Not supported + /// Item that will not be removed + /// Nothing because the method throws an exception + bool ICollection.Remove(TValue item) { + throw new NotSupportedException( + "Items cannot be removed from a dictionary through its values collection" + ); + } + + #endregion ICollection<> implementation + + #region ICollection implementation + + /// Copies the contents of the collection into an array + /// Array the collection's contents are copied into + /// + /// Starting index in the array where writing will begin + /// + void ICollection.CopyTo(Array array, int arrayIndex) { + foreach(ICollection values in this.dictionary.Values) { + foreach(TValue value in values) { + array.SetValue(value, arrayIndex); + ++arrayIndex; + } + } + } + + /// Whether the dictionary is thread-safe + bool ICollection.IsSynchronized { + get { return this.dictionaryAsICollection.IsSynchronized; } + } + + /// + /// The synchronization root used by the dictionary for thread synchronization + /// + object ICollection.SyncRoot { + get { return this.dictionaryAsICollection.SyncRoot; } + } + + #endregion // ICollection implementation + + /// Dictionary whose values the collection represents + private MultiDictionary dictionary; + /// The dictionary under its ICollection interface + private ICollection dictionaryAsICollection; + + } + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/MultiDictionary.cs b/Source/Collections/MultiDictionary.cs index d514bc2..933b255 100644 --- a/Source/Collections/MultiDictionary.cs +++ b/Source/Collections/MultiDictionary.cs @@ -1,415 +1,414 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace Nuclex.Support.Collections { - - /// Dictionary that can contain multiple values under the same key - /// Type of keys used within the dictionary - /// Type of values used within the dictionary - public partial class MultiDictionary : IMultiDictionary { - - #region class Enumerator - - /// Enumerates the values stored in a multi dictionary - private class Enumerator : - IDictionaryEnumerator, - IEnumerator> { - - /// Initializes a new multi dictionary enumerator - /// Dictionary that will be enumerated - public Enumerator(MultiDictionary dictionary) { - this.dictionary = dictionary; - - Reset(); - } - - /// The current entry the enumerator is pointing at - public KeyValuePair Current { - get { - if(this.currentValue == null) { - throw new InvalidOperationException("Enumerator is not on a valid position"); - } - - return new KeyValuePair( - this.currentCollection.Current.Key, this.currentValue.Current - ); - } - } - - /// Immediately releases all resources owned by the instance - public void Dispose() { - if(this.currentValue != null) { - this.currentValue.Dispose(); - this.currentValue = null; - } - if(this.currentCollection != null) { - this.currentCollection.Dispose(); - this.currentCollection = null; - } - } - - /// Advances the enumerator to the entry - /// - /// True if there was a next entry, false if the end of the set has been reached - /// - public bool MoveNext() { - if(this.currentCollection == null) { - return false; - } - - for(; ; ) { - - // Try to move the enumerator in the current key's list to the next item - if(this.currentValue != null) { - if(this.currentValue.MoveNext()) { - return true; // We found the next item - } else { - this.currentValue.Dispose(); - } - } - - // Enumerator for the current key's list reached the end, go to the next key - if(this.currentCollection.MoveNext()) { - this.currentValue = this.currentCollection.Current.Value.GetEnumerator(); - } else { - this.currentValue = null; // Guaranteed to be disposed already - this.currentCollection.Dispose(); - this.currentCollection = null; - return false; - } - - } - } - - /// Resets the enumerator to its initial position - public void Reset() { - if(this.currentValue != null) { - this.currentValue.Dispose(); - this.currentValue = null; - } - if(this.currentCollection != null) { - this.currentCollection.Dispose(); - } - this.currentCollection = this.dictionary.GetEnumerator(); - } - - #region IEnumerator implementation - - /// The item the enumerator is currently pointing at - object IEnumerator.Current { - get { return Current; } - } - - #endregion // IEnumerator implementation - - #region IDictionaryEnumerator implementation - - /// The current entry the enumerator is pointing to - DictionaryEntry IDictionaryEnumerator.Entry { - get { - enforceEnumeratorOnValidPosition(); - - return new DictionaryEntry( - this.currentCollection.Current.Key, this.currentValue.Current - ); - } - } - - /// The current dictionary key - object IDictionaryEnumerator.Key { - get { - enforceEnumeratorOnValidPosition(); - return this.currentCollection.Current.Key; - } - } - - /// The current dictionary value - object IDictionaryEnumerator.Value { - get { - enforceEnumeratorOnValidPosition(); - return this.currentValue.Current; - } - } - - #endregion // IDictionaryEnumerator implementation - - /// - /// Throws an exception if the enumerator is not on a valid position - /// - private void enforceEnumeratorOnValidPosition() { - if(this.currentValue == null) { - throw new InvalidOperationException("Enumerator is not on a valid position"); - } - } - - /// Dictionary over whose entries the enumerator is enumerating - private IDictionary> dictionary; - /// Current key the enumerator is at - private IEnumerator>> currentCollection; - /// Current value in the current key the enumerator is at - private IEnumerator currentValue; - - } - - #endregion // class Enumerator - - #region class ValueList - - /// Stores the list of values for a dictionary key - private class ValueList : Collection { - - /// Initializes a new value list - /// Dictionary the value list belongs to - public ValueList(MultiDictionary dictionary) { - this.dictionary = dictionary; - } - - /// Called when the value list is being cleared - protected override void ClearItems() { - this.dictionary.count -= Count; - base.ClearItems(); - } - - /// Called when an item is inserted into the value list - /// Index at which the item is being inserted - /// Item that is being inserted - protected override void InsertItem(int index, TValue item) { - base.InsertItem(index, item); - ++this.dictionary.count; - } - - /// Called when an item is removed from the value list - /// Index at which the item is being removed - protected override void RemoveItem(int index) { - base.RemoveItem(index); - --this.dictionary.count; - } - - /// The dictionary the value list belongs to - private MultiDictionary dictionary; - - } - - #endregion // class ValueList - - /// Initializes a new multi dictionary - public MultiDictionary() : this(new Dictionary>()) { } - - /// Initializes a new multi dictionary - /// Dictionary the multi dictionary will be based on - internal MultiDictionary(IDictionary> dictionary) { - this.typedDictionary = dictionary; - this.objectDictionary = (this.typedDictionary as IDictionary); - - foreach(ICollection values in dictionary.Values) { - this.count += values.Count; - } - } - - /// Whether the dictionary is write-protected - public bool IsReadOnly { - get { return this.typedDictionary.IsReadOnly; } - } - - /// Determines the number of values stored under the specified key - /// Key whose values will be counted - /// The number of values stored under the specified key - public int CountValues(TKey key) { - ICollection values; - if(this.typedDictionary.TryGetValue(key, out values)) { - return values.Count; - } else { - return 0; - } - } - - /// - /// Determines whether the specified KeyValuePair is contained in the dictionary - /// - /// KeyValuePair that will be checked for - /// True if the provided KeyValuePair was contained in the dictionary - public bool Contains(KeyValuePair item) { - ICollection values; - if(this.typedDictionary.TryGetValue(item.Key, out values)) { - return values.Contains(item.Value); - } else { - return false; - } - } - - /// Determines whether the Dictionary contains the specified key - /// Key that will be checked for - /// - /// True if an entry with the specified key was contained in the Dictionary - /// - public bool ContainsKey(TKey key) { - return this.typedDictionary.ContainsKey(key); - } - - /// Copies the contents of the Dictionary into an array - /// Array the Dictionary will be copied into - /// - /// Starting index at which to begin filling the destination array - /// - public void CopyTo(KeyValuePair[] array, int arrayIndex) { - foreach(KeyValuePair> item in this.typedDictionary) { - foreach(TValue value in item.Value) { - array[arrayIndex] = new KeyValuePair(item.Key, value); - ++arrayIndex; - } - } - } - - /// Number of elements contained in the multi dictionary - public int Count { - get { return this.count; } - } - - /// Creates a new enumerator for the dictionary - /// The new dictionary enumerator - public IEnumerator> GetEnumerator() { - return new Enumerator(this); - } - - /// Collection of all keys contained in the dictionary - public ICollection Keys { - get { return this.typedDictionary.Keys; } - } - - /// Collection of all values contained in the dictionary - public ICollection Values { - get { - if(this.valueCollection == null) { - this.valueCollection = new ValueCollection(this); - } - - return this.valueCollection; - } - } - - /// - /// Attempts to retrieve the item with the specified key from the dictionary - /// - /// Key of the item to attempt to retrieve - /// - /// Output parameter that will receive the values upon successful completion - /// - /// - /// True if the item was found and has been placed in the output parameter - /// - public bool TryGetValue(TKey key, out ICollection values) { - return this.typedDictionary.TryGetValue(key, out values); - } - - /// Accesses an item in the dictionary by its key - /// Key of the item that will be accessed - public ICollection this[TKey key] { - get { return this.typedDictionary[key]; } - set { - if(value == null) { - RemoveKey(key); - } else { - ICollection currentValues; - if(this.typedDictionary.TryGetValue(key, out currentValues)) { - currentValues.Clear(); - } else { - currentValues = new ValueList(this); - this.typedDictionary.Add(key, currentValues); - } - foreach(TValue addedValue in value) { - currentValues.Add(addedValue); - } - } - } - } - - /// Inserts an item into the dictionary - /// Key under which to add the new item - /// Item that will be added to the dictionary - public void Add(TKey key, TValue value) { - ICollection values; - if(!this.typedDictionary.TryGetValue(key, out values)) { - values = new ValueList(this); - this.typedDictionary.Add(key, values); - } - - values.Add(value); - } - - /// - /// Removes the item with the specified key and value from the dictionary - /// - /// Key of the item that will be removed - /// Value of the item that will be removed - /// - /// True if the specified item was contained in the dictionary and was removed - /// - /// If the dictionary is read-only - public bool Remove(TKey key, TValue value) { - ICollection values; - if(this.typedDictionary.TryGetValue(key, out values)) { - values.Remove(value); - if(values.Count == 0) { - this.typedDictionary.Remove(key); - } - return true; - } else { - return false; - } - } - - /// Removes all items with the specified key from the dictionary - /// Key of the item that will be removed - /// The number of items that have been removed from the dictionary - /// If the dictionary is read-only - public int RemoveKey(TKey key) { - ICollection values; - if(this.typedDictionary.TryGetValue(key, out values)) { - this.count -= values.Count; - this.typedDictionary.Remove(key); - return values.Count; - } else { - return 0; - } - } - - /// Removes all items from the Dictionary - public void Clear() { - this.typedDictionary.Clear(); - this.count = 0; - } - - /// The wrapped Dictionary under its type-safe interface - private IDictionary> typedDictionary; - /// The wrapped Dictionary under its object interface - private IDictionary objectDictionary; - /// The number of items currently in the multi dictionary - private int count; - /// Provides the values stores in the dictionary in sequence - private ValueCollection valueCollection; - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Nuclex.Support.Collections { + + /// Dictionary that can contain multiple values under the same key + /// Type of keys used within the dictionary + /// Type of values used within the dictionary + public partial class MultiDictionary : IMultiDictionary { + + #region class Enumerator + + /// Enumerates the values stored in a multi dictionary + private class Enumerator : + IDictionaryEnumerator, + IEnumerator> { + + /// Initializes a new multi dictionary enumerator + /// Dictionary that will be enumerated + public Enumerator(MultiDictionary dictionary) { + this.dictionary = dictionary; + + Reset(); + } + + /// The current entry the enumerator is pointing at + public KeyValuePair Current { + get { + if(this.currentValue == null) { + throw new InvalidOperationException("Enumerator is not on a valid position"); + } + + return new KeyValuePair( + this.currentCollection.Current.Key, this.currentValue.Current + ); + } + } + + /// Immediately releases all resources owned by the instance + public void Dispose() { + if(this.currentValue != null) { + this.currentValue.Dispose(); + this.currentValue = null; + } + if(this.currentCollection != null) { + this.currentCollection.Dispose(); + this.currentCollection = null; + } + } + + /// Advances the enumerator to the entry + /// + /// True if there was a next entry, false if the end of the set has been reached + /// + public bool MoveNext() { + if(this.currentCollection == null) { + return false; + } + + for(; ; ) { + + // Try to move the enumerator in the current key's list to the next item + if(this.currentValue != null) { + if(this.currentValue.MoveNext()) { + return true; // We found the next item + } else { + this.currentValue.Dispose(); + } + } + + // Enumerator for the current key's list reached the end, go to the next key + if(this.currentCollection.MoveNext()) { + this.currentValue = this.currentCollection.Current.Value.GetEnumerator(); + } else { + this.currentValue = null; // Guaranteed to be disposed already + this.currentCollection.Dispose(); + this.currentCollection = null; + return false; + } + + } + } + + /// Resets the enumerator to its initial position + public void Reset() { + if(this.currentValue != null) { + this.currentValue.Dispose(); + this.currentValue = null; + } + if(this.currentCollection != null) { + this.currentCollection.Dispose(); + } + this.currentCollection = this.dictionary.GetEnumerator(); + } + + #region IEnumerator implementation + + /// The item the enumerator is currently pointing at + object IEnumerator.Current { + get { return Current; } + } + + #endregion // IEnumerator implementation + + #region IDictionaryEnumerator implementation + + /// The current entry the enumerator is pointing to + DictionaryEntry IDictionaryEnumerator.Entry { + get { + enforceEnumeratorOnValidPosition(); + + return new DictionaryEntry( + this.currentCollection.Current.Key, this.currentValue.Current + ); + } + } + + /// The current dictionary key + object IDictionaryEnumerator.Key { + get { + enforceEnumeratorOnValidPosition(); + return this.currentCollection.Current.Key; + } + } + + /// The current dictionary value + object IDictionaryEnumerator.Value { + get { + enforceEnumeratorOnValidPosition(); + return this.currentValue.Current; + } + } + + #endregion // IDictionaryEnumerator implementation + + /// + /// Throws an exception if the enumerator is not on a valid position + /// + private void enforceEnumeratorOnValidPosition() { + if(this.currentValue == null) { + throw new InvalidOperationException("Enumerator is not on a valid position"); + } + } + + /// Dictionary over whose entries the enumerator is enumerating + private IDictionary> dictionary; + /// Current key the enumerator is at + private IEnumerator>> currentCollection; + /// Current value in the current key the enumerator is at + private IEnumerator currentValue; + + } + + #endregion // class Enumerator + + #region class ValueList + + /// Stores the list of values for a dictionary key + private class ValueList : Collection { + + /// Initializes a new value list + /// Dictionary the value list belongs to + public ValueList(MultiDictionary dictionary) { + this.dictionary = dictionary; + } + + /// Called when the value list is being cleared + protected override void ClearItems() { + this.dictionary.count -= Count; + base.ClearItems(); + } + + /// Called when an item is inserted into the value list + /// Index at which the item is being inserted + /// Item that is being inserted + protected override void InsertItem(int index, TValue item) { + base.InsertItem(index, item); + ++this.dictionary.count; + } + + /// Called when an item is removed from the value list + /// Index at which the item is being removed + protected override void RemoveItem(int index) { + base.RemoveItem(index); + --this.dictionary.count; + } + + /// The dictionary the value list belongs to + private MultiDictionary dictionary; + + } + + #endregion // class ValueList + + /// Initializes a new multi dictionary + public MultiDictionary() : this(new Dictionary>()) { } + + /// Initializes a new multi dictionary + /// Dictionary the multi dictionary will be based on + internal MultiDictionary(IDictionary> dictionary) { + this.typedDictionary = dictionary; + this.objectDictionary = (this.typedDictionary as IDictionary); + + foreach(ICollection values in dictionary.Values) { + this.count += values.Count; + } + } + + /// Whether the dictionary is write-protected + public bool IsReadOnly { + get { return this.typedDictionary.IsReadOnly; } + } + + /// Determines the number of values stored under the specified key + /// Key whose values will be counted + /// The number of values stored under the specified key + public int CountValues(TKey key) { + ICollection values; + if(this.typedDictionary.TryGetValue(key, out values)) { + return values.Count; + } else { + return 0; + } + } + + /// + /// Determines whether the specified KeyValuePair is contained in the dictionary + /// + /// KeyValuePair that will be checked for + /// True if the provided KeyValuePair was contained in the dictionary + public bool Contains(KeyValuePair item) { + ICollection values; + if(this.typedDictionary.TryGetValue(item.Key, out values)) { + return values.Contains(item.Value); + } else { + return false; + } + } + + /// Determines whether the Dictionary contains the specified key + /// Key that will be checked for + /// + /// True if an entry with the specified key was contained in the Dictionary + /// + public bool ContainsKey(TKey key) { + return this.typedDictionary.ContainsKey(key); + } + + /// Copies the contents of the Dictionary into an array + /// Array the Dictionary will be copied into + /// + /// Starting index at which to begin filling the destination array + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) { + foreach(KeyValuePair> item in this.typedDictionary) { + foreach(TValue value in item.Value) { + array[arrayIndex] = new KeyValuePair(item.Key, value); + ++arrayIndex; + } + } + } + + /// Number of elements contained in the multi dictionary + public int Count { + get { return this.count; } + } + + /// Creates a new enumerator for the dictionary + /// The new dictionary enumerator + public IEnumerator> GetEnumerator() { + return new Enumerator(this); + } + + /// Collection of all keys contained in the dictionary + public ICollection Keys { + get { return this.typedDictionary.Keys; } + } + + /// Collection of all values contained in the dictionary + public ICollection Values { + get { + if(this.valueCollection == null) { + this.valueCollection = new ValueCollection(this); + } + + return this.valueCollection; + } + } + + /// + /// Attempts to retrieve the item with the specified key from the dictionary + /// + /// Key of the item to attempt to retrieve + /// + /// Output parameter that will receive the values upon successful completion + /// + /// + /// True if the item was found and has been placed in the output parameter + /// + public bool TryGetValue(TKey key, out ICollection values) { + return this.typedDictionary.TryGetValue(key, out values); + } + + /// Accesses an item in the dictionary by its key + /// Key of the item that will be accessed + public ICollection this[TKey key] { + get { return this.typedDictionary[key]; } + set { + if(value == null) { + RemoveKey(key); + } else { + ICollection currentValues; + if(this.typedDictionary.TryGetValue(key, out currentValues)) { + currentValues.Clear(); + } else { + currentValues = new ValueList(this); + this.typedDictionary.Add(key, currentValues); + } + foreach(TValue addedValue in value) { + currentValues.Add(addedValue); + } + } + } + } + + /// Inserts an item into the dictionary + /// Key under which to add the new item + /// Item that will be added to the dictionary + public void Add(TKey key, TValue value) { + ICollection values; + if(!this.typedDictionary.TryGetValue(key, out values)) { + values = new ValueList(this); + this.typedDictionary.Add(key, values); + } + + values.Add(value); + } + + /// + /// Removes the item with the specified key and value from the dictionary + /// + /// Key of the item that will be removed + /// Value of the item that will be removed + /// + /// True if the specified item was contained in the dictionary and was removed + /// + /// If the dictionary is read-only + public bool Remove(TKey key, TValue value) { + ICollection values; + if(this.typedDictionary.TryGetValue(key, out values)) { + values.Remove(value); + if(values.Count == 0) { + this.typedDictionary.Remove(key); + } + return true; + } else { + return false; + } + } + + /// Removes all items with the specified key from the dictionary + /// Key of the item that will be removed + /// The number of items that have been removed from the dictionary + /// If the dictionary is read-only + public int RemoveKey(TKey key) { + ICollection values; + if(this.typedDictionary.TryGetValue(key, out values)) { + this.count -= values.Count; + this.typedDictionary.Remove(key); + return values.Count; + } else { + return 0; + } + } + + /// Removes all items from the Dictionary + public void Clear() { + this.typedDictionary.Clear(); + this.count = 0; + } + + /// The wrapped Dictionary under its type-safe interface + private IDictionary> typedDictionary; + /// The wrapped Dictionary under its object interface + private IDictionary objectDictionary; + /// The number of items currently in the multi dictionary + private int count; + /// Provides the values stores in the dictionary in sequence + private ValueCollection valueCollection; + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/ObservableCollection.Test.cs b/Source/Collections/ObservableCollection.Test.cs index a45798d..b95d20f 100644 --- a/Source/Collections/ObservableCollection.Test.cs +++ b/Source/Collections/ObservableCollection.Test.cs @@ -1,145 +1,144 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if !NO_NMOCK - -using System; -using System.Collections.Generic; - -#if UNITTEST - -using NUnit.Framework; -using NMock; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the observable collection class - [TestFixture] - internal class ObservableCollectionTest { - - #region interface IObservableCollectionSubscriber - - /// Interface used to test the observable collection - public interface IObservableCollectionSubscriber { - - /// Called when the collection is about to clear its contents - /// Collection that is clearing its contents - /// Not used - void Clearing(object sender, EventArgs arguments); - - /// Called when the collection has been cleared of its contents - /// Collection that was cleared of its contents - /// Not used - void Cleared(object sender, EventArgs arguments); - - /// Called when an item is added to the collection - /// Collection to which an item is being added - /// Contains the item that is being added - void ItemAdded(object sender, ItemEventArgs arguments); - - /// Called when an item is removed from the collection - /// Collection from which an item is being removed - /// Contains the item that is being removed - void ItemRemoved(object sender, ItemEventArgs arguments); - - } - - #endregion // interface IObservableCollectionSubscriber - - /// Initialization routine executed before each test is run - [SetUp] - public void Setup() { - this.mockery = new MockFactory(); - - this.mockedSubscriber = this.mockery.CreateMock(); - - this.observedCollection = new ObservableCollection(); - this.observedCollection.Clearing += new EventHandler( - this.mockedSubscriber.MockObject.Clearing - ); - this.observedCollection.Cleared += new EventHandler( - this.mockedSubscriber.MockObject.Cleared - ); - this.observedCollection.ItemAdded += new EventHandler>( - this.mockedSubscriber.MockObject.ItemAdded - ); - this.observedCollection.ItemRemoved += new EventHandler>( - this.mockedSubscriber.MockObject.ItemRemoved - ); - } - - /// Tests whether the Clearing event is fired - [Test] - public void TestClearingEvent() { - this.mockedSubscriber.Expects.One.Method(m => m.Clearing(null, null)).WithAnyArguments(); - this.mockedSubscriber.Expects.One.Method(m => m.Cleared(null, null)).WithAnyArguments(); - this.observedCollection.Clear(); - - this.mockery.VerifyAllExpectationsHaveBeenMet(); - } - - /// Tests whether the ItemAdded event is fired - [Test] - public void TestItemAddedEvent() { - this.mockedSubscriber.Expects.One.Method(m => m.ItemAdded(null, null)).WithAnyArguments(); - - this.observedCollection.Add(123); - - this.mockery.VerifyAllExpectationsHaveBeenMet(); - } - - /// Tests whether the ItemRemoved event is fired - [Test] - public void TestItemRemovedEvent() { - this.mockedSubscriber.Expects.One.Method(m => m.ItemAdded(null, null)).WithAnyArguments(); - - this.observedCollection.Add(123); - - this.mockedSubscriber.Expects.One.Method(m => m.ItemRemoved(null, null)).WithAnyArguments(); - - this.observedCollection.Remove(123); - - this.mockery.VerifyAllExpectationsHaveBeenMet(); - } - - /// Tests whether a the list constructor is working - [Test] - public void TestListConstructor() { - int[] integers = new int[] { 12, 34, 56, 78 }; - - var testCollection = new ObservableCollection(integers); - - CollectionAssert.AreEqual(integers, testCollection); - } - - /// Mock object factory - private MockFactory mockery; - /// The mocked observable collection subscriber - private Mock mockedSubscriber; - /// An observable collection to which a mock will be subscribed - private ObservableCollection observedCollection; - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST - -#endif // !NO_NMOCK +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if !NO_NMOCK + +using System; +using System.Collections.Generic; + +#if UNITTEST + +using NUnit.Framework; +using NMock; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the observable collection class + [TestFixture] + internal class ObservableCollectionTest { + + #region interface IObservableCollectionSubscriber + + /// Interface used to test the observable collection + public interface IObservableCollectionSubscriber { + + /// Called when the collection is about to clear its contents + /// Collection that is clearing its contents + /// Not used + void Clearing(object sender, EventArgs arguments); + + /// Called when the collection has been cleared of its contents + /// Collection that was cleared of its contents + /// Not used + void Cleared(object sender, EventArgs arguments); + + /// Called when an item is added to the collection + /// Collection to which an item is being added + /// Contains the item that is being added + void ItemAdded(object sender, ItemEventArgs arguments); + + /// Called when an item is removed from the collection + /// Collection from which an item is being removed + /// Contains the item that is being removed + void ItemRemoved(object sender, ItemEventArgs arguments); + + } + + #endregion // interface IObservableCollectionSubscriber + + /// Initialization routine executed before each test is run + [SetUp] + public void Setup() { + this.mockery = new MockFactory(); + + this.mockedSubscriber = this.mockery.CreateMock(); + + this.observedCollection = new ObservableCollection(); + this.observedCollection.Clearing += new EventHandler( + this.mockedSubscriber.MockObject.Clearing + ); + this.observedCollection.Cleared += new EventHandler( + this.mockedSubscriber.MockObject.Cleared + ); + this.observedCollection.ItemAdded += new EventHandler>( + this.mockedSubscriber.MockObject.ItemAdded + ); + this.observedCollection.ItemRemoved += new EventHandler>( + this.mockedSubscriber.MockObject.ItemRemoved + ); + } + + /// Tests whether the Clearing event is fired + [Test] + public void TestClearingEvent() { + this.mockedSubscriber.Expects.One.Method(m => m.Clearing(null, null)).WithAnyArguments(); + this.mockedSubscriber.Expects.One.Method(m => m.Cleared(null, null)).WithAnyArguments(); + this.observedCollection.Clear(); + + this.mockery.VerifyAllExpectationsHaveBeenMet(); + } + + /// Tests whether the ItemAdded event is fired + [Test] + public void TestItemAddedEvent() { + this.mockedSubscriber.Expects.One.Method(m => m.ItemAdded(null, null)).WithAnyArguments(); + + this.observedCollection.Add(123); + + this.mockery.VerifyAllExpectationsHaveBeenMet(); + } + + /// Tests whether the ItemRemoved event is fired + [Test] + public void TestItemRemovedEvent() { + this.mockedSubscriber.Expects.One.Method(m => m.ItemAdded(null, null)).WithAnyArguments(); + + this.observedCollection.Add(123); + + this.mockedSubscriber.Expects.One.Method(m => m.ItemRemoved(null, null)).WithAnyArguments(); + + this.observedCollection.Remove(123); + + this.mockery.VerifyAllExpectationsHaveBeenMet(); + } + + /// Tests whether a the list constructor is working + [Test] + public void TestListConstructor() { + int[] integers = new int[] { 12, 34, 56, 78 }; + + var testCollection = new ObservableCollection(integers); + + CollectionAssert.AreEqual(integers, testCollection); + } + + /// Mock object factory + private MockFactory mockery; + /// The mocked observable collection subscriber + private Mock mockedSubscriber; + /// An observable collection to which a mock will be subscribed + private ObservableCollection observedCollection; + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST + +#endif // !NO_NMOCK diff --git a/Source/Collections/ObservableCollection.cs b/Source/Collections/ObservableCollection.cs index f17e6eb..81c6a27 100644 --- a/Source/Collections/ObservableCollection.cs +++ b/Source/Collections/ObservableCollection.cs @@ -1,231 +1,230 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.ObjectModel; -#if !NO_SPECIALIZED_COLLECTIONS -using System.Collections.Specialized; -#endif - -namespace Nuclex.Support.Collections { - - /// Collection which fires events when items are added or removed - /// Type of items the collection manages - public class ObservableCollection : - ICollection, - ICollection, -#if !NO_SPECIALIZED_COLLECTIONS - INotifyCollectionChanged, -#endif - IObservableCollection { - - /// Raised when an item has been added to the collection - public event EventHandler> ItemAdded; - /// Raised when an item is removed from the collection - public event EventHandler> ItemRemoved; - /// Raised when an item is replaced in the collection - public event EventHandler> ItemReplaced { - add { } - remove { } - } - /// Raised when the collection is about to be cleared - /// - /// This could be covered by calling ItemRemoved for each item currently - /// contained in the collection, but it is often simpler and more efficient - /// to process the clearing of the entire collection as a special operation. - /// - public event EventHandler Clearing; - /// Raised when the collection has been cleared - public event EventHandler Cleared; - -#if !NO_SPECIALIZED_COLLECTIONS - /// Called when the collection has changed - public event NotifyCollectionChangedEventHandler CollectionChanged; -#endif - - /// Initializes a new ObservableCollection with no items - public ObservableCollection() : this(new Collection()) { } - - /// - /// Initializes a new ObservableCollection as a wrapper for an existing collection - /// - /// Collection that will be wrapped - /// List is null - public ObservableCollection(ICollection collection) { - this.typedCollection = collection; - this.objectCollection = (collection as ICollection); - } - - /// Removes all elements from the Collection - public void Clear() { - OnClearing(); - this.typedCollection.Clear(); - OnCleared(); - } - - /// Adds an item to the collection - /// Collection an item will be added to - public void Add(TItem item) { - this.typedCollection.Add(item); - OnAdded(item); - } - - /// Determines whether the collection contains the specified item - /// Item the collection will be searched for - /// - /// True if the collection contains the specified item, false otherwise - /// - public bool Contains(TItem item) { - return this.typedCollection.Contains(item); - } - - /// Copies the contents of the collection into an array - /// Array the collection's contents will be copied into - /// - /// Index in the array where the collection's first item will be placed - /// - public void CopyTo(TItem[] array, int arrayIndex) { - this.typedCollection.CopyTo(array, arrayIndex); - } - - /// The total number of items currently in the collection - public int Count { - get { return this.typedCollection.Count; } - } - - /// Whether the collection is read-only - public bool IsReadOnly { - get { return this.typedCollection.IsReadOnly; } - } - - /// Removes an item from the collection - /// Item that will be removed from the collection - /// True if the item was found and removed, false otherwise - public bool Remove(TItem item) { - bool wasRemoved = this.typedCollection.Remove(item); - if(wasRemoved) { - OnRemoved(item); - } - - return wasRemoved; - } - - /// Returns an enumerator for the items in the collection - /// An enumeration for the items in the collection - public IEnumerator GetEnumerator() { - return this.typedCollection.GetEnumerator(); - } - - /// Fires the 'ItemAdded' event - /// Item that has been added to the collection - protected virtual void OnAdded(TItem item) { - if(ItemAdded != null) { - ItemAdded(this, new ItemEventArgs(item)); - } -#if !NO_SPECIALIZED_COLLECTIONS - if(CollectionChanged != null) { - CollectionChanged( - this, - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item) - ); - } -#endif - } - - /// Fires the 'ItemRemoved' event - /// Item that has been removed from the collection - protected virtual void OnRemoved(TItem item) { - if(ItemRemoved != null) { - ItemRemoved(this, new ItemEventArgs(item)); - } -#if !NO_SPECIALIZED_COLLECTIONS - if(CollectionChanged != null) { - CollectionChanged( - this, - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item) - ); - } -#endif - } - - /// Fires the 'Clearing' event - protected virtual void OnClearing() { - if(Clearing != null) { - Clearing(this, EventArgs.Empty); - } - } - - /// Fires the 'Cleared' event - protected virtual void OnCleared() { - if(Cleared != null) { - Cleared(this, EventArgs.Empty); - } -#if !NO_SPECIALIZED_COLLECTIONS - if(CollectionChanged != null) { - CollectionChanged(this, Constants.NotifyCollectionResetEventArgs); - } -#endif - } - - #region IEnumerable implementation - - /// Returns an enumerator for the items in the collection - /// An enumeration for the items in the collection - IEnumerator IEnumerable.GetEnumerator() { - return this.objectCollection.GetEnumerator(); - } - - #endregion // IEnumerable implementation - - #region ICollection implementation - - /// Copies the contents of the collection into an array - /// Array the collection's contents will be copied into - /// - /// Index in the array where the collection's first item will be placed - /// - void ICollection.CopyTo(Array array, int arrayIndex) { - this.objectCollection.CopyTo(array, arrayIndex); - } - - /// Whether the collection synchronizes accesses from multiple threads - bool ICollection.IsSynchronized { - get { return this.objectCollection.IsSynchronized; } - } - - /// - /// Synchronization root used to synchronize threads accessing the collection - /// - object ICollection.SyncRoot { - get { return this.objectCollection.SyncRoot; } - } - - #endregion // IEnumerable implementation - - /// The wrapped collection under its type-safe interface - private ICollection typedCollection; - /// The wrapped collection under its object interface - private ICollection objectCollection; - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +#if !NO_SPECIALIZED_COLLECTIONS +using System.Collections.Specialized; +#endif + +namespace Nuclex.Support.Collections { + + /// Collection which fires events when items are added or removed + /// Type of items the collection manages + public class ObservableCollection : + ICollection, + ICollection, +#if !NO_SPECIALIZED_COLLECTIONS + INotifyCollectionChanged, +#endif + IObservableCollection { + + /// Raised when an item has been added to the collection + public event EventHandler> ItemAdded; + /// Raised when an item is removed from the collection + public event EventHandler> ItemRemoved; + /// Raised when an item is replaced in the collection + public event EventHandler> ItemReplaced { + add { } + remove { } + } + /// Raised when the collection is about to be cleared + /// + /// This could be covered by calling ItemRemoved for each item currently + /// contained in the collection, but it is often simpler and more efficient + /// to process the clearing of the entire collection as a special operation. + /// + public event EventHandler Clearing; + /// Raised when the collection has been cleared + public event EventHandler Cleared; + +#if !NO_SPECIALIZED_COLLECTIONS + /// Called when the collection has changed + public event NotifyCollectionChangedEventHandler CollectionChanged; +#endif + + /// Initializes a new ObservableCollection with no items + public ObservableCollection() : this(new Collection()) { } + + /// + /// Initializes a new ObservableCollection as a wrapper for an existing collection + /// + /// Collection that will be wrapped + /// List is null + public ObservableCollection(ICollection collection) { + this.typedCollection = collection; + this.objectCollection = (collection as ICollection); + } + + /// Removes all elements from the Collection + public void Clear() { + OnClearing(); + this.typedCollection.Clear(); + OnCleared(); + } + + /// Adds an item to the collection + /// Collection an item will be added to + public void Add(TItem item) { + this.typedCollection.Add(item); + OnAdded(item); + } + + /// Determines whether the collection contains the specified item + /// Item the collection will be searched for + /// + /// True if the collection contains the specified item, false otherwise + /// + public bool Contains(TItem item) { + return this.typedCollection.Contains(item); + } + + /// Copies the contents of the collection into an array + /// Array the collection's contents will be copied into + /// + /// Index in the array where the collection's first item will be placed + /// + public void CopyTo(TItem[] array, int arrayIndex) { + this.typedCollection.CopyTo(array, arrayIndex); + } + + /// The total number of items currently in the collection + public int Count { + get { return this.typedCollection.Count; } + } + + /// Whether the collection is read-only + public bool IsReadOnly { + get { return this.typedCollection.IsReadOnly; } + } + + /// Removes an item from the collection + /// Item that will be removed from the collection + /// True if the item was found and removed, false otherwise + public bool Remove(TItem item) { + bool wasRemoved = this.typedCollection.Remove(item); + if(wasRemoved) { + OnRemoved(item); + } + + return wasRemoved; + } + + /// Returns an enumerator for the items in the collection + /// An enumeration for the items in the collection + public IEnumerator GetEnumerator() { + return this.typedCollection.GetEnumerator(); + } + + /// Fires the 'ItemAdded' event + /// Item that has been added to the collection + protected virtual void OnAdded(TItem item) { + if(ItemAdded != null) { + ItemAdded(this, new ItemEventArgs(item)); + } +#if !NO_SPECIALIZED_COLLECTIONS + if(CollectionChanged != null) { + CollectionChanged( + this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item) + ); + } +#endif + } + + /// Fires the 'ItemRemoved' event + /// Item that has been removed from the collection + protected virtual void OnRemoved(TItem item) { + if(ItemRemoved != null) { + ItemRemoved(this, new ItemEventArgs(item)); + } +#if !NO_SPECIALIZED_COLLECTIONS + if(CollectionChanged != null) { + CollectionChanged( + this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item) + ); + } +#endif + } + + /// Fires the 'Clearing' event + protected virtual void OnClearing() { + if(Clearing != null) { + Clearing(this, EventArgs.Empty); + } + } + + /// Fires the 'Cleared' event + protected virtual void OnCleared() { + if(Cleared != null) { + Cleared(this, EventArgs.Empty); + } +#if !NO_SPECIALIZED_COLLECTIONS + if(CollectionChanged != null) { + CollectionChanged(this, Constants.NotifyCollectionResetEventArgs); + } +#endif + } + + #region IEnumerable implementation + + /// Returns an enumerator for the items in the collection + /// An enumeration for the items in the collection + IEnumerator IEnumerable.GetEnumerator() { + return this.objectCollection.GetEnumerator(); + } + + #endregion // IEnumerable implementation + + #region ICollection implementation + + /// Copies the contents of the collection into an array + /// Array the collection's contents will be copied into + /// + /// Index in the array where the collection's first item will be placed + /// + void ICollection.CopyTo(Array array, int arrayIndex) { + this.objectCollection.CopyTo(array, arrayIndex); + } + + /// Whether the collection synchronizes accesses from multiple threads + bool ICollection.IsSynchronized { + get { return this.objectCollection.IsSynchronized; } + } + + /// + /// Synchronization root used to synchronize threads accessing the collection + /// + object ICollection.SyncRoot { + get { return this.objectCollection.SyncRoot; } + } + + #endregion // IEnumerable implementation + + /// The wrapped collection under its type-safe interface + private ICollection typedCollection; + /// The wrapped collection under its object interface + private ICollection objectCollection; + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/ObservableDictionary.Test.cs b/Source/Collections/ObservableDictionary.Test.cs index b84ffe0..ce155fc 100644 --- a/Source/Collections/ObservableDictionary.Test.cs +++ b/Source/Collections/ObservableDictionary.Test.cs @@ -1,590 +1,589 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if !NO_NMOCK - -#if UNITTEST - -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Runtime.Serialization.Formatters.Binary; - -using NUnit.Framework; -using NMock; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the observable dictionary wrapper - [TestFixture] - internal class ObservableDictionaryTest { - - #region interface IObservableDictionarySubscriber - - /// Interface used to test the observable dictionary - public interface IObservableDictionarySubscriber { - - /// Called when the dictionary is about to clear its contents - /// Dictionary that is clearing its contents - /// Not used - void Clearing(object sender, EventArgs arguments); - - /// Called when the dictionary has been clear of its contents - /// Dictionary that was cleared of its contents - /// Not used - void Cleared(object sender, EventArgs arguments); - - /// Called when an item is added to the dictionary - /// Dictionary to which an item is being added - /// Contains the item that is being added - void ItemAdded(object sender, ItemEventArgs> arguments); - - /// Called when an item is removed from the dictionary - /// Dictionary from which an item is being removed - /// Contains the item that is being removed - void ItemRemoved(object sender, ItemEventArgs> arguments); - - /// Called when an item is replaced in the dictionary - /// Dictionary in which an item is being replaced - /// Contains the replaced item and its replacement - void ItemReplaced( - object sender, ItemReplaceEventArgs> arguments - ); - - } - - #endregion // interface IObservableDictionarySubscriber - - /// Initialization routine executed before each test is run - [SetUp] - public void Setup() { - this.mockery = new MockFactory(); - - this.mockedSubscriber = this.mockery.CreateMock(); - - this.observedDictionary = new ObservableDictionary(); - this.observedDictionary.Add(1, "one"); - this.observedDictionary.Add(2, "two"); - this.observedDictionary.Add(3, "three"); - this.observedDictionary.Add(42, "forty-two"); - - this.observedDictionary.Clearing += - new EventHandler(this.mockedSubscriber.MockObject.Clearing); - this.observedDictionary.Cleared += - new EventHandler(this.mockedSubscriber.MockObject.Cleared); - this.observedDictionary.ItemAdded += - new EventHandler>>( - this.mockedSubscriber.MockObject.ItemAdded - ); - this.observedDictionary.ItemRemoved += - new EventHandler>>( - this.mockedSubscriber.MockObject.ItemRemoved - ); - this.observedDictionary.ItemReplaced += - new EventHandler>>( - this.mockedSubscriber.MockObject.ItemReplaced - ); - } - - /// - /// Verifies that the default constructor of the observable dictionary works - /// - [Test] - public void TestDefaultConstructor() { - ObservableDictionary testDictionary = - new ObservableDictionary(); - - Assert.AreEqual(0, testDictionary.Count); - } - - /// - /// Verifies that the copy constructor of the observable dictionary works - /// - [Test] - public void TestCopyConstructor() { - Dictionary numbers = createTestDictionary(); - ObservableDictionary testDictionary = makeObservable(numbers); - - CollectionAssert.AreEqual(numbers, testDictionary); - } - - /// Verifies that the IsReadOnly property is working - [Test] - public void TestIsReadOnly() { - Dictionary numbers = createTestDictionary(); - ObservableDictionary testDictionary = makeObservable(numbers); - - Assert.IsFalse(testDictionary.IsReadOnly); - } - - /// - /// Checks whether the Contains() method of the observable dictionary is able to - /// determine if the dictionary contains an item - /// - [Test] - public void TestContains() { - Dictionary numbers = createTestDictionary(); - ObservableDictionary testDictionary = makeObservable(numbers); - - Assert.IsTrue( - testDictionary.Contains(new KeyValuePair(42, "forty-two")) - ); - Assert.IsFalse( - testDictionary.Contains(new KeyValuePair(24, "twenty-four")) - ); - } - - /// - /// Checks whether the Contains() method of the observable dictionary is able to - /// determine if the dictionary contains a key - /// - [Test] - public void TestContainsKey() { - Dictionary numbers = createTestDictionary(); - ObservableDictionary testDictionary = makeObservable(numbers); - - Assert.IsTrue(testDictionary.ContainsKey(42)); - Assert.IsFalse(testDictionary.ContainsKey(24)); - } - - /// - /// Verifies that the CopyTo() of the observable dictionary works - /// - [Test] - public void TestCopyToArray() { - Dictionary numbers = createTestDictionary(); - ObservableDictionary testDictionary = makeObservable(numbers); - - KeyValuePair[] items = new KeyValuePair[numbers.Count]; - - testDictionary.CopyTo(items, 0); - - CollectionAssert.AreEqual(numbers, items); - } - - /// - /// Tests whether the typesafe enumerator of the observable dictionary is working - /// - [Test] - public void TestTypesafeEnumerator() { - Dictionary numbers = createTestDictionary(); - ObservableDictionary testDictionary = makeObservable(numbers); - - List> outputItems = new List>(); - foreach(KeyValuePair item in testDictionary) { - outputItems.Add(item); - } - - CollectionAssert.AreEqual(numbers, outputItems); - } - - /// - /// Tests whether the keys collection of the observable dictionary can be queried - /// - [Test] - public void TestGetKeysCollection() { - Dictionary numbers = createTestDictionary(); - ObservableDictionary testDictionary = makeObservable(numbers); - - ICollection inputKeys = numbers.Keys; - ICollection keys = testDictionary.Keys; - CollectionAssert.AreEquivalent(inputKeys, keys); - } - - /// - /// Tests whether the values collection of the observable dictionary can be queried - /// - [Test] - public void TestGetValuesCollection() { - Dictionary numbers = createTestDictionary(); - ObservableDictionary testDictionary = makeObservable(numbers); - - ICollection inputValues = numbers.Values; - ICollection values = testDictionary.Values; - CollectionAssert.AreEquivalent(inputValues, values); - } - - /// - /// Tests whether the TryGetValue() method of the observable dictionary is working - /// - [Test] - public void TestTryGetValue() { - string value; - - Assert.IsTrue(this.observedDictionary.TryGetValue(42, out value)); - Assert.AreEqual("forty-two", value); - - Assert.IsFalse(this.observedDictionary.TryGetValue(24, out value)); - Assert.AreEqual(null, value); - } - - /// - /// Tests whether the retrieval of values using the indexer of the observable - /// dictionary is working - /// - [Test] - public void TestRetrieveValueByIndexer() { - Assert.AreEqual("forty-two", this.observedDictionary[42]); - } - - /// - /// Tests whether an exception is thrown if the indexer of the observable dictionary - /// is used to attempt to retrieve a non-existing value - /// - [Test] - public void TestRetrieveNonExistingValueByIndexer() { - Assert.Throws( - delegate() { Console.WriteLine(this.observedDictionary[24]); } - ); - } - - /// - /// Checks whether the Add() methods works via the generic - /// IDictionary<> interface - /// - [Test] - public void TestAddViaGenericIDictionary() { - this.mockedSubscriber.Expects.One.Method(m => m.ItemAdded(null, null)).WithAnyArguments(); - (this.observedDictionary as IDictionary).Add(10, "ten"); - this.mockery.VerifyAllExpectationsHaveBeenMet(); - - CollectionAssert.Contains( - this.observedDictionary, new KeyValuePair(10, "ten") - ); - } - - /// - /// Checks whether the Remove() method works via the generic - /// IDictionary<> interface - /// - [Test] - public void TestRemoveViaGenericIDictionary() { - this.mockedSubscriber.Expects.One.Method(m => m.ItemRemoved(null, null)).WithAnyArguments(); - (this.observedDictionary as IDictionary).Remove(3); - this.mockery.VerifyAllExpectationsHaveBeenMet(); - - CollectionAssert.DoesNotContain(this.observedDictionary.Keys, 3); - } - - /// - /// Tests whether the TryGetValue() method of the observable dictionary is working - /// - [Test] - public void TestRetrieveValueByIndexerViaGenericIDictionary() { - Assert.AreEqual( - "forty-two", (this.observedDictionary as IDictionary)[42] - ); - } - - /// - /// Verifies that the indexer can be used to insert an item via the generic - /// IDictionar<> interface - /// - [Test] - public void TestReplaceByIndexerViaGenericIDictionary() { - this.mockedSubscriber.Expects.One.Method( - m => m.ItemReplaced(null, null) - ).WithAnyArguments(); - - (this.observedDictionary as IDictionary)[42] = "two and fourty"; - this.mockery.VerifyAllExpectationsHaveBeenMet(); - - Assert.AreEqual("two and fourty", this.observedDictionary[42]); - } - - /// - /// Checks whether the Clear() method of observable dictionary is working - /// - [Test] - public void TestClearViaIDictionary() { - this.mockedSubscriber.Expects.One.Method( - m => m.Clearing(null, null) - ).WithAnyArguments(); - this.mockedSubscriber.Expects.One.Method( - m => m.Cleared(null, null) - ).WithAnyArguments(); - (this.observedDictionary as IDictionary).Clear(); - this.mockery.VerifyAllExpectationsHaveBeenMet(); - - Assert.AreEqual(0, this.observedDictionary.Count); - } - - /// - /// Checks whether the Add() method works via the IDictionary interface - /// - [Test] - public void TestAddViaIDictionary() { - this.mockedSubscriber.Expects.One.Method( - m => m.ItemAdded(null, null) - ).WithAnyArguments(); - (this.observedDictionary as IDictionary).Add(24, "twenty-four"); - this.mockery.VerifyAllExpectationsHaveBeenMet(); - - CollectionAssert.Contains( - this.observedDictionary, new KeyValuePair(24, "twenty-four") - ); - } - - /// - /// Checks whether the Contains() method of the observable dictionary is able to - /// determine if the dictionary contains an item via the IDictionary interface - /// - [Test] - public void TestContainsViaIDictionary() { - Assert.IsTrue((this.observedDictionary as IDictionary).Contains(42)); - Assert.IsFalse((this.observedDictionary as IDictionary).Contains(24)); - } - - /// - /// Checks whether the GetEnumerator() method of the observable dictionary - /// returns a working enumerator if accessed via the IDictionary interface - /// - [Test] - public void TestEnumeratorViaIDictionary() { - Dictionary outputNumbers = new Dictionary(); - foreach(DictionaryEntry entry in (this.observedDictionary as IDictionary)) { - (outputNumbers as IDictionary).Add(entry.Key, entry.Value); - } - - CollectionAssert.AreEquivalent(this.observedDictionary, outputNumbers); - } - - /// - /// Checks whether the IsFixedSize property of the observable dictionary returns - /// the expected result for a read only dictionary based on a dynamic dictionary - /// - [Test] - public void TestIsFixedSizeViaIList() { - Assert.IsFalse((this.observedDictionary as IDictionary).IsFixedSize); - } - - /// - /// Tests whether the keys collection of the observable dictionary can be queried - /// via the IDictionary interface - /// - [Test] - public void TestGetKeysCollectionViaIDictionary() { - ICollection keys = (this.observedDictionary as IDictionary).Keys; - Assert.AreEqual(this.observedDictionary.Count, keys.Count); - } - - /// - /// Tests whether the values collection of the observable dictionary can be queried - /// via the IDictionary interface - /// - [Test] - public void TestGetValuesCollectionViaIDictionary() { - ICollection values = (this.observedDictionary as IDictionary).Values; - Assert.AreEqual(this.observedDictionary.Count, values.Count); - } - - /// - /// Checks whether Remove() method works via the IDictionary interface - /// - [Test] - public void TestRemoveViaIDictionary() { - this.mockedSubscriber.Expects.One.Method(m => m.ItemRemoved(null, null)).WithAnyArguments(); - (this.observedDictionary as IDictionary).Remove(3); - this.mockery.VerifyAllExpectationsHaveBeenMet(); - - CollectionAssert.DoesNotContain(this.observedDictionary.Keys, 3); - } - - /// - /// Tests whether the retrieval of values using the indexer of the observable - /// dictionary is working via the IDictionary interface - /// - [Test] - public void TestRetrieveValueByIndexerViaIDictionary() { - Assert.AreEqual("forty-two", (this.observedDictionary as IDictionary)[42]); - } - - /// - /// Verifies the indexer can be used to insert an item via the IDictionary interface - /// - [Test] - public void TestReplaceByIndexerViaIDictionary() { - this.mockedSubscriber.Expects.One.Method( - m => m.ItemRemoved(null, null) - ).WithAnyArguments(); - this.mockedSubscriber.Expects.One.Method( - m => m.ItemAdded(null, null) - ).WithAnyArguments(); - - (this.observedDictionary as IDictionary)[42] = "two and fourty"; - this.mockery.VerifyAllExpectationsHaveBeenMet(); - - Assert.AreEqual("two and fourty", this.observedDictionary[42]); - } - - /// - /// Checks whether Add() method is working via the generic - /// ICollection<> interface - /// - [Test] - public void TestAddViaGenericICollection() { - this.mockedSubscriber.Expects.One.Method( - m => m.ItemAdded(null, null) - ).WithAnyArguments(); - - (this.observedDictionary as ICollection>).Add( - new KeyValuePair(24, "twenty-four") - ); - this.mockery.VerifyAllExpectationsHaveBeenMet(); - - CollectionAssert.Contains( - this.observedDictionary, new KeyValuePair(24, "twenty-four") - ); - } - - /// - /// Checks whether the Clear() method is working via the generic - /// ICollection<> interface - /// - [Test] - public void TestClearViaGenericICollection() { - this.mockedSubscriber.Expects.One.Method( - m => m.Clearing(null, null) - ).WithAnyArguments(); - this.mockedSubscriber.Expects.One.Method( - m => m.Cleared(null, null) - ).WithAnyArguments(); - - (this.observedDictionary as ICollection>).Clear(); - this.mockery.VerifyAllExpectationsHaveBeenMet(); - - Assert.AreEqual(0, this.observedDictionary.Count); - } - - /// - /// Checks whether the Remove() method is working via the - /// generic ICollection<> interface - /// - [Test] - public void TestRemoveViaGenericICollection() { - IEnumerator> enumerator = - (this.observedDictionary as ICollection>).GetEnumerator(); - enumerator.MoveNext(); - - this.mockedSubscriber.Expects.One.Method( - m => m.ItemRemoved(null, null) - ).WithAnyArguments(); - - (this.observedDictionary as ICollection>).Remove( - enumerator.Current - ); - this.mockery.VerifyAllExpectationsHaveBeenMet(); - - CollectionAssert.DoesNotContain(this.observedDictionary, enumerator.Current); - } - - /// - /// Verifies that the CopyTo() of the observable dictionary works when called - /// via the the ICollection interface - /// - [Test] - public void TestCopyToArrayViaICollection() { - Dictionary numbers = createTestDictionary(); - ObservableDictionary testDictionary = makeObservable(numbers); - - DictionaryEntry[] entries = new DictionaryEntry[numbers.Count]; - (testDictionary as ICollection).CopyTo(entries, 0); - - KeyValuePair[] items = new KeyValuePair[numbers.Count]; - for(int index = 0; index < entries.Length; ++index) { - items[index] = new KeyValuePair( - (int)entries[index].Key, (string)entries[index].Value - ); - } - CollectionAssert.AreEquivalent(numbers, items); - } - - /// - /// Verifies that the IsSynchronized property and the SyncRoot property are working - /// - [Test] - public void TestSynchronization() { - Dictionary numbers = createTestDictionary(); - ObservableDictionary testDictionary = makeObservable(numbers); - - if(!(testDictionary as ICollection).IsSynchronized) { - lock((testDictionary as ICollection).SyncRoot) { - Assert.AreEqual(numbers.Count, testDictionary.Count); - } - } - } - - /// - /// Test whether the observable dictionary can be serialized - /// - [Test] - public void TestSerialization() { - BinaryFormatter formatter = new BinaryFormatter(); - - using(MemoryStream memory = new MemoryStream()) { - Dictionary numbers = createTestDictionary(); - ObservableDictionary testDictionary1 = makeObservable(numbers); - - formatter.Serialize(memory, testDictionary1); - memory.Position = 0; - object testDictionary2 = formatter.Deserialize(memory); - - CollectionAssert.AreEquivalent(testDictionary1, (IEnumerable)testDictionary2); - } - } - - /// - /// Creates a new observable dictionary filled with some values for testing - /// - /// The newly created observable dictionary - private static Dictionary createTestDictionary() { - Dictionary numbers = new Dictionary(); - numbers.Add(1, "one"); - numbers.Add(2, "two"); - numbers.Add(3, "three"); - numbers.Add(42, "forty-two"); - return new Dictionary(numbers); - } - - /// - /// Creates a new observable dictionary filled with some values for testing - /// - /// The newly created observable dictionary - private static ObservableDictionary makeObservable( - IDictionary dictionary - ) { - return new ObservableDictionary(dictionary); - } - - /// Mock object factory - private MockFactory mockery; - /// The mocked observable collection subscriber - private Mock mockedSubscriber; - /// An observable dictionary to which a mock will be subscribed - private ObservableDictionary observedDictionary; - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST - -#endif // !NO_NMOCK +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if !NO_NMOCK + +#if UNITTEST + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; + +using NUnit.Framework; +using NMock; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the observable dictionary wrapper + [TestFixture] + internal class ObservableDictionaryTest { + + #region interface IObservableDictionarySubscriber + + /// Interface used to test the observable dictionary + public interface IObservableDictionarySubscriber { + + /// Called when the dictionary is about to clear its contents + /// Dictionary that is clearing its contents + /// Not used + void Clearing(object sender, EventArgs arguments); + + /// Called when the dictionary has been clear of its contents + /// Dictionary that was cleared of its contents + /// Not used + void Cleared(object sender, EventArgs arguments); + + /// Called when an item is added to the dictionary + /// Dictionary to which an item is being added + /// Contains the item that is being added + void ItemAdded(object sender, ItemEventArgs> arguments); + + /// Called when an item is removed from the dictionary + /// Dictionary from which an item is being removed + /// Contains the item that is being removed + void ItemRemoved(object sender, ItemEventArgs> arguments); + + /// Called when an item is replaced in the dictionary + /// Dictionary in which an item is being replaced + /// Contains the replaced item and its replacement + void ItemReplaced( + object sender, ItemReplaceEventArgs> arguments + ); + + } + + #endregion // interface IObservableDictionarySubscriber + + /// Initialization routine executed before each test is run + [SetUp] + public void Setup() { + this.mockery = new MockFactory(); + + this.mockedSubscriber = this.mockery.CreateMock(); + + this.observedDictionary = new ObservableDictionary(); + this.observedDictionary.Add(1, "one"); + this.observedDictionary.Add(2, "two"); + this.observedDictionary.Add(3, "three"); + this.observedDictionary.Add(42, "forty-two"); + + this.observedDictionary.Clearing += + new EventHandler(this.mockedSubscriber.MockObject.Clearing); + this.observedDictionary.Cleared += + new EventHandler(this.mockedSubscriber.MockObject.Cleared); + this.observedDictionary.ItemAdded += + new EventHandler>>( + this.mockedSubscriber.MockObject.ItemAdded + ); + this.observedDictionary.ItemRemoved += + new EventHandler>>( + this.mockedSubscriber.MockObject.ItemRemoved + ); + this.observedDictionary.ItemReplaced += + new EventHandler>>( + this.mockedSubscriber.MockObject.ItemReplaced + ); + } + + /// + /// Verifies that the default constructor of the observable dictionary works + /// + [Test] + public void TestDefaultConstructor() { + ObservableDictionary testDictionary = + new ObservableDictionary(); + + Assert.AreEqual(0, testDictionary.Count); + } + + /// + /// Verifies that the copy constructor of the observable dictionary works + /// + [Test] + public void TestCopyConstructor() { + Dictionary numbers = createTestDictionary(); + ObservableDictionary testDictionary = makeObservable(numbers); + + CollectionAssert.AreEqual(numbers, testDictionary); + } + + /// Verifies that the IsReadOnly property is working + [Test] + public void TestIsReadOnly() { + Dictionary numbers = createTestDictionary(); + ObservableDictionary testDictionary = makeObservable(numbers); + + Assert.IsFalse(testDictionary.IsReadOnly); + } + + /// + /// Checks whether the Contains() method of the observable dictionary is able to + /// determine if the dictionary contains an item + /// + [Test] + public void TestContains() { + Dictionary numbers = createTestDictionary(); + ObservableDictionary testDictionary = makeObservable(numbers); + + Assert.IsTrue( + testDictionary.Contains(new KeyValuePair(42, "forty-two")) + ); + Assert.IsFalse( + testDictionary.Contains(new KeyValuePair(24, "twenty-four")) + ); + } + + /// + /// Checks whether the Contains() method of the observable dictionary is able to + /// determine if the dictionary contains a key + /// + [Test] + public void TestContainsKey() { + Dictionary numbers = createTestDictionary(); + ObservableDictionary testDictionary = makeObservable(numbers); + + Assert.IsTrue(testDictionary.ContainsKey(42)); + Assert.IsFalse(testDictionary.ContainsKey(24)); + } + + /// + /// Verifies that the CopyTo() of the observable dictionary works + /// + [Test] + public void TestCopyToArray() { + Dictionary numbers = createTestDictionary(); + ObservableDictionary testDictionary = makeObservable(numbers); + + KeyValuePair[] items = new KeyValuePair[numbers.Count]; + + testDictionary.CopyTo(items, 0); + + CollectionAssert.AreEqual(numbers, items); + } + + /// + /// Tests whether the typesafe enumerator of the observable dictionary is working + /// + [Test] + public void TestTypesafeEnumerator() { + Dictionary numbers = createTestDictionary(); + ObservableDictionary testDictionary = makeObservable(numbers); + + List> outputItems = new List>(); + foreach(KeyValuePair item in testDictionary) { + outputItems.Add(item); + } + + CollectionAssert.AreEqual(numbers, outputItems); + } + + /// + /// Tests whether the keys collection of the observable dictionary can be queried + /// + [Test] + public void TestGetKeysCollection() { + Dictionary numbers = createTestDictionary(); + ObservableDictionary testDictionary = makeObservable(numbers); + + ICollection inputKeys = numbers.Keys; + ICollection keys = testDictionary.Keys; + CollectionAssert.AreEquivalent(inputKeys, keys); + } + + /// + /// Tests whether the values collection of the observable dictionary can be queried + /// + [Test] + public void TestGetValuesCollection() { + Dictionary numbers = createTestDictionary(); + ObservableDictionary testDictionary = makeObservable(numbers); + + ICollection inputValues = numbers.Values; + ICollection values = testDictionary.Values; + CollectionAssert.AreEquivalent(inputValues, values); + } + + /// + /// Tests whether the TryGetValue() method of the observable dictionary is working + /// + [Test] + public void TestTryGetValue() { + string value; + + Assert.IsTrue(this.observedDictionary.TryGetValue(42, out value)); + Assert.AreEqual("forty-two", value); + + Assert.IsFalse(this.observedDictionary.TryGetValue(24, out value)); + Assert.AreEqual(null, value); + } + + /// + /// Tests whether the retrieval of values using the indexer of the observable + /// dictionary is working + /// + [Test] + public void TestRetrieveValueByIndexer() { + Assert.AreEqual("forty-two", this.observedDictionary[42]); + } + + /// + /// Tests whether an exception is thrown if the indexer of the observable dictionary + /// is used to attempt to retrieve a non-existing value + /// + [Test] + public void TestRetrieveNonExistingValueByIndexer() { + Assert.Throws( + delegate() { Console.WriteLine(this.observedDictionary[24]); } + ); + } + + /// + /// Checks whether the Add() methods works via the generic + /// IDictionary<> interface + /// + [Test] + public void TestAddViaGenericIDictionary() { + this.mockedSubscriber.Expects.One.Method(m => m.ItemAdded(null, null)).WithAnyArguments(); + (this.observedDictionary as IDictionary).Add(10, "ten"); + this.mockery.VerifyAllExpectationsHaveBeenMet(); + + CollectionAssert.Contains( + this.observedDictionary, new KeyValuePair(10, "ten") + ); + } + + /// + /// Checks whether the Remove() method works via the generic + /// IDictionary<> interface + /// + [Test] + public void TestRemoveViaGenericIDictionary() { + this.mockedSubscriber.Expects.One.Method(m => m.ItemRemoved(null, null)).WithAnyArguments(); + (this.observedDictionary as IDictionary).Remove(3); + this.mockery.VerifyAllExpectationsHaveBeenMet(); + + CollectionAssert.DoesNotContain(this.observedDictionary.Keys, 3); + } + + /// + /// Tests whether the TryGetValue() method of the observable dictionary is working + /// + [Test] + public void TestRetrieveValueByIndexerViaGenericIDictionary() { + Assert.AreEqual( + "forty-two", (this.observedDictionary as IDictionary)[42] + ); + } + + /// + /// Verifies that the indexer can be used to insert an item via the generic + /// IDictionar<> interface + /// + [Test] + public void TestReplaceByIndexerViaGenericIDictionary() { + this.mockedSubscriber.Expects.One.Method( + m => m.ItemReplaced(null, null) + ).WithAnyArguments(); + + (this.observedDictionary as IDictionary)[42] = "two and fourty"; + this.mockery.VerifyAllExpectationsHaveBeenMet(); + + Assert.AreEqual("two and fourty", this.observedDictionary[42]); + } + + /// + /// Checks whether the Clear() method of observable dictionary is working + /// + [Test] + public void TestClearViaIDictionary() { + this.mockedSubscriber.Expects.One.Method( + m => m.Clearing(null, null) + ).WithAnyArguments(); + this.mockedSubscriber.Expects.One.Method( + m => m.Cleared(null, null) + ).WithAnyArguments(); + (this.observedDictionary as IDictionary).Clear(); + this.mockery.VerifyAllExpectationsHaveBeenMet(); + + Assert.AreEqual(0, this.observedDictionary.Count); + } + + /// + /// Checks whether the Add() method works via the IDictionary interface + /// + [Test] + public void TestAddViaIDictionary() { + this.mockedSubscriber.Expects.One.Method( + m => m.ItemAdded(null, null) + ).WithAnyArguments(); + (this.observedDictionary as IDictionary).Add(24, "twenty-four"); + this.mockery.VerifyAllExpectationsHaveBeenMet(); + + CollectionAssert.Contains( + this.observedDictionary, new KeyValuePair(24, "twenty-four") + ); + } + + /// + /// Checks whether the Contains() method of the observable dictionary is able to + /// determine if the dictionary contains an item via the IDictionary interface + /// + [Test] + public void TestContainsViaIDictionary() { + Assert.IsTrue((this.observedDictionary as IDictionary).Contains(42)); + Assert.IsFalse((this.observedDictionary as IDictionary).Contains(24)); + } + + /// + /// Checks whether the GetEnumerator() method of the observable dictionary + /// returns a working enumerator if accessed via the IDictionary interface + /// + [Test] + public void TestEnumeratorViaIDictionary() { + Dictionary outputNumbers = new Dictionary(); + foreach(DictionaryEntry entry in (this.observedDictionary as IDictionary)) { + (outputNumbers as IDictionary).Add(entry.Key, entry.Value); + } + + CollectionAssert.AreEquivalent(this.observedDictionary, outputNumbers); + } + + /// + /// Checks whether the IsFixedSize property of the observable dictionary returns + /// the expected result for a read only dictionary based on a dynamic dictionary + /// + [Test] + public void TestIsFixedSizeViaIList() { + Assert.IsFalse((this.observedDictionary as IDictionary).IsFixedSize); + } + + /// + /// Tests whether the keys collection of the observable dictionary can be queried + /// via the IDictionary interface + /// + [Test] + public void TestGetKeysCollectionViaIDictionary() { + ICollection keys = (this.observedDictionary as IDictionary).Keys; + Assert.AreEqual(this.observedDictionary.Count, keys.Count); + } + + /// + /// Tests whether the values collection of the observable dictionary can be queried + /// via the IDictionary interface + /// + [Test] + public void TestGetValuesCollectionViaIDictionary() { + ICollection values = (this.observedDictionary as IDictionary).Values; + Assert.AreEqual(this.observedDictionary.Count, values.Count); + } + + /// + /// Checks whether Remove() method works via the IDictionary interface + /// + [Test] + public void TestRemoveViaIDictionary() { + this.mockedSubscriber.Expects.One.Method(m => m.ItemRemoved(null, null)).WithAnyArguments(); + (this.observedDictionary as IDictionary).Remove(3); + this.mockery.VerifyAllExpectationsHaveBeenMet(); + + CollectionAssert.DoesNotContain(this.observedDictionary.Keys, 3); + } + + /// + /// Tests whether the retrieval of values using the indexer of the observable + /// dictionary is working via the IDictionary interface + /// + [Test] + public void TestRetrieveValueByIndexerViaIDictionary() { + Assert.AreEqual("forty-two", (this.observedDictionary as IDictionary)[42]); + } + + /// + /// Verifies the indexer can be used to insert an item via the IDictionary interface + /// + [Test] + public void TestReplaceByIndexerViaIDictionary() { + this.mockedSubscriber.Expects.One.Method( + m => m.ItemRemoved(null, null) + ).WithAnyArguments(); + this.mockedSubscriber.Expects.One.Method( + m => m.ItemAdded(null, null) + ).WithAnyArguments(); + + (this.observedDictionary as IDictionary)[42] = "two and fourty"; + this.mockery.VerifyAllExpectationsHaveBeenMet(); + + Assert.AreEqual("two and fourty", this.observedDictionary[42]); + } + + /// + /// Checks whether Add() method is working via the generic + /// ICollection<> interface + /// + [Test] + public void TestAddViaGenericICollection() { + this.mockedSubscriber.Expects.One.Method( + m => m.ItemAdded(null, null) + ).WithAnyArguments(); + + (this.observedDictionary as ICollection>).Add( + new KeyValuePair(24, "twenty-four") + ); + this.mockery.VerifyAllExpectationsHaveBeenMet(); + + CollectionAssert.Contains( + this.observedDictionary, new KeyValuePair(24, "twenty-four") + ); + } + + /// + /// Checks whether the Clear() method is working via the generic + /// ICollection<> interface + /// + [Test] + public void TestClearViaGenericICollection() { + this.mockedSubscriber.Expects.One.Method( + m => m.Clearing(null, null) + ).WithAnyArguments(); + this.mockedSubscriber.Expects.One.Method( + m => m.Cleared(null, null) + ).WithAnyArguments(); + + (this.observedDictionary as ICollection>).Clear(); + this.mockery.VerifyAllExpectationsHaveBeenMet(); + + Assert.AreEqual(0, this.observedDictionary.Count); + } + + /// + /// Checks whether the Remove() method is working via the + /// generic ICollection<> interface + /// + [Test] + public void TestRemoveViaGenericICollection() { + IEnumerator> enumerator = + (this.observedDictionary as ICollection>).GetEnumerator(); + enumerator.MoveNext(); + + this.mockedSubscriber.Expects.One.Method( + m => m.ItemRemoved(null, null) + ).WithAnyArguments(); + + (this.observedDictionary as ICollection>).Remove( + enumerator.Current + ); + this.mockery.VerifyAllExpectationsHaveBeenMet(); + + CollectionAssert.DoesNotContain(this.observedDictionary, enumerator.Current); + } + + /// + /// Verifies that the CopyTo() of the observable dictionary works when called + /// via the the ICollection interface + /// + [Test] + public void TestCopyToArrayViaICollection() { + Dictionary numbers = createTestDictionary(); + ObservableDictionary testDictionary = makeObservable(numbers); + + DictionaryEntry[] entries = new DictionaryEntry[numbers.Count]; + (testDictionary as ICollection).CopyTo(entries, 0); + + KeyValuePair[] items = new KeyValuePair[numbers.Count]; + for(int index = 0; index < entries.Length; ++index) { + items[index] = new KeyValuePair( + (int)entries[index].Key, (string)entries[index].Value + ); + } + CollectionAssert.AreEquivalent(numbers, items); + } + + /// + /// Verifies that the IsSynchronized property and the SyncRoot property are working + /// + [Test] + public void TestSynchronization() { + Dictionary numbers = createTestDictionary(); + ObservableDictionary testDictionary = makeObservable(numbers); + + if(!(testDictionary as ICollection).IsSynchronized) { + lock((testDictionary as ICollection).SyncRoot) { + Assert.AreEqual(numbers.Count, testDictionary.Count); + } + } + } + + /// + /// Test whether the observable dictionary can be serialized + /// + [Test] + public void TestSerialization() { + BinaryFormatter formatter = new BinaryFormatter(); + + using(MemoryStream memory = new MemoryStream()) { + Dictionary numbers = createTestDictionary(); + ObservableDictionary testDictionary1 = makeObservable(numbers); + + formatter.Serialize(memory, testDictionary1); + memory.Position = 0; + object testDictionary2 = formatter.Deserialize(memory); + + CollectionAssert.AreEquivalent(testDictionary1, (IEnumerable)testDictionary2); + } + } + + /// + /// Creates a new observable dictionary filled with some values for testing + /// + /// The newly created observable dictionary + private static Dictionary createTestDictionary() { + Dictionary numbers = new Dictionary(); + numbers.Add(1, "one"); + numbers.Add(2, "two"); + numbers.Add(3, "three"); + numbers.Add(42, "forty-two"); + return new Dictionary(numbers); + } + + /// + /// Creates a new observable dictionary filled with some values for testing + /// + /// The newly created observable dictionary + private static ObservableDictionary makeObservable( + IDictionary dictionary + ) { + return new ObservableDictionary(dictionary); + } + + /// Mock object factory + private MockFactory mockery; + /// The mocked observable collection subscriber + private Mock mockedSubscriber; + /// An observable dictionary to which a mock will be subscribed + private ObservableDictionary observedDictionary; + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST + +#endif // !NO_NMOCK diff --git a/Source/Collections/ObservableDictionary.cs b/Source/Collections/ObservableDictionary.cs index ff881f9..764095c 100644 --- a/Source/Collections/ObservableDictionary.cs +++ b/Source/Collections/ObservableDictionary.cs @@ -1,489 +1,488 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections; -using System.Collections.Generic; -#if !NO_SPECIALIZED_COLLECTIONS -using System.Collections.Specialized; -#endif -using System.Runtime.Serialization; - -namespace Nuclex.Support.Collections { - - /// A dictionary that sneds out change notifications - /// Type of the keys used in the dictionary - /// Type of the values used in the dictionary -#if !NO_SERIALIZATION - [Serializable] -#endif - public class ObservableDictionary : -#if !NO_SERIALIZATION - ISerializable, - IDeserializationCallback, -#endif - IDictionary, - IDictionary, -#if !NO_SPECIALIZED_COLLECTIONS - INotifyCollectionChanged, -#endif - IObservableCollection> { - -#if !NO_SERIALIZATION - #region class SerializedDictionary - - /// - /// Dictionary wrapped used to reconstruct a serialized read only dictionary - /// - private class SerializedDictionary : Dictionary { - - /// - /// Initializes a new instance of the System.WeakReference class, using deserialized - /// data from the specified serialization and stream objects. - /// - /// - /// An object that holds all the data needed to serialize or deserialize the - /// current System.WeakReference object. - /// - /// - /// (Reserved) Describes the source and destination of the serialized stream - /// specified by info. - /// - /// - /// The info parameter is null. - /// - public SerializedDictionary(SerializationInfo info, StreamingContext context) : - base(info, context) { } - - } - - #endregion // class SerializedDictionary -#endif // !NO_SERIALIZATION - - /// Raised when an item has been added to the dictionary - public event EventHandler>> ItemAdded; - /// Raised when an item is removed from the dictionary - public event EventHandler>> ItemRemoved; - /// Raised when an item is replaced in the collection - public event EventHandler>> ItemReplaced; - /// Raised when the dictionary is about to be cleared - public event EventHandler Clearing; - /// Raised when the dictionary has been cleared - public event EventHandler Cleared; - -#if !NO_SPECIALIZED_COLLECTIONS - /// Called when the collection has changed - public event NotifyCollectionChangedEventHandler CollectionChanged; -#endif - - /// Initializes a new observable dictionary - public ObservableDictionary() : this(new Dictionary()) { } - - /// Initializes a new observable Dictionary wrapper - /// Dictionary that will be wrapped - public ObservableDictionary(IDictionary dictionary) { - this.typedDictionary = dictionary; - this.objectDictionary = (this.typedDictionary as IDictionary); - } - -#if !NO_SERIALIZATION - - /// - /// Initializes a new instance of the System.WeakReference class, using deserialized - /// data from the specified serialization and stream objects. - /// - /// - /// An object that holds all the data needed to serialize or deserialize the - /// current System.WeakReference object. - /// - /// - /// (Reserved) Describes the source and destination of the serialized stream - /// specified by info. - /// - /// - /// The info parameter is null. - /// - protected ObservableDictionary(SerializationInfo info, StreamingContext context) : - this(new SerializedDictionary(info, context)) { } - -#endif // !NO_SERIALIZATION - - /// Whether the directory is write-protected - public bool IsReadOnly { - get { return this.typedDictionary.IsReadOnly; } - } - - /// - /// Determines whether the specified KeyValuePair is contained in the Dictionary - /// - /// KeyValuePair that will be checked for - /// True if the provided KeyValuePair was contained in the Dictionary - public bool Contains(KeyValuePair item) { - return this.typedDictionary.Contains(item); - } - - /// Determines whether the Dictionary contains the specified key - /// Key that will be checked for - /// - /// True if an entry with the specified key was contained in the Dictionary - /// - public bool ContainsKey(TKey key) { - return this.typedDictionary.ContainsKey(key); - } - - /// Copies the contents of the Dictionary into an array - /// Array the Dictionary will be copied into - /// - /// Starting index at which to begin filling the destination array - /// - public void CopyTo(KeyValuePair[] array, int arrayIndex) { - this.typedDictionary.CopyTo(array, arrayIndex); - } - - /// Number of elements contained in the Dictionary - public int Count { - get { return this.typedDictionary.Count; } - } - - /// Creates a new enumerator for the Dictionary - /// The new Dictionary enumerator - public IEnumerator> GetEnumerator() { - return this.typedDictionary.GetEnumerator(); - } - - /// Collection of all keys contained in the Dictionary - public ICollection Keys { - get { return this.typedDictionary.Keys; } - } - - /// Collection of all values contained in the Dictionary - public ICollection Values { - get { return this.typedDictionary.Values; } - } - - /// - /// Attempts to retrieve the item with the specified key from the Dictionary - /// - /// Key of the item to attempt to retrieve - /// - /// Output parameter that will receive the key upon successful completion - /// - /// - /// True if the item was found and has been placed in the output parameter - /// - public bool TryGetValue(TKey key, out TValue value) { - return this.typedDictionary.TryGetValue(key, out value); - } - - /// Accesses an item in the Dictionary by its key - /// Key of the item that will be accessed - public TValue this[TKey key] { - get { return this.typedDictionary[key]; } - set { - bool removed; - TValue oldValue; - removed = this.typedDictionary.TryGetValue(key, out oldValue); - - this.typedDictionary[key] = value; - - if(removed) { - OnReplaced( - new KeyValuePair(key, oldValue), - new KeyValuePair(key, value) - ); - } else { - OnAdded(new KeyValuePair(key, value)); - } - } - } - - /// Inserts an item into the Dictionary - /// Key under which to add the new item - /// Item that will be added to the Dictionary - public void Add(TKey key, TValue value) { - this.typedDictionary.Add(key, value); - OnAdded(new KeyValuePair(key, value)); - } - - /// Removes the item with the specified key from the Dictionary - /// Key of the elementes that will be removed - /// True if an item with the specified key was found and removed - public bool Remove(TKey key) { - TValue oldValue; - this.typedDictionary.TryGetValue(key, out oldValue); - - bool removed = this.typedDictionary.Remove(key); - if(removed) { - OnRemoved(new KeyValuePair(key, oldValue)); - } - return removed; - } - - /// Removes all items from the Dictionary - public void Clear() { - OnClearing(); - this.typedDictionary.Clear(); - OnCleared(); - } - - /// Fires the 'ItemAdded' event - /// Item that has been added to the collection - protected virtual void OnAdded(KeyValuePair item) { - if(ItemAdded != null) - ItemAdded(this, new ItemEventArgs>(item)); - -#if !NO_SPECIALIZED_COLLECTIONS - if(CollectionChanged != null) { - CollectionChanged( - this, - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item) - ); - } -#endif - } - - /// Fires the 'ItemRemoved' event - /// Item that has been removed from the collection - protected virtual void OnRemoved(KeyValuePair item) { - if(ItemRemoved != null) { - ItemRemoved(this, new ItemEventArgs>(item)); - } -#if !NO_SPECIALIZED_COLLECTIONS - if(CollectionChanged != null) { - CollectionChanged( - this, - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item) - ); - } -#endif - } - - /// Fires the 'ItemReplaced' event - /// Item that has been replaced in the collection - /// Item with which the original item was replaced - protected virtual void OnReplaced( - KeyValuePair oldItem, KeyValuePair newItem - ) { - if(ItemReplaced != null) { - ItemReplaced( - this, - new ItemReplaceEventArgs>(oldItem, newItem) - ); - } -#if !NO_SPECIALIZED_COLLECTIONS - if(CollectionChanged != null) { - CollectionChanged( - this, new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Replace, newItem, oldItem - ) - ); - } -#endif - } - - /// Fires the 'Clearing' event - protected virtual void OnClearing() { - if(Clearing != null) { - Clearing(this, EventArgs.Empty); - } - } - - /// Fires the 'Cleared' event - protected virtual void OnCleared() { - if(Cleared != null) { - Cleared(this, EventArgs.Empty); - } -#if !NO_SPECIALIZED_COLLECTIONS - if(CollectionChanged != null) { - CollectionChanged(this, Constants.NotifyCollectionResetEventArgs); - } -#endif - } - - #region IEnumerable implementation - - /// Returns a new object enumerator for the Dictionary - /// The new object enumerator - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { - return (this.typedDictionary as IEnumerable).GetEnumerator(); - } - - #endregion - - #region IDictionary implementation - - /// Adds an item into the Dictionary - /// Key under which the item will be added - /// Item that will be added - void IDictionary.Add(object key, object value) { - this.objectDictionary.Add(key, value); - OnAdded(new KeyValuePair((TKey)key, (TValue)value)); - } - - /// Determines whether the specified key exists in the Dictionary - /// Key that will be checked for - /// True if an item with the specified key exists in the Dictionary - bool IDictionary.Contains(object key) { - return this.objectDictionary.Contains(key); - } - - /// Whether the size of the Dictionary is fixed - bool IDictionary.IsFixedSize { - get { return this.objectDictionary.IsFixedSize; } - } - - /// Returns a collection of all keys in the Dictionary - ICollection IDictionary.Keys { - get { return this.objectDictionary.Keys; } - } - - /// Returns a collection of all values stored in the Dictionary - ICollection IDictionary.Values { - get { return this.objectDictionary.Values; } - } - - /// Removes an item from the Dictionary - /// Key of the item that will be removed - void IDictionary.Remove(object key) { - TValue value; - bool removed = this.typedDictionary.TryGetValue((TKey)key, out value); - this.objectDictionary.Remove(key); - if(removed) { - OnRemoved(new KeyValuePair((TKey)key, (TValue)value)); - } - } - - /// Accesses an item in the Dictionary by its key - /// Key of the item that will be accessed - /// The item with the specified key - object IDictionary.this[object key] { - get { return this.objectDictionary[key]; } - set { - bool removed; - TValue oldValue; - removed = this.typedDictionary.TryGetValue((TKey)key, out oldValue); - - this.objectDictionary[key] = value; - - if(removed) { - OnRemoved(new KeyValuePair((TKey)key, oldValue)); - } - OnAdded(new KeyValuePair((TKey)key, (TValue)value)); - } - } - - #endregion // IDictionary implementation - - #region IDictionaryEnumerator implementation - - /// Returns a new entry enumerator for the dictionary - /// The new entry enumerator - IDictionaryEnumerator IDictionary.GetEnumerator() { - return this.objectDictionary.GetEnumerator(); - } - - #endregion // IDictionaryEnumerator implementation - - #region ICollection<> implementation - - /// Inserts an already prepared element into the Dictionary - /// Prepared element that will be added to the Dictionary - void ICollection>.Add( - KeyValuePair item - ) { - this.typedDictionary.Add(item); - OnAdded(item); - } - - /// Removes all items from the Dictionary - void ICollection>.Clear() { - OnClearing(); - this.typedDictionary.Clear(); - OnCleared(); - } - - /// Removes all items from the Dictionary - /// Item that will be removed from the Dictionary - bool ICollection>.Remove( - KeyValuePair itemToRemove - ) { - bool removed = this.typedDictionary.Remove(itemToRemove); - if(removed) { - OnRemoved(itemToRemove); - } - return removed; - } - - #endregion - - #region ICollection implementation - - /// Copies the contents of the Dictionary into an array - /// Array the Dictionary contents will be copied into - /// - /// Starting index at which to begin filling the destination array - /// - void ICollection.CopyTo(Array array, int index) { - this.objectDictionary.CopyTo(array, index); - } - - /// Whether the Dictionary is synchronized for multi-threaded usage - bool ICollection.IsSynchronized { - get { return this.objectDictionary.IsSynchronized; } - } - - /// Synchronization root on which the Dictionary locks - object ICollection.SyncRoot { - get { return this.objectDictionary.SyncRoot; } - } - - #endregion - -#if !NO_SERIALIZATION - #region ISerializable implementation - - /// Serializes the Dictionary - /// - /// Provides the container into which the Dictionary will serialize itself - /// - /// - /// Contextual informations about the serialization environment - /// - void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) { - (this.typedDictionary as ISerializable).GetObjectData(info, context); - } - - /// Called after all objects have been successfully deserialized - /// Nicht unterstützt - void IDeserializationCallback.OnDeserialization(object sender) { - (this.typedDictionary as IDeserializationCallback).OnDeserialization(sender); - } - - #endregion -#endif //!NO_SERIALIZATION - - /// The wrapped Dictionary under its type-safe interface - private IDictionary typedDictionary; - /// The wrapped Dictionary under its object interface - private IDictionary objectDictionary; - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections; +using System.Collections.Generic; +#if !NO_SPECIALIZED_COLLECTIONS +using System.Collections.Specialized; +#endif +using System.Runtime.Serialization; + +namespace Nuclex.Support.Collections { + + /// A dictionary that sneds out change notifications + /// Type of the keys used in the dictionary + /// Type of the values used in the dictionary +#if !NO_SERIALIZATION + [Serializable] +#endif + public class ObservableDictionary : +#if !NO_SERIALIZATION + ISerializable, + IDeserializationCallback, +#endif + IDictionary, + IDictionary, +#if !NO_SPECIALIZED_COLLECTIONS + INotifyCollectionChanged, +#endif + IObservableCollection> { + +#if !NO_SERIALIZATION + #region class SerializedDictionary + + /// + /// Dictionary wrapped used to reconstruct a serialized read only dictionary + /// + private class SerializedDictionary : Dictionary { + + /// + /// Initializes a new instance of the System.WeakReference class, using deserialized + /// data from the specified serialization and stream objects. + /// + /// + /// An object that holds all the data needed to serialize or deserialize the + /// current System.WeakReference object. + /// + /// + /// (Reserved) Describes the source and destination of the serialized stream + /// specified by info. + /// + /// + /// The info parameter is null. + /// + public SerializedDictionary(SerializationInfo info, StreamingContext context) : + base(info, context) { } + + } + + #endregion // class SerializedDictionary +#endif // !NO_SERIALIZATION + + /// Raised when an item has been added to the dictionary + public event EventHandler>> ItemAdded; + /// Raised when an item is removed from the dictionary + public event EventHandler>> ItemRemoved; + /// Raised when an item is replaced in the collection + public event EventHandler>> ItemReplaced; + /// Raised when the dictionary is about to be cleared + public event EventHandler Clearing; + /// Raised when the dictionary has been cleared + public event EventHandler Cleared; + +#if !NO_SPECIALIZED_COLLECTIONS + /// Called when the collection has changed + public event NotifyCollectionChangedEventHandler CollectionChanged; +#endif + + /// Initializes a new observable dictionary + public ObservableDictionary() : this(new Dictionary()) { } + + /// Initializes a new observable Dictionary wrapper + /// Dictionary that will be wrapped + public ObservableDictionary(IDictionary dictionary) { + this.typedDictionary = dictionary; + this.objectDictionary = (this.typedDictionary as IDictionary); + } + +#if !NO_SERIALIZATION + + /// + /// Initializes a new instance of the System.WeakReference class, using deserialized + /// data from the specified serialization and stream objects. + /// + /// + /// An object that holds all the data needed to serialize or deserialize the + /// current System.WeakReference object. + /// + /// + /// (Reserved) Describes the source and destination of the serialized stream + /// specified by info. + /// + /// + /// The info parameter is null. + /// + protected ObservableDictionary(SerializationInfo info, StreamingContext context) : + this(new SerializedDictionary(info, context)) { } + +#endif // !NO_SERIALIZATION + + /// Whether the directory is write-protected + public bool IsReadOnly { + get { return this.typedDictionary.IsReadOnly; } + } + + /// + /// Determines whether the specified KeyValuePair is contained in the Dictionary + /// + /// KeyValuePair that will be checked for + /// True if the provided KeyValuePair was contained in the Dictionary + public bool Contains(KeyValuePair item) { + return this.typedDictionary.Contains(item); + } + + /// Determines whether the Dictionary contains the specified key + /// Key that will be checked for + /// + /// True if an entry with the specified key was contained in the Dictionary + /// + public bool ContainsKey(TKey key) { + return this.typedDictionary.ContainsKey(key); + } + + /// Copies the contents of the Dictionary into an array + /// Array the Dictionary will be copied into + /// + /// Starting index at which to begin filling the destination array + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) { + this.typedDictionary.CopyTo(array, arrayIndex); + } + + /// Number of elements contained in the Dictionary + public int Count { + get { return this.typedDictionary.Count; } + } + + /// Creates a new enumerator for the Dictionary + /// The new Dictionary enumerator + public IEnumerator> GetEnumerator() { + return this.typedDictionary.GetEnumerator(); + } + + /// Collection of all keys contained in the Dictionary + public ICollection Keys { + get { return this.typedDictionary.Keys; } + } + + /// Collection of all values contained in the Dictionary + public ICollection Values { + get { return this.typedDictionary.Values; } + } + + /// + /// Attempts to retrieve the item with the specified key from the Dictionary + /// + /// Key of the item to attempt to retrieve + /// + /// Output parameter that will receive the key upon successful completion + /// + /// + /// True if the item was found and has been placed in the output parameter + /// + public bool TryGetValue(TKey key, out TValue value) { + return this.typedDictionary.TryGetValue(key, out value); + } + + /// Accesses an item in the Dictionary by its key + /// Key of the item that will be accessed + public TValue this[TKey key] { + get { return this.typedDictionary[key]; } + set { + bool removed; + TValue oldValue; + removed = this.typedDictionary.TryGetValue(key, out oldValue); + + this.typedDictionary[key] = value; + + if(removed) { + OnReplaced( + new KeyValuePair(key, oldValue), + new KeyValuePair(key, value) + ); + } else { + OnAdded(new KeyValuePair(key, value)); + } + } + } + + /// Inserts an item into the Dictionary + /// Key under which to add the new item + /// Item that will be added to the Dictionary + public void Add(TKey key, TValue value) { + this.typedDictionary.Add(key, value); + OnAdded(new KeyValuePair(key, value)); + } + + /// Removes the item with the specified key from the Dictionary + /// Key of the elementes that will be removed + /// True if an item with the specified key was found and removed + public bool Remove(TKey key) { + TValue oldValue; + this.typedDictionary.TryGetValue(key, out oldValue); + + bool removed = this.typedDictionary.Remove(key); + if(removed) { + OnRemoved(new KeyValuePair(key, oldValue)); + } + return removed; + } + + /// Removes all items from the Dictionary + public void Clear() { + OnClearing(); + this.typedDictionary.Clear(); + OnCleared(); + } + + /// Fires the 'ItemAdded' event + /// Item that has been added to the collection + protected virtual void OnAdded(KeyValuePair item) { + if(ItemAdded != null) + ItemAdded(this, new ItemEventArgs>(item)); + +#if !NO_SPECIALIZED_COLLECTIONS + if(CollectionChanged != null) { + CollectionChanged( + this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item) + ); + } +#endif + } + + /// Fires the 'ItemRemoved' event + /// Item that has been removed from the collection + protected virtual void OnRemoved(KeyValuePair item) { + if(ItemRemoved != null) { + ItemRemoved(this, new ItemEventArgs>(item)); + } +#if !NO_SPECIALIZED_COLLECTIONS + if(CollectionChanged != null) { + CollectionChanged( + this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item) + ); + } +#endif + } + + /// Fires the 'ItemReplaced' event + /// Item that has been replaced in the collection + /// Item with which the original item was replaced + protected virtual void OnReplaced( + KeyValuePair oldItem, KeyValuePair newItem + ) { + if(ItemReplaced != null) { + ItemReplaced( + this, + new ItemReplaceEventArgs>(oldItem, newItem) + ); + } +#if !NO_SPECIALIZED_COLLECTIONS + if(CollectionChanged != null) { + CollectionChanged( + this, new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Replace, newItem, oldItem + ) + ); + } +#endif + } + + /// Fires the 'Clearing' event + protected virtual void OnClearing() { + if(Clearing != null) { + Clearing(this, EventArgs.Empty); + } + } + + /// Fires the 'Cleared' event + protected virtual void OnCleared() { + if(Cleared != null) { + Cleared(this, EventArgs.Empty); + } +#if !NO_SPECIALIZED_COLLECTIONS + if(CollectionChanged != null) { + CollectionChanged(this, Constants.NotifyCollectionResetEventArgs); + } +#endif + } + + #region IEnumerable implementation + + /// Returns a new object enumerator for the Dictionary + /// The new object enumerator + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { + return (this.typedDictionary as IEnumerable).GetEnumerator(); + } + + #endregion + + #region IDictionary implementation + + /// Adds an item into the Dictionary + /// Key under which the item will be added + /// Item that will be added + void IDictionary.Add(object key, object value) { + this.objectDictionary.Add(key, value); + OnAdded(new KeyValuePair((TKey)key, (TValue)value)); + } + + /// Determines whether the specified key exists in the Dictionary + /// Key that will be checked for + /// True if an item with the specified key exists in the Dictionary + bool IDictionary.Contains(object key) { + return this.objectDictionary.Contains(key); + } + + /// Whether the size of the Dictionary is fixed + bool IDictionary.IsFixedSize { + get { return this.objectDictionary.IsFixedSize; } + } + + /// Returns a collection of all keys in the Dictionary + ICollection IDictionary.Keys { + get { return this.objectDictionary.Keys; } + } + + /// Returns a collection of all values stored in the Dictionary + ICollection IDictionary.Values { + get { return this.objectDictionary.Values; } + } + + /// Removes an item from the Dictionary + /// Key of the item that will be removed + void IDictionary.Remove(object key) { + TValue value; + bool removed = this.typedDictionary.TryGetValue((TKey)key, out value); + this.objectDictionary.Remove(key); + if(removed) { + OnRemoved(new KeyValuePair((TKey)key, (TValue)value)); + } + } + + /// Accesses an item in the Dictionary by its key + /// Key of the item that will be accessed + /// The item with the specified key + object IDictionary.this[object key] { + get { return this.objectDictionary[key]; } + set { + bool removed; + TValue oldValue; + removed = this.typedDictionary.TryGetValue((TKey)key, out oldValue); + + this.objectDictionary[key] = value; + + if(removed) { + OnRemoved(new KeyValuePair((TKey)key, oldValue)); + } + OnAdded(new KeyValuePair((TKey)key, (TValue)value)); + } + } + + #endregion // IDictionary implementation + + #region IDictionaryEnumerator implementation + + /// Returns a new entry enumerator for the dictionary + /// The new entry enumerator + IDictionaryEnumerator IDictionary.GetEnumerator() { + return this.objectDictionary.GetEnumerator(); + } + + #endregion // IDictionaryEnumerator implementation + + #region ICollection<> implementation + + /// Inserts an already prepared element into the Dictionary + /// Prepared element that will be added to the Dictionary + void ICollection>.Add( + KeyValuePair item + ) { + this.typedDictionary.Add(item); + OnAdded(item); + } + + /// Removes all items from the Dictionary + void ICollection>.Clear() { + OnClearing(); + this.typedDictionary.Clear(); + OnCleared(); + } + + /// Removes all items from the Dictionary + /// Item that will be removed from the Dictionary + bool ICollection>.Remove( + KeyValuePair itemToRemove + ) { + bool removed = this.typedDictionary.Remove(itemToRemove); + if(removed) { + OnRemoved(itemToRemove); + } + return removed; + } + + #endregion + + #region ICollection implementation + + /// Copies the contents of the Dictionary into an array + /// Array the Dictionary contents will be copied into + /// + /// Starting index at which to begin filling the destination array + /// + void ICollection.CopyTo(Array array, int index) { + this.objectDictionary.CopyTo(array, index); + } + + /// Whether the Dictionary is synchronized for multi-threaded usage + bool ICollection.IsSynchronized { + get { return this.objectDictionary.IsSynchronized; } + } + + /// Synchronization root on which the Dictionary locks + object ICollection.SyncRoot { + get { return this.objectDictionary.SyncRoot; } + } + + #endregion + +#if !NO_SERIALIZATION + #region ISerializable implementation + + /// Serializes the Dictionary + /// + /// Provides the container into which the Dictionary will serialize itself + /// + /// + /// Contextual informations about the serialization environment + /// + void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) { + (this.typedDictionary as ISerializable).GetObjectData(info, context); + } + + /// Called after all objects have been successfully deserialized + /// Nicht unterstützt + void IDeserializationCallback.OnDeserialization(object sender) { + (this.typedDictionary as IDeserializationCallback).OnDeserialization(sender); + } + + #endregion +#endif //!NO_SERIALIZATION + + /// The wrapped Dictionary under its type-safe interface + private IDictionary typedDictionary; + /// The wrapped Dictionary under its object interface + private IDictionary objectDictionary; + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/ObservableList.Test.cs b/Source/Collections/ObservableList.Test.cs index 7dfc545..1d1000d 100644 --- a/Source/Collections/ObservableList.Test.cs +++ b/Source/Collections/ObservableList.Test.cs @@ -1,176 +1,175 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if !NO_NMOCK - -using System; -using System.Collections.Generic; - -#if UNITTEST - -using NUnit.Framework; -using NMock; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the observable list class - [TestFixture] - internal class ObservableListTest { - - #region interface IObservableCollectionSubscriber - - /// Interface used to test the observable collection - public interface IObservableCollectionSubscriber { - - /// Called when the collection is about to clear its contents - /// Collection that is clearing its contents - /// Not used - void Clearing(object sender, EventArgs arguments); - - /// Called when the collection has been cleared of its contents - /// Collection that was cleared of its contents - /// Not used - void Cleared(object sender, EventArgs arguments); - - /// Called when an item is added to the collection - /// Collection to which an item is being added - /// Contains the item that is being added - void ItemAdded(object sender, ItemEventArgs arguments); - - /// Called when an item is removed from the collection - /// Collection from which an item is being removed - /// Contains the item that is being removed - void ItemRemoved(object sender, ItemEventArgs arguments); - - /// Called when an item is replaced in the dictionary - /// Dictionary in which an item is being replaced - /// Contains the replaced item and its replacement - void ItemReplaced(object sender, ItemReplaceEventArgs arguments); - - } - - #endregion // interface IObservableCollectionSubscriber - - /// Initialization routine executed before each test is run - [SetUp] - public void Setup() { - this.mockery = new MockFactory(); - - this.mockedSubscriber = this.mockery.CreateMock(); - - this.observedList = new ObservableList(); - this.observedList.Clearing += new EventHandler( - this.mockedSubscriber.MockObject.Clearing - ); - this.observedList.Cleared += new EventHandler( - this.mockedSubscriber.MockObject.Cleared - ); - this.observedList.ItemAdded += new EventHandler>( - this.mockedSubscriber.MockObject.ItemAdded - ); - this.observedList.ItemRemoved += new EventHandler>( - this.mockedSubscriber.MockObject.ItemRemoved - ); - this.observedList.ItemReplaced += new EventHandler>( - this.mockedSubscriber.MockObject.ItemReplaced - ); - } - - /// Tests whether the Clearing event is fired - [Test] - public void TestClearingEvent() { - this.mockedSubscriber.Expects.One.Method(m => m.Clearing(null, null)).WithAnyArguments(); - this.mockedSubscriber.Expects.One.Method(m => m.Cleared(null, null)).WithAnyArguments(); - this.observedList.Clear(); - - this.mockery.VerifyAllExpectationsHaveBeenMet(); - } - - /// Tests whether the ItemAdded event is fired - [Test] - public void TestItemAddedEvent() { - this.mockedSubscriber.Expects.One.Method(m => m.ItemAdded(null, null)).WithAnyArguments(); - - this.observedList.Add(123); - - this.mockery.VerifyAllExpectationsHaveBeenMet(); - } - - /// Tests whether the ItemRemoved event is fired - [Test] - public void TestItemRemovedEvent() { - this.mockedSubscriber.Expects.One.Method(m => m.ItemAdded(null, null)).WithAnyArguments(); - - this.observedList.Add(123); - - this.mockedSubscriber.Expects.One.Method(m => m.ItemRemoved(null, null)).WithAnyArguments(); - - this.observedList.Remove(123); - - this.mockery.VerifyAllExpectationsHaveBeenMet(); - } - - /// Tests whether items in the collection can be replaced - [Test] - public void TestItemReplacement() { - this.mockedSubscriber.Expects.Exactly(3).Method( - m => m.ItemAdded(null, null) - ).WithAnyArguments(); - - this.observedList.Add(1); - this.observedList.Add(2); - this.observedList.Add(3); - - this.mockedSubscriber.Expects.One.Method(m => m.ItemReplaced(null, null)).WithAnyArguments(); - - // Replace the middle item with something else - this.observedList[1] = 4; - - Assert.AreEqual( - 1, this.observedList.IndexOf(4) - ); - - this.mockery.VerifyAllExpectationsHaveBeenMet(); - } - - /// Tests whether a the list constructor is working - [Test] - public void TestListConstructor() { - int[] integers = new int[] { 12, 34, 56, 78 }; - - var testList = new ObservableList(integers); - - CollectionAssert.AreEqual(integers, testList); - } - - /// Mock object factory - private MockFactory mockery; - /// The mocked observable collection subscriber - private Mock mockedSubscriber; - /// An observable collection to which a mock will be subscribed - private ObservableList observedList; - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST - -#endif // !NO_NMOCK +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if !NO_NMOCK + +using System; +using System.Collections.Generic; + +#if UNITTEST + +using NUnit.Framework; +using NMock; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the observable list class + [TestFixture] + internal class ObservableListTest { + + #region interface IObservableCollectionSubscriber + + /// Interface used to test the observable collection + public interface IObservableCollectionSubscriber { + + /// Called when the collection is about to clear its contents + /// Collection that is clearing its contents + /// Not used + void Clearing(object sender, EventArgs arguments); + + /// Called when the collection has been cleared of its contents + /// Collection that was cleared of its contents + /// Not used + void Cleared(object sender, EventArgs arguments); + + /// Called when an item is added to the collection + /// Collection to which an item is being added + /// Contains the item that is being added + void ItemAdded(object sender, ItemEventArgs arguments); + + /// Called when an item is removed from the collection + /// Collection from which an item is being removed + /// Contains the item that is being removed + void ItemRemoved(object sender, ItemEventArgs arguments); + + /// Called when an item is replaced in the dictionary + /// Dictionary in which an item is being replaced + /// Contains the replaced item and its replacement + void ItemReplaced(object sender, ItemReplaceEventArgs arguments); + + } + + #endregion // interface IObservableCollectionSubscriber + + /// Initialization routine executed before each test is run + [SetUp] + public void Setup() { + this.mockery = new MockFactory(); + + this.mockedSubscriber = this.mockery.CreateMock(); + + this.observedList = new ObservableList(); + this.observedList.Clearing += new EventHandler( + this.mockedSubscriber.MockObject.Clearing + ); + this.observedList.Cleared += new EventHandler( + this.mockedSubscriber.MockObject.Cleared + ); + this.observedList.ItemAdded += new EventHandler>( + this.mockedSubscriber.MockObject.ItemAdded + ); + this.observedList.ItemRemoved += new EventHandler>( + this.mockedSubscriber.MockObject.ItemRemoved + ); + this.observedList.ItemReplaced += new EventHandler>( + this.mockedSubscriber.MockObject.ItemReplaced + ); + } + + /// Tests whether the Clearing event is fired + [Test] + public void TestClearingEvent() { + this.mockedSubscriber.Expects.One.Method(m => m.Clearing(null, null)).WithAnyArguments(); + this.mockedSubscriber.Expects.One.Method(m => m.Cleared(null, null)).WithAnyArguments(); + this.observedList.Clear(); + + this.mockery.VerifyAllExpectationsHaveBeenMet(); + } + + /// Tests whether the ItemAdded event is fired + [Test] + public void TestItemAddedEvent() { + this.mockedSubscriber.Expects.One.Method(m => m.ItemAdded(null, null)).WithAnyArguments(); + + this.observedList.Add(123); + + this.mockery.VerifyAllExpectationsHaveBeenMet(); + } + + /// Tests whether the ItemRemoved event is fired + [Test] + public void TestItemRemovedEvent() { + this.mockedSubscriber.Expects.One.Method(m => m.ItemAdded(null, null)).WithAnyArguments(); + + this.observedList.Add(123); + + this.mockedSubscriber.Expects.One.Method(m => m.ItemRemoved(null, null)).WithAnyArguments(); + + this.observedList.Remove(123); + + this.mockery.VerifyAllExpectationsHaveBeenMet(); + } + + /// Tests whether items in the collection can be replaced + [Test] + public void TestItemReplacement() { + this.mockedSubscriber.Expects.Exactly(3).Method( + m => m.ItemAdded(null, null) + ).WithAnyArguments(); + + this.observedList.Add(1); + this.observedList.Add(2); + this.observedList.Add(3); + + this.mockedSubscriber.Expects.One.Method(m => m.ItemReplaced(null, null)).WithAnyArguments(); + + // Replace the middle item with something else + this.observedList[1] = 4; + + Assert.AreEqual( + 1, this.observedList.IndexOf(4) + ); + + this.mockery.VerifyAllExpectationsHaveBeenMet(); + } + + /// Tests whether a the list constructor is working + [Test] + public void TestListConstructor() { + int[] integers = new int[] { 12, 34, 56, 78 }; + + var testList = new ObservableList(integers); + + CollectionAssert.AreEqual(integers, testList); + } + + /// Mock object factory + private MockFactory mockery; + /// The mocked observable collection subscriber + private Mock mockedSubscriber; + /// An observable collection to which a mock will be subscribed + private ObservableList observedList; + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST + +#endif // !NO_NMOCK diff --git a/Source/Collections/ObservableList.cs b/Source/Collections/ObservableList.cs index 7398d32..6d8b5e6 100644 --- a/Source/Collections/ObservableList.cs +++ b/Source/Collections/ObservableList.cs @@ -1,361 +1,360 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections; -using System.Collections.Generic; - -#if !NO_SPECIALIZED_COLLECTIONS -using System.Collections.Specialized; -#endif - -namespace Nuclex.Support.Collections { - - /// List which fires events when items are added or removed - /// Type of items the collection manages - public class ObservableList : - IList, - IList, - ICollection, -#if !NO_SPECIALIZED_COLLECTIONS - INotifyCollectionChanged, -#endif - IObservableCollection { - - /// Raised when an item has been added to the collection - public event EventHandler> ItemAdded; - /// Raised when an item is removed from the collection - public event EventHandler> ItemRemoved; - /// Raised when an item is replaced in the collection - public event EventHandler> ItemReplaced; - /// Raised when the collection is about to be cleared - /// - /// This could be covered by calling ItemRemoved for each item currently - /// contained in the collection, but it is often simpler and more efficient - /// to process the clearing of the entire collection as a special operation. - /// - public event EventHandler Clearing; - /// Raised when the collection has been cleared - public event EventHandler Cleared; - -#if !NO_SPECIALIZED_COLLECTIONS - /// Called when the collection has changed - public event NotifyCollectionChangedEventHandler CollectionChanged; -#endif - - /// - /// Initializes a new instance of the ObservableList class that is empty. - /// - public ObservableList() : this(new List()) { } - - /// - /// Initializes a new instance of the ObservableList class as a wrapper - /// for the specified list. - /// - /// The list that is wrapped by the new collection. - /// List is null - public ObservableList(IList list) { - this.typedList = list; - this.objectList = list as IList; // Gah! - } - - /// Determines the index of the specified item in the list - /// Item whose index will be determined - /// The index of the item in the list or -1 if not found - public int IndexOf(TItem item) { - return this.typedList.IndexOf(item); - } - - /// Inserts an item into the list at the specified index - /// Index the item will be insertted at - /// Item that will be inserted into the list - public void Insert(int index, TItem item) { - this.typedList.Insert(index, item); - OnAdded(item, index); - } - - /// Removes the item at the specified index from the list - /// Index at which the item will be removed - public void RemoveAt(int index) { - TItem item = this.typedList[index]; - this.typedList.RemoveAt(index); - OnRemoved(item, index); - } - - /// Accesses the item at the specified index in the list - /// Index of the item that will be accessed - /// The item at the specified index - public TItem this[int index] { - get { return this.typedList[index]; } - set { - TItem oldItem = this.typedList[index]; - this.typedList[index] = value; - OnReplaced(oldItem, value, index); - } - } - - /// Adds an item to the end of the list - /// Item that will be added to the list - public void Add(TItem item) { - this.typedList.Add(item); - OnAdded(item, this.typedList.Count - 1); - } - - /// Removes all items from the list - public void Clear() { - OnClearing(); - this.typedList.Clear(); - OnCleared(); - } - - /// Checks whether the list contains the specified item - /// Item the list will be checked for - /// True if the list contains the specified items - public bool Contains(TItem item) { - return this.typedList.Contains(item); - } - - /// Copies the contents of the list into an array - /// Array the list will be copied into - /// - /// Index in the target array where the first item will be copied to - /// - public void CopyTo(TItem[] array, int arrayIndex) { - this.typedList.CopyTo(array, arrayIndex); - } - - /// Total number of items in the list - public int Count { - get { return this.typedList.Count; } - } - - /// Whether the list is a read-only list - public bool IsReadOnly { - get { return this.typedList.IsReadOnly; } - } - - /// Removes the specified item from the list - /// Item that will be removed from the list - /// - /// True if the item was found and removed from the list, false otherwise - /// - public bool Remove(TItem item) { - int index = this.typedList.IndexOf(item); - if(index == -1) { - return false; - } - - TItem removedItem = this.typedList[index]; - this.typedList.RemoveAt(index); - OnRemoved(removedItem, index); - - return true; - } - - /// Returns an enumerator for the items in the list - /// An enumerator for the list's items - public IEnumerator GetEnumerator() { - return this.typedList.GetEnumerator(); - } - - #region IEnumerable implementation - - /// Returns an enumerator for the items in the list - /// An enumerator for the list's items - IEnumerator IEnumerable.GetEnumerator() { - return this.objectList.GetEnumerator(); - } - - #endregion // IEnumerable implementation - - #region ICollection implementation - - /// Copies the contents of the list into an array - /// Array the list will be copied into - /// - /// Index in the target array where the first item will be copied to - /// - void ICollection.CopyTo(Array array, int arrayIndex) { - this.objectList.CopyTo(array, arrayIndex); - } - - /// Whether this list performs thread synchronization - bool ICollection.IsSynchronized { - get { return this.objectList.IsSynchronized; } - } - - /// Synchronization root used by the list to synchronize threads - object ICollection.SyncRoot { - get { return this.objectList.SyncRoot; } - } - - #endregion // ICollection implementation - - #region IList implementation - - /// Adds an item to the list - /// Item that will be added to the list - /// - /// The position at which the item has been inserted or -1 if the item was not inserted - /// - int IList.Add(object value) { - int index = this.objectList.Add(value); - TItem addedItem = this.typedList[index]; - OnAdded(addedItem, index); - return index; - } - - /// Checks whether the list contains the specified item - /// Item the list will be checked for - /// True if the list contains the specified items - bool IList.Contains(object item) { - return this.objectList.Contains(item); - } - - /// Determines the index of the specified item in the list - /// Item whose index will be determined - /// The index of the item in the list or -1 if not found - int IList.IndexOf(object item) { - return this.objectList.IndexOf(item); - } - - /// Inserts an item into the list at the specified index - /// Index the item will be insertted at - /// Item that will be inserted into the list - void IList.Insert(int index, object item) { - this.objectList.Insert(index, item); - TItem addedItem = this.typedList[index]; - OnAdded(addedItem, index); - } - - /// Whether the list is of a fixed size - bool IList.IsFixedSize { - get { return this.objectList.IsFixedSize; } - } - - /// Removes the specified item from the list - /// Item that will be removed from the list - void IList.Remove(object item) { - int index = this.objectList.IndexOf(item); - if(index == -1) { - return; - } - - TItem removedItem = this.typedList[index]; - this.objectList.RemoveAt(index); - OnRemoved(removedItem, index); - } - - /// Accesses the item at the specified index in the list - /// Index of the item that will be accessed - /// The item at the specified index - object IList.this[int index] { - get { return this.objectList[index]; } - set { - TItem oldItem = this.typedList[index]; - this.objectList[index] = value; - TItem newItem = this.typedList[index]; - OnReplaced(oldItem, newItem, index); - } - } - - #endregion // IList implementation - - /// Fires the 'ItemAdded' event - /// Item that has been added to the collection - /// Index of the added item - protected virtual void OnAdded(TItem item, int index) { - if(ItemAdded != null) { - ItemAdded(this, new ItemEventArgs(item)); - } -#if !NO_SPECIALIZED_COLLECTIONS - if(CollectionChanged != null) { - CollectionChanged( - this, - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index) - ); - } -#endif - } - - /// Fires the 'ItemRemoved' event - /// Item that has been removed from the collection - /// Index the item has been removed from - protected virtual void OnRemoved(TItem item, int index) { - if(ItemRemoved != null) { - ItemRemoved(this, new ItemEventArgs(item)); - } -#if !NO_SPECIALIZED_COLLECTIONS - if(CollectionChanged != null) { - CollectionChanged( - this, - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index) - ); - } -#endif - } - - /// Fires the 'ItemReplaced' event - /// Item that has been replaced - /// New item the original item was replaced with - /// Index of the replaced item - protected virtual void OnReplaced(TItem oldItem, TItem newItem, int index) { - if(ItemReplaced != null) { - ItemReplaced(this, new ItemReplaceEventArgs(oldItem, newItem)); - } -#if !NO_SPECIALIZED_COLLECTIONS - if(CollectionChanged != null) { - CollectionChanged( - this, - new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Replace, newItem, oldItem, index - ) - ); - } -#endif - } - - /// Fires the 'Clearing' event - protected virtual void OnClearing() { - if(Clearing != null) { - Clearing(this, EventArgs.Empty); - } - } - - /// Fires the 'Cleared' event - protected virtual void OnCleared() { - if(Cleared != null) { - Cleared(this, EventArgs.Empty); - } -#if !NO_SPECIALIZED_COLLECTIONS - if(CollectionChanged != null) { - CollectionChanged(this, Constants.NotifyCollectionResetEventArgs); - } -#endif - } - - /// The wrapped list under its type-safe interface - private IList typedList; - /// The wrapped list under its object interface - private IList objectList; - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections; +using System.Collections.Generic; + +#if !NO_SPECIALIZED_COLLECTIONS +using System.Collections.Specialized; +#endif + +namespace Nuclex.Support.Collections { + + /// List which fires events when items are added or removed + /// Type of items the collection manages + public class ObservableList : + IList, + IList, + ICollection, +#if !NO_SPECIALIZED_COLLECTIONS + INotifyCollectionChanged, +#endif + IObservableCollection { + + /// Raised when an item has been added to the collection + public event EventHandler> ItemAdded; + /// Raised when an item is removed from the collection + public event EventHandler> ItemRemoved; + /// Raised when an item is replaced in the collection + public event EventHandler> ItemReplaced; + /// Raised when the collection is about to be cleared + /// + /// This could be covered by calling ItemRemoved for each item currently + /// contained in the collection, but it is often simpler and more efficient + /// to process the clearing of the entire collection as a special operation. + /// + public event EventHandler Clearing; + /// Raised when the collection has been cleared + public event EventHandler Cleared; + +#if !NO_SPECIALIZED_COLLECTIONS + /// Called when the collection has changed + public event NotifyCollectionChangedEventHandler CollectionChanged; +#endif + + /// + /// Initializes a new instance of the ObservableList class that is empty. + /// + public ObservableList() : this(new List()) { } + + /// + /// Initializes a new instance of the ObservableList class as a wrapper + /// for the specified list. + /// + /// The list that is wrapped by the new collection. + /// List is null + public ObservableList(IList list) { + this.typedList = list; + this.objectList = list as IList; // Gah! + } + + /// Determines the index of the specified item in the list + /// Item whose index will be determined + /// The index of the item in the list or -1 if not found + public int IndexOf(TItem item) { + return this.typedList.IndexOf(item); + } + + /// Inserts an item into the list at the specified index + /// Index the item will be insertted at + /// Item that will be inserted into the list + public void Insert(int index, TItem item) { + this.typedList.Insert(index, item); + OnAdded(item, index); + } + + /// Removes the item at the specified index from the list + /// Index at which the item will be removed + public void RemoveAt(int index) { + TItem item = this.typedList[index]; + this.typedList.RemoveAt(index); + OnRemoved(item, index); + } + + /// Accesses the item at the specified index in the list + /// Index of the item that will be accessed + /// The item at the specified index + public TItem this[int index] { + get { return this.typedList[index]; } + set { + TItem oldItem = this.typedList[index]; + this.typedList[index] = value; + OnReplaced(oldItem, value, index); + } + } + + /// Adds an item to the end of the list + /// Item that will be added to the list + public void Add(TItem item) { + this.typedList.Add(item); + OnAdded(item, this.typedList.Count - 1); + } + + /// Removes all items from the list + public void Clear() { + OnClearing(); + this.typedList.Clear(); + OnCleared(); + } + + /// Checks whether the list contains the specified item + /// Item the list will be checked for + /// True if the list contains the specified items + public bool Contains(TItem item) { + return this.typedList.Contains(item); + } + + /// Copies the contents of the list into an array + /// Array the list will be copied into + /// + /// Index in the target array where the first item will be copied to + /// + public void CopyTo(TItem[] array, int arrayIndex) { + this.typedList.CopyTo(array, arrayIndex); + } + + /// Total number of items in the list + public int Count { + get { return this.typedList.Count; } + } + + /// Whether the list is a read-only list + public bool IsReadOnly { + get { return this.typedList.IsReadOnly; } + } + + /// Removes the specified item from the list + /// Item that will be removed from the list + /// + /// True if the item was found and removed from the list, false otherwise + /// + public bool Remove(TItem item) { + int index = this.typedList.IndexOf(item); + if(index == -1) { + return false; + } + + TItem removedItem = this.typedList[index]; + this.typedList.RemoveAt(index); + OnRemoved(removedItem, index); + + return true; + } + + /// Returns an enumerator for the items in the list + /// An enumerator for the list's items + public IEnumerator GetEnumerator() { + return this.typedList.GetEnumerator(); + } + + #region IEnumerable implementation + + /// Returns an enumerator for the items in the list + /// An enumerator for the list's items + IEnumerator IEnumerable.GetEnumerator() { + return this.objectList.GetEnumerator(); + } + + #endregion // IEnumerable implementation + + #region ICollection implementation + + /// Copies the contents of the list into an array + /// Array the list will be copied into + /// + /// Index in the target array where the first item will be copied to + /// + void ICollection.CopyTo(Array array, int arrayIndex) { + this.objectList.CopyTo(array, arrayIndex); + } + + /// Whether this list performs thread synchronization + bool ICollection.IsSynchronized { + get { return this.objectList.IsSynchronized; } + } + + /// Synchronization root used by the list to synchronize threads + object ICollection.SyncRoot { + get { return this.objectList.SyncRoot; } + } + + #endregion // ICollection implementation + + #region IList implementation + + /// Adds an item to the list + /// Item that will be added to the list + /// + /// The position at which the item has been inserted or -1 if the item was not inserted + /// + int IList.Add(object value) { + int index = this.objectList.Add(value); + TItem addedItem = this.typedList[index]; + OnAdded(addedItem, index); + return index; + } + + /// Checks whether the list contains the specified item + /// Item the list will be checked for + /// True if the list contains the specified items + bool IList.Contains(object item) { + return this.objectList.Contains(item); + } + + /// Determines the index of the specified item in the list + /// Item whose index will be determined + /// The index of the item in the list or -1 if not found + int IList.IndexOf(object item) { + return this.objectList.IndexOf(item); + } + + /// Inserts an item into the list at the specified index + /// Index the item will be insertted at + /// Item that will be inserted into the list + void IList.Insert(int index, object item) { + this.objectList.Insert(index, item); + TItem addedItem = this.typedList[index]; + OnAdded(addedItem, index); + } + + /// Whether the list is of a fixed size + bool IList.IsFixedSize { + get { return this.objectList.IsFixedSize; } + } + + /// Removes the specified item from the list + /// Item that will be removed from the list + void IList.Remove(object item) { + int index = this.objectList.IndexOf(item); + if(index == -1) { + return; + } + + TItem removedItem = this.typedList[index]; + this.objectList.RemoveAt(index); + OnRemoved(removedItem, index); + } + + /// Accesses the item at the specified index in the list + /// Index of the item that will be accessed + /// The item at the specified index + object IList.this[int index] { + get { return this.objectList[index]; } + set { + TItem oldItem = this.typedList[index]; + this.objectList[index] = value; + TItem newItem = this.typedList[index]; + OnReplaced(oldItem, newItem, index); + } + } + + #endregion // IList implementation + + /// Fires the 'ItemAdded' event + /// Item that has been added to the collection + /// Index of the added item + protected virtual void OnAdded(TItem item, int index) { + if(ItemAdded != null) { + ItemAdded(this, new ItemEventArgs(item)); + } +#if !NO_SPECIALIZED_COLLECTIONS + if(CollectionChanged != null) { + CollectionChanged( + this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index) + ); + } +#endif + } + + /// Fires the 'ItemRemoved' event + /// Item that has been removed from the collection + /// Index the item has been removed from + protected virtual void OnRemoved(TItem item, int index) { + if(ItemRemoved != null) { + ItemRemoved(this, new ItemEventArgs(item)); + } +#if !NO_SPECIALIZED_COLLECTIONS + if(CollectionChanged != null) { + CollectionChanged( + this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index) + ); + } +#endif + } + + /// Fires the 'ItemReplaced' event + /// Item that has been replaced + /// New item the original item was replaced with + /// Index of the replaced item + protected virtual void OnReplaced(TItem oldItem, TItem newItem, int index) { + if(ItemReplaced != null) { + ItemReplaced(this, new ItemReplaceEventArgs(oldItem, newItem)); + } +#if !NO_SPECIALIZED_COLLECTIONS + if(CollectionChanged != null) { + CollectionChanged( + this, + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Replace, newItem, oldItem, index + ) + ); + } +#endif + } + + /// Fires the 'Clearing' event + protected virtual void OnClearing() { + if(Clearing != null) { + Clearing(this, EventArgs.Empty); + } + } + + /// Fires the 'Cleared' event + protected virtual void OnCleared() { + if(Cleared != null) { + Cleared(this, EventArgs.Empty); + } +#if !NO_SPECIALIZED_COLLECTIONS + if(CollectionChanged != null) { + CollectionChanged(this, Constants.NotifyCollectionResetEventArgs); + } +#endif + } + + /// The wrapped list under its type-safe interface + private IList typedList; + /// The wrapped list under its object interface + private IList objectList; + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/ObservableSet.Test.cs b/Source/Collections/ObservableSet.Test.cs index 65d0fd4..af840a9 100644 --- a/Source/Collections/ObservableSet.Test.cs +++ b/Source/Collections/ObservableSet.Test.cs @@ -1,301 +1,300 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if !NO_SETS - -#if UNITTEST - -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Runtime.Serialization.Formatters.Binary; - -using NUnit.Framework; -using NMock; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the observable set wrapper - [TestFixture] - internal class ObservableSetTest { - - #region interface IObservableCollectionSubscriber - - public interface IObservableCollectionSubscriber { - - /// Called when an item has been added to the collection - void ItemAdded(object sender, ItemEventArgs arguments); - /// Called when an item is removed from the collection - void ItemRemoved(object sender, ItemEventArgs arguments); - /// Called when an item is replaced in the collection - void ItemReplaced(object sender, ItemReplaceEventArgs arguments); - /// Called when the collection is about to be cleared - void Clearing(object sender, EventArgs arguments); - /// Called when the collection has been cleared - void Cleared(object sender, EventArgs arguments); - - } - - #endregion // interface IObservableCollectionSubscriber - - /// Called before each test is run - [SetUp] - public void Setup() { - this.mockFactory = new MockFactory(); - this.observableSet = new ObservableSet(); - - this.subscriber = this.mockFactory.CreateMock>(); - this.observableSet.ItemAdded += this.subscriber.MockObject.ItemAdded; - this.observableSet.ItemRemoved += this.subscriber.MockObject.ItemRemoved; - this.observableSet.ItemReplaced += this.subscriber.MockObject.ItemReplaced; - this.observableSet.Clearing += this.subscriber.MockObject.Clearing; - this.observableSet.Cleared += this.subscriber.MockObject.Cleared; - } - - /// Called after each test has run - [TearDown] - public void Teardown() { - if(this.mockFactory != null) { - this.mockFactory.VerifyAllExpectationsHaveBeenMet(); - - this.subscriber = null; - this.mockFactory.Dispose(); - this.mockFactory = null; - } - } - - /// - /// Verifies that the observable set has a default constructor - /// - [Test] - public void HasDefaultConstructor() { - Assert.IsNotNull(new ObservableSet()); - } - - /// - /// Verifies that adding items to the set triggers the 'ItemAdded' event - /// - [Test] - public void AddingItemsTriggersEvent() { - this.subscriber.Expects.One.Method((s) => s.ItemAdded(null, null)).WithAnyArguments(); - this.observableSet.Add(123); - } - - /// - /// Verifies that removing items from the set triggers the 'ItemRemoved' event - /// - [Test] - public void RemovingItemsTriggersEvent() { - this.subscriber.Expects.One.Method((s) => s.ItemAdded(null, null)).WithAnyArguments(); - this.observableSet.Add(123); - - this.subscriber.Expects.One.Method((s) => s.ItemRemoved(null, null)).WithAnyArguments(); - this.observableSet.Remove(123); - } - - /// - /// Verifies that adding items to the set triggers the 'ItemAdded' event - /// - [Test] - public void AddingAlreadyContainedItemDoesNotTriggerEvent() { - this.subscriber.Expects.One.Method((s) => s.ItemAdded(null, null)).WithAnyArguments(); - this.observableSet.Add(123); - - this.subscriber.Expects.No.Method((s) => s.ItemAdded(null, null)).WithAnyArguments(); - this.observableSet.Add(123); - } - - /// - /// Verifies that excepting the set with itself empties the set - /// - [Test] - public void ExceptWithSelfEmptiesSet() { - this.subscriber.Expects.Exactly(3).Method( - (s) => s.ItemAdded(null, null) - ).WithAnyArguments(); - - this.observableSet.Add(1); - this.observableSet.Add(2); - this.observableSet.Add(3); - - Assert.AreEqual(3, this.observableSet.Count); - - this.subscriber.Expects.One.Method((s) => s.Clearing(null, null)).WithAnyArguments(); - this.subscriber.Expects.One.Method((s) => s.Cleared(null, null)).WithAnyArguments(); - - this.observableSet.ExceptWith(this.observableSet); - Assert.AreEqual(0, this.observableSet.Count); - } - - /// - /// Verifies that a set can be excepted with a collection - /// - [Test] - public void SetCanBeExceptedWithCollection() { - this.subscriber.Expects.Exactly(2).Method( - (s) => s.ItemAdded(null, null) - ).WithAnyArguments(); - - this.observableSet.Add(1); - this.observableSet.Add(2); - - var collection = new List() { 1 }; - - this.subscriber.Expects.One.Method((s) => s.ItemRemoved(null, null)).WithAnyArguments(); - this.observableSet.ExceptWith(collection); - Assert.AreEqual(1, this.observableSet.Count); - Assert.IsTrue(this.observableSet.Contains(2)); - } - - /// - /// Verifies that a set can be intersected with a collection - /// - [Test] - public void SetCanBeIntersectedWithCollection() { - this.subscriber.Expects.Exactly(2).Method( - (s) => s.ItemAdded(null, null) - ).WithAnyArguments(); - - this.observableSet.Add(1); - this.observableSet.Add(2); - - var collection = new List() { 1 }; - - this.subscriber.Expects.One.Method((s) => s.ItemRemoved(null, null)).WithAnyArguments(); - this.observableSet.IntersectWith(collection); - Assert.AreEqual(1, this.observableSet.Count); - Assert.IsTrue(this.observableSet.Contains(1)); - } - - /// - /// Verifies that it's possible to determine whether a set is a proper subset - /// or superset of another set - /// - [Test] - public void CanDetermineProperSubsetAndSuperset() { - var set1 = new ObservableSet() { 1, 2, 3 }; - var set2 = new HashSet() { 1, 3 }; - - Assert.IsTrue(set1.IsProperSupersetOf(set2)); - Assert.IsTrue(set2.IsProperSubsetOf(set1)); - - set2.Add(2); - - Assert.IsFalse(set1.IsProperSupersetOf(set2)); - Assert.IsFalse(set2.IsProperSubsetOf(set1)); - } - - /// - /// Verifies that it's possible to determine whether a set is a subset - /// or a superset of another set - /// - [Test] - public void CanDetermineSubsetAndSuperset() { - var set1 = new ObservableSet() { 1, 2, 3 }; - var set2 = new HashSet() { 1, 2, 3 }; - - Assert.IsTrue(set1.IsSupersetOf(set2)); - Assert.IsTrue(set2.IsSubsetOf(set1)); - - set2.Add(4); - - Assert.IsFalse(set1.IsSupersetOf(set2)); - Assert.IsFalse(set2.IsSubsetOf(set1)); - } - - /// - /// Verifies that a set can determine if another set overlaps with it - /// - [Test] - public void CanDetermineOverlap() { - var set1 = new ObservableSet() { 1, 3, 5 }; - var set2 = new HashSet() { 3 }; - - Assert.IsTrue(set1.Overlaps(set2)); - Assert.IsTrue(set2.Overlaps(set1)); - } - - /// - /// Verifies that a set can determine if another set contains the same elements - /// - [Test] - public void CanDetermineSetEquality() { - var set1 = new ObservableSet() { 1, 3, 5 }; - var set2 = new HashSet() { 3, 1, 5 }; - - Assert.IsTrue(set1.SetEquals(set2)); - Assert.IsTrue(set2.SetEquals(set1)); - - set1.Add(7); - - Assert.IsFalse(set1.SetEquals(set2)); - Assert.IsFalse(set2.SetEquals(set1)); - } - - /// - /// Verifies that a set can be symmetrically excepted with another set - /// - [Test] - public void CanBeSymmetricallyExcepted() { - var set1 = new ObservableSet() { 1, 2, 3 }; - var set2 = new HashSet() { 3, 4, 5 }; - - set1.SymmetricExceptWith(set2); - - Assert.AreEqual(4, set1.Count); - } - - /// - /// Verifies that a union of two sets can be built - /// - [Test] - public void CanBeUnioned() { - this.subscriber.Expects.Exactly(3).Method( - (s) => s.ItemAdded(null, null) - ).WithAnyArguments(); - - this.observableSet.Add(1); - this.observableSet.Add(2); - this.observableSet.Add(3); - - var set2 = new ObservableSet() { 3, 4, 5 }; - - this.subscriber.Expects.Exactly(2).Method( - (s) => s.ItemAdded(null, null) - ).WithAnyArguments(); - this.observableSet.UnionWith(set2); - Assert.AreEqual(5, this.observableSet.Count); - } - - /// Creates mock object for the test - private MockFactory mockFactory; - /// Observable set being tested - private ObservableSet observableSet; - /// Subscriber for the observable set's events - private Mock> subscriber; - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST - -#endif // !NO_SETS +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if !NO_SETS + +#if UNITTEST + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; + +using NUnit.Framework; +using NMock; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the observable set wrapper + [TestFixture] + internal class ObservableSetTest { + + #region interface IObservableCollectionSubscriber + + public interface IObservableCollectionSubscriber { + + /// Called when an item has been added to the collection + void ItemAdded(object sender, ItemEventArgs arguments); + /// Called when an item is removed from the collection + void ItemRemoved(object sender, ItemEventArgs arguments); + /// Called when an item is replaced in the collection + void ItemReplaced(object sender, ItemReplaceEventArgs arguments); + /// Called when the collection is about to be cleared + void Clearing(object sender, EventArgs arguments); + /// Called when the collection has been cleared + void Cleared(object sender, EventArgs arguments); + + } + + #endregion // interface IObservableCollectionSubscriber + + /// Called before each test is run + [SetUp] + public void Setup() { + this.mockFactory = new MockFactory(); + this.observableSet = new ObservableSet(); + + this.subscriber = this.mockFactory.CreateMock>(); + this.observableSet.ItemAdded += this.subscriber.MockObject.ItemAdded; + this.observableSet.ItemRemoved += this.subscriber.MockObject.ItemRemoved; + this.observableSet.ItemReplaced += this.subscriber.MockObject.ItemReplaced; + this.observableSet.Clearing += this.subscriber.MockObject.Clearing; + this.observableSet.Cleared += this.subscriber.MockObject.Cleared; + } + + /// Called after each test has run + [TearDown] + public void Teardown() { + if(this.mockFactory != null) { + this.mockFactory.VerifyAllExpectationsHaveBeenMet(); + + this.subscriber = null; + this.mockFactory.Dispose(); + this.mockFactory = null; + } + } + + /// + /// Verifies that the observable set has a default constructor + /// + [Test] + public void HasDefaultConstructor() { + Assert.IsNotNull(new ObservableSet()); + } + + /// + /// Verifies that adding items to the set triggers the 'ItemAdded' event + /// + [Test] + public void AddingItemsTriggersEvent() { + this.subscriber.Expects.One.Method((s) => s.ItemAdded(null, null)).WithAnyArguments(); + this.observableSet.Add(123); + } + + /// + /// Verifies that removing items from the set triggers the 'ItemRemoved' event + /// + [Test] + public void RemovingItemsTriggersEvent() { + this.subscriber.Expects.One.Method((s) => s.ItemAdded(null, null)).WithAnyArguments(); + this.observableSet.Add(123); + + this.subscriber.Expects.One.Method((s) => s.ItemRemoved(null, null)).WithAnyArguments(); + this.observableSet.Remove(123); + } + + /// + /// Verifies that adding items to the set triggers the 'ItemAdded' event + /// + [Test] + public void AddingAlreadyContainedItemDoesNotTriggerEvent() { + this.subscriber.Expects.One.Method((s) => s.ItemAdded(null, null)).WithAnyArguments(); + this.observableSet.Add(123); + + this.subscriber.Expects.No.Method((s) => s.ItemAdded(null, null)).WithAnyArguments(); + this.observableSet.Add(123); + } + + /// + /// Verifies that excepting the set with itself empties the set + /// + [Test] + public void ExceptWithSelfEmptiesSet() { + this.subscriber.Expects.Exactly(3).Method( + (s) => s.ItemAdded(null, null) + ).WithAnyArguments(); + + this.observableSet.Add(1); + this.observableSet.Add(2); + this.observableSet.Add(3); + + Assert.AreEqual(3, this.observableSet.Count); + + this.subscriber.Expects.One.Method((s) => s.Clearing(null, null)).WithAnyArguments(); + this.subscriber.Expects.One.Method((s) => s.Cleared(null, null)).WithAnyArguments(); + + this.observableSet.ExceptWith(this.observableSet); + Assert.AreEqual(0, this.observableSet.Count); + } + + /// + /// Verifies that a set can be excepted with a collection + /// + [Test] + public void SetCanBeExceptedWithCollection() { + this.subscriber.Expects.Exactly(2).Method( + (s) => s.ItemAdded(null, null) + ).WithAnyArguments(); + + this.observableSet.Add(1); + this.observableSet.Add(2); + + var collection = new List() { 1 }; + + this.subscriber.Expects.One.Method((s) => s.ItemRemoved(null, null)).WithAnyArguments(); + this.observableSet.ExceptWith(collection); + Assert.AreEqual(1, this.observableSet.Count); + Assert.IsTrue(this.observableSet.Contains(2)); + } + + /// + /// Verifies that a set can be intersected with a collection + /// + [Test] + public void SetCanBeIntersectedWithCollection() { + this.subscriber.Expects.Exactly(2).Method( + (s) => s.ItemAdded(null, null) + ).WithAnyArguments(); + + this.observableSet.Add(1); + this.observableSet.Add(2); + + var collection = new List() { 1 }; + + this.subscriber.Expects.One.Method((s) => s.ItemRemoved(null, null)).WithAnyArguments(); + this.observableSet.IntersectWith(collection); + Assert.AreEqual(1, this.observableSet.Count); + Assert.IsTrue(this.observableSet.Contains(1)); + } + + /// + /// Verifies that it's possible to determine whether a set is a proper subset + /// or superset of another set + /// + [Test] + public void CanDetermineProperSubsetAndSuperset() { + var set1 = new ObservableSet() { 1, 2, 3 }; + var set2 = new HashSet() { 1, 3 }; + + Assert.IsTrue(set1.IsProperSupersetOf(set2)); + Assert.IsTrue(set2.IsProperSubsetOf(set1)); + + set2.Add(2); + + Assert.IsFalse(set1.IsProperSupersetOf(set2)); + Assert.IsFalse(set2.IsProperSubsetOf(set1)); + } + + /// + /// Verifies that it's possible to determine whether a set is a subset + /// or a superset of another set + /// + [Test] + public void CanDetermineSubsetAndSuperset() { + var set1 = new ObservableSet() { 1, 2, 3 }; + var set2 = new HashSet() { 1, 2, 3 }; + + Assert.IsTrue(set1.IsSupersetOf(set2)); + Assert.IsTrue(set2.IsSubsetOf(set1)); + + set2.Add(4); + + Assert.IsFalse(set1.IsSupersetOf(set2)); + Assert.IsFalse(set2.IsSubsetOf(set1)); + } + + /// + /// Verifies that a set can determine if another set overlaps with it + /// + [Test] + public void CanDetermineOverlap() { + var set1 = new ObservableSet() { 1, 3, 5 }; + var set2 = new HashSet() { 3 }; + + Assert.IsTrue(set1.Overlaps(set2)); + Assert.IsTrue(set2.Overlaps(set1)); + } + + /// + /// Verifies that a set can determine if another set contains the same elements + /// + [Test] + public void CanDetermineSetEquality() { + var set1 = new ObservableSet() { 1, 3, 5 }; + var set2 = new HashSet() { 3, 1, 5 }; + + Assert.IsTrue(set1.SetEquals(set2)); + Assert.IsTrue(set2.SetEquals(set1)); + + set1.Add(7); + + Assert.IsFalse(set1.SetEquals(set2)); + Assert.IsFalse(set2.SetEquals(set1)); + } + + /// + /// Verifies that a set can be symmetrically excepted with another set + /// + [Test] + public void CanBeSymmetricallyExcepted() { + var set1 = new ObservableSet() { 1, 2, 3 }; + var set2 = new HashSet() { 3, 4, 5 }; + + set1.SymmetricExceptWith(set2); + + Assert.AreEqual(4, set1.Count); + } + + /// + /// Verifies that a union of two sets can be built + /// + [Test] + public void CanBeUnioned() { + this.subscriber.Expects.Exactly(3).Method( + (s) => s.ItemAdded(null, null) + ).WithAnyArguments(); + + this.observableSet.Add(1); + this.observableSet.Add(2); + this.observableSet.Add(3); + + var set2 = new ObservableSet() { 3, 4, 5 }; + + this.subscriber.Expects.Exactly(2).Method( + (s) => s.ItemAdded(null, null) + ).WithAnyArguments(); + this.observableSet.UnionWith(set2); + Assert.AreEqual(5, this.observableSet.Count); + } + + /// Creates mock object for the test + private MockFactory mockFactory; + /// Observable set being tested + private ObservableSet observableSet; + /// Subscriber for the observable set's events + private Mock> subscriber; + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST + +#endif // !NO_SETS diff --git a/Source/Collections/ObservableSet.cs b/Source/Collections/ObservableSet.cs index 5d7ae59..777b373 100644 --- a/Source/Collections/ObservableSet.cs +++ b/Source/Collections/ObservableSet.cs @@ -1,340 +1,339 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; -using System.Collections; - -#if !NO_SPECIALIZED_COLLECTIONS -using System.Collections.Specialized; -#endif - -#if !NO_SETS - -namespace Nuclex.Support.Collections { - - /// Set which fires events when items are removed or added to it - /// Type of items to manage in the set - public class ObservableSet : - ISet, - ICollection, -#if !NO_SPECIALIZED_COLLECTIONS - INotifyCollectionChanged, -#endif - IObservableCollection { - - /// Raised when an item has been added to the collection - public event EventHandler> ItemAdded; - /// Raised when an item is removed from the collection - public event EventHandler> ItemRemoved; - /// Raised when an item is replaced in the collection - public event EventHandler> ItemReplaced { - add { } - remove { } - } - /// Raised when the collection is about to be cleared - /// - /// This could be covered by calling ItemRemoved for each item currently - /// contained in the collection, but it is often simpler and more efficient - /// to process the clearing of the entire collection as a special operation. - /// - public event EventHandler Clearing; - /// Raised when the collection has been cleared - public event EventHandler Cleared; - -#if !NO_SPECIALIZED_COLLECTIONS - /// Called when the collection has changed - public event NotifyCollectionChangedEventHandler CollectionChanged; -#endif - - /// Initializes a new observable set based on a hashed set - public ObservableSet() : this(new HashSet()) { } - - /// - /// Initializes a new observable set forwarding operations to the specified set - /// - /// Set operations will be forwarded to - public ObservableSet(ISet set) { - this.set = set; - } - - /// Adds an item to the set - /// Item that will be added to the set - /// - /// True if the element was added, false if it was already contained in the set - /// - public bool Add(TItem item) { - bool wasAdded = this.set.Add(item); - if(wasAdded) { - OnAdded(item); - } - return wasAdded; - } - - /// Removes all elements that are contained in the collection - /// Collection whose elements will be removed from this set - public void ExceptWith(IEnumerable other) { - if(other == this) { - Clear(); - return; - } - - foreach(TItem item in other) { - if(this.set.Remove(item)) { - OnRemoved(item); - } - } - } - - /// - /// Only keeps those elements in this set that are contained in the collection - /// - /// Other set this set will be filtered by - public void IntersectWith(IEnumerable other) { - var otherSet = other as ISet; - if(otherSet == null) { - otherSet = new HashSet(other); - } - - var itemsToRemove = new List(); - foreach(TItem item in this.set) { - if(!otherSet.Contains(item)) { - itemsToRemove.Add(item); - } - } - - for(int index = 0; index < itemsToRemove.Count; ++index) { - this.set.Remove(itemsToRemove[index]); - OnRemoved(itemsToRemove[index]); - } - } - - /// - /// Determines whether the current set is a proper (strict) subset of a collection - /// - /// Collection against which the set will be tested - /// True if the set is a proper subset of the specified collection - public bool IsProperSubsetOf(IEnumerable other) { - return this.set.IsProperSubsetOf(other); - } - - /// - /// Determines whether the current set is a proper (strict) superset of a collection - /// - /// Collection against which the set will be tested - /// True if the set is a proper superset of the specified collection - public bool IsProperSupersetOf(IEnumerable other) { - return this.set.IsProperSupersetOf(other); - } - - /// Determines whether the current set is a subset of a collection - /// Collection against which the set will be tested - /// True if the set is a subset of the specified collection - public bool IsSubsetOf(IEnumerable other) { - return this.set.IsSubsetOf(other); - } - - /// Determines whether the current set is a superset of a collection - /// Collection against which the set will be tested - /// True if the set is a superset of the specified collection - public bool IsSupersetOf(IEnumerable other) { - return this.set.IsSupersetOf(other); - } - - /// - /// Determines if the set shares at least one common element with the collection - /// - /// Collection the set will be tested against - /// - /// True if the set shares at least one common element with the collection - /// - public bool Overlaps(IEnumerable other) { - return this.set.Overlaps(other); - } - - /// - /// Determines whether the set contains the same elements as the specified collection - /// - /// Collection the set will be tested against - /// True if the set contains the same elements as the collection - public bool SetEquals(IEnumerable other) { - return this.set.SetEquals(other); - } - - /// - /// Modifies the current set so that it contains only elements that are present either - /// in the current set or in the specified collection, but not both - /// - /// Collection the set will be excepted with - public void SymmetricExceptWith(IEnumerable other) { - foreach(TItem item in other) { - if(this.set.Remove(item)) { - OnRemoved(item); - } else { - this.Add(item); - OnAdded(item); - } - } - } - - /// - /// Modifies the current set so that it contains all elements that are present in both - /// the current set and in the specified collection - /// - /// Collection an union will be built with - public void UnionWith(IEnumerable other) { - foreach(TItem item in other) { - if(this.set.Add(item)) { - OnAdded(item); - } - } - } - - /// Removes all items from the set - public void Clear() { - OnClearing(); - this.set.Clear(); - OnCleared(); - } - - /// Determines whether the set contains the specified item - /// Item the set will be tested for - /// True if the set contains the specified item - public bool Contains(TItem item) { - return this.set.Contains(item); - } - - /// Copies the contents of the set into an array - /// Array the set's contents will be copied to - /// - /// Index in the array the first copied element will be written to - /// - public void CopyTo(TItem[] array, int arrayIndex) { - this.set.CopyTo(array, arrayIndex); - } - - /// Counts the number of items contained in the set - public int Count { - get { return this.set.Count; } - } - - /// Determines whether the set is readonly - public bool IsReadOnly { - get { return this.set.IsReadOnly; } - } - - /// Removes an item from the set - /// Item that will be removed from the set - /// - /// True if the item was contained in the set and is now removed - /// - public bool Remove(TItem item) { - bool wasRemoved = this.set.Remove(item); - if(wasRemoved) { - OnRemoved(item); - } - return wasRemoved; - } - - /// Creates an enumerator for the set's contents - /// A new enumerator for the sets contents - public IEnumerator GetEnumerator() { - return this.set.GetEnumerator(); - } - - /// Fires the 'ItemAdded' event - /// Item that has been added to the collection - protected virtual void OnAdded(TItem item) { - if(ItemAdded != null) { - ItemAdded(this, new ItemEventArgs(item)); - } -#if !NO_SPECIALIZED_COLLECTIONS - if(CollectionChanged != null) { - CollectionChanged( - this, - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item) - ); - } -#endif - } - - /// Fires the 'ItemRemoved' event - /// Item that has been removed from the collection - protected virtual void OnRemoved(TItem item) { - if(ItemRemoved != null) { - ItemRemoved(this, new ItemEventArgs(item)); - } -#if !NO_SPECIALIZED_COLLECTIONS - if(CollectionChanged != null) { - CollectionChanged( - this, - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item) - ); - } -#endif - } - - /// Fires the 'Clearing' event - protected virtual void OnClearing() { - if(Clearing != null) { - Clearing(this, EventArgs.Empty); - } - } - - /// Fires the 'Cleared' event - protected virtual void OnCleared() { - if(Cleared != null) { - Cleared(this, EventArgs.Empty); - } -#if !NO_SPECIALIZED_COLLECTIONS - if(CollectionChanged != null) { - CollectionChanged(this, Constants.NotifyCollectionResetEventArgs); - } -#endif - } - - #region ICollection implementation - - /// Adds an item to the set - /// Item that will be added to the set - void ICollection.Add(TItem item) { - this.set.Add(item); - } - - #endregion // ICollection implementation - - #region IEnumerable implementation - - /// Creates an enumerator for the set's contents - /// A new enumerator for the sets contents - IEnumerator IEnumerable.GetEnumerator() { - return this.set.GetEnumerator(); - } - - #endregion // IEnumerable implementation - - /// The set being wrapped - private ISet set; - - } - -} // namespace Nuclex.Support.Collections - -#endif // !NO_SETS +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; +using System.Collections; + +#if !NO_SPECIALIZED_COLLECTIONS +using System.Collections.Specialized; +#endif + +#if !NO_SETS + +namespace Nuclex.Support.Collections { + + /// Set which fires events when items are removed or added to it + /// Type of items to manage in the set + public class ObservableSet : + ISet, + ICollection, +#if !NO_SPECIALIZED_COLLECTIONS + INotifyCollectionChanged, +#endif + IObservableCollection { + + /// Raised when an item has been added to the collection + public event EventHandler> ItemAdded; + /// Raised when an item is removed from the collection + public event EventHandler> ItemRemoved; + /// Raised when an item is replaced in the collection + public event EventHandler> ItemReplaced { + add { } + remove { } + } + /// Raised when the collection is about to be cleared + /// + /// This could be covered by calling ItemRemoved for each item currently + /// contained in the collection, but it is often simpler and more efficient + /// to process the clearing of the entire collection as a special operation. + /// + public event EventHandler Clearing; + /// Raised when the collection has been cleared + public event EventHandler Cleared; + +#if !NO_SPECIALIZED_COLLECTIONS + /// Called when the collection has changed + public event NotifyCollectionChangedEventHandler CollectionChanged; +#endif + + /// Initializes a new observable set based on a hashed set + public ObservableSet() : this(new HashSet()) { } + + /// + /// Initializes a new observable set forwarding operations to the specified set + /// + /// Set operations will be forwarded to + public ObservableSet(ISet set) { + this.set = set; + } + + /// Adds an item to the set + /// Item that will be added to the set + /// + /// True if the element was added, false if it was already contained in the set + /// + public bool Add(TItem item) { + bool wasAdded = this.set.Add(item); + if(wasAdded) { + OnAdded(item); + } + return wasAdded; + } + + /// Removes all elements that are contained in the collection + /// Collection whose elements will be removed from this set + public void ExceptWith(IEnumerable other) { + if(other == this) { + Clear(); + return; + } + + foreach(TItem item in other) { + if(this.set.Remove(item)) { + OnRemoved(item); + } + } + } + + /// + /// Only keeps those elements in this set that are contained in the collection + /// + /// Other set this set will be filtered by + public void IntersectWith(IEnumerable other) { + var otherSet = other as ISet; + if(otherSet == null) { + otherSet = new HashSet(other); + } + + var itemsToRemove = new List(); + foreach(TItem item in this.set) { + if(!otherSet.Contains(item)) { + itemsToRemove.Add(item); + } + } + + for(int index = 0; index < itemsToRemove.Count; ++index) { + this.set.Remove(itemsToRemove[index]); + OnRemoved(itemsToRemove[index]); + } + } + + /// + /// Determines whether the current set is a proper (strict) subset of a collection + /// + /// Collection against which the set will be tested + /// True if the set is a proper subset of the specified collection + public bool IsProperSubsetOf(IEnumerable other) { + return this.set.IsProperSubsetOf(other); + } + + /// + /// Determines whether the current set is a proper (strict) superset of a collection + /// + /// Collection against which the set will be tested + /// True if the set is a proper superset of the specified collection + public bool IsProperSupersetOf(IEnumerable other) { + return this.set.IsProperSupersetOf(other); + } + + /// Determines whether the current set is a subset of a collection + /// Collection against which the set will be tested + /// True if the set is a subset of the specified collection + public bool IsSubsetOf(IEnumerable other) { + return this.set.IsSubsetOf(other); + } + + /// Determines whether the current set is a superset of a collection + /// Collection against which the set will be tested + /// True if the set is a superset of the specified collection + public bool IsSupersetOf(IEnumerable other) { + return this.set.IsSupersetOf(other); + } + + /// + /// Determines if the set shares at least one common element with the collection + /// + /// Collection the set will be tested against + /// + /// True if the set shares at least one common element with the collection + /// + public bool Overlaps(IEnumerable other) { + return this.set.Overlaps(other); + } + + /// + /// Determines whether the set contains the same elements as the specified collection + /// + /// Collection the set will be tested against + /// True if the set contains the same elements as the collection + public bool SetEquals(IEnumerable other) { + return this.set.SetEquals(other); + } + + /// + /// Modifies the current set so that it contains only elements that are present either + /// in the current set or in the specified collection, but not both + /// + /// Collection the set will be excepted with + public void SymmetricExceptWith(IEnumerable other) { + foreach(TItem item in other) { + if(this.set.Remove(item)) { + OnRemoved(item); + } else { + this.Add(item); + OnAdded(item); + } + } + } + + /// + /// Modifies the current set so that it contains all elements that are present in both + /// the current set and in the specified collection + /// + /// Collection an union will be built with + public void UnionWith(IEnumerable other) { + foreach(TItem item in other) { + if(this.set.Add(item)) { + OnAdded(item); + } + } + } + + /// Removes all items from the set + public void Clear() { + OnClearing(); + this.set.Clear(); + OnCleared(); + } + + /// Determines whether the set contains the specified item + /// Item the set will be tested for + /// True if the set contains the specified item + public bool Contains(TItem item) { + return this.set.Contains(item); + } + + /// Copies the contents of the set into an array + /// Array the set's contents will be copied to + /// + /// Index in the array the first copied element will be written to + /// + public void CopyTo(TItem[] array, int arrayIndex) { + this.set.CopyTo(array, arrayIndex); + } + + /// Counts the number of items contained in the set + public int Count { + get { return this.set.Count; } + } + + /// Determines whether the set is readonly + public bool IsReadOnly { + get { return this.set.IsReadOnly; } + } + + /// Removes an item from the set + /// Item that will be removed from the set + /// + /// True if the item was contained in the set and is now removed + /// + public bool Remove(TItem item) { + bool wasRemoved = this.set.Remove(item); + if(wasRemoved) { + OnRemoved(item); + } + return wasRemoved; + } + + /// Creates an enumerator for the set's contents + /// A new enumerator for the sets contents + public IEnumerator GetEnumerator() { + return this.set.GetEnumerator(); + } + + /// Fires the 'ItemAdded' event + /// Item that has been added to the collection + protected virtual void OnAdded(TItem item) { + if(ItemAdded != null) { + ItemAdded(this, new ItemEventArgs(item)); + } +#if !NO_SPECIALIZED_COLLECTIONS + if(CollectionChanged != null) { + CollectionChanged( + this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item) + ); + } +#endif + } + + /// Fires the 'ItemRemoved' event + /// Item that has been removed from the collection + protected virtual void OnRemoved(TItem item) { + if(ItemRemoved != null) { + ItemRemoved(this, new ItemEventArgs(item)); + } +#if !NO_SPECIALIZED_COLLECTIONS + if(CollectionChanged != null) { + CollectionChanged( + this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item) + ); + } +#endif + } + + /// Fires the 'Clearing' event + protected virtual void OnClearing() { + if(Clearing != null) { + Clearing(this, EventArgs.Empty); + } + } + + /// Fires the 'Cleared' event + protected virtual void OnCleared() { + if(Cleared != null) { + Cleared(this, EventArgs.Empty); + } +#if !NO_SPECIALIZED_COLLECTIONS + if(CollectionChanged != null) { + CollectionChanged(this, Constants.NotifyCollectionResetEventArgs); + } +#endif + } + + #region ICollection implementation + + /// Adds an item to the set + /// Item that will be added to the set + void ICollection.Add(TItem item) { + this.set.Add(item); + } + + #endregion // ICollection implementation + + #region IEnumerable implementation + + /// Creates an enumerator for the set's contents + /// A new enumerator for the sets contents + IEnumerator IEnumerable.GetEnumerator() { + return this.set.GetEnumerator(); + } + + #endregion // IEnumerable implementation + + /// The set being wrapped + private ISet set; + + } + +} // namespace Nuclex.Support.Collections + +#endif // !NO_SETS diff --git a/Source/Collections/PairPriorityQueue.Test.cs b/Source/Collections/PairPriorityQueue.Test.cs index 3e383c9..20b15d6 100644 --- a/Source/Collections/PairPriorityQueue.Test.cs +++ b/Source/Collections/PairPriorityQueue.Test.cs @@ -1,151 +1,150 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; - -#if UNITTEST - -using NUnit.Framework; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the priority queue class - [TestFixture] - internal class PairPriorityQueueTest { - - /// Tests to ensure the count property is properly updated - [Test] - public void TestCount() { - PairPriorityQueue testQueue = new PairPriorityQueue(); - - Assert.AreEqual(0, testQueue.Count); - testQueue.Enqueue(12.34f, "a"); - Assert.AreEqual(1, testQueue.Count); - testQueue.Enqueue(56.78f, "b"); - Assert.AreEqual(2, testQueue.Count); - testQueue.Dequeue(); - Assert.AreEqual(1, testQueue.Count); - testQueue.Enqueue(9.0f, "c"); - Assert.AreEqual(2, testQueue.Count); - testQueue.Clear(); - Assert.AreEqual(0, testQueue.Count); - } - - /// Tests to ensure that the priority collection actually sorts items - [Test] - public void TestOrdering() { - PairPriorityQueue testQueue = new PairPriorityQueue(); - - testQueue.Enqueue(1.0f, "a"); - testQueue.Enqueue(9.0f, "i"); - testQueue.Enqueue(2.0f, "b"); - testQueue.Enqueue(8.0f, "h"); - testQueue.Enqueue(3.0f, "c"); - testQueue.Enqueue(7.0f, "g"); - testQueue.Enqueue(4.0f, "d"); - testQueue.Enqueue(6.0f, "f"); - testQueue.Enqueue(5.0f, "e"); - - Assert.AreEqual("i", testQueue.Dequeue().Item); - Assert.AreEqual("h", testQueue.Dequeue().Item); - Assert.AreEqual("g", testQueue.Dequeue().Item); - Assert.AreEqual("f", testQueue.Dequeue().Item); - Assert.AreEqual("e", testQueue.Dequeue().Item); - Assert.AreEqual("d", testQueue.Dequeue().Item); - Assert.AreEqual("c", testQueue.Dequeue().Item); - Assert.AreEqual("b", testQueue.Dequeue().Item); - Assert.AreEqual("a", testQueue.Dequeue().Item); - } - - /// Tests to ensure that the priority collection's Peek() method works - [Test] - public void TestPeek() { - PairPriorityQueue testQueue = new PairPriorityQueue(); - - testQueue.Enqueue(1.0f, "a"); - testQueue.Enqueue(2.0f, "b"); - testQueue.Enqueue(0.0f, "c"); - - Assert.AreEqual("b", testQueue.Peek().Item); - } - - /// Tests whether the priority collection can copy itself into an array - [Test] - public void TestCopyTo() { - PairPriorityQueue testQueue = new PairPriorityQueue(); - - testQueue.Enqueue(1.0f, "a"); - testQueue.Enqueue(9.0f, "i"); - testQueue.Enqueue(2.0f, "b"); - testQueue.Enqueue(8.0f, "h"); - testQueue.Enqueue(3.0f, "c"); - testQueue.Enqueue(7.0f, "g"); - testQueue.Enqueue(4.0f, "d"); - testQueue.Enqueue(6.0f, "f"); - testQueue.Enqueue(5.0f, "e"); - - PriorityItemPair[] itemArray = new PriorityItemPair[9]; - testQueue.CopyTo(itemArray, 0); - - CollectionAssert.AreEquivalent(testQueue, itemArray); - } - - /// - /// Tests whether the priority collection provides a synchronization root - /// - [Test] - public void TestSyncRoot() { - PairPriorityQueue testQueue = new PairPriorityQueue(); - - // If IsSynchronized returns true, SyncRoot is allowed to be null - if(!testQueue.IsSynchronized) { - lock(testQueue.SyncRoot) { - testQueue.Clear(); - } - } - } - - /// - /// Tests whether the priority collection provides a working type-safe enumerator - /// - [Test] - public void TestEnumerator() { - PairPriorityQueue testQueue = new PairPriorityQueue(); - - testQueue.Enqueue(1.0f, "a"); - testQueue.Enqueue(2.0f, "b"); - testQueue.Enqueue(0.0f, "c"); - - List> testList = - new List>(); - - foreach(PriorityItemPair entry in testQueue) { - testList.Add(entry); - } - - CollectionAssert.AreEquivalent(testQueue, testList); - } - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; + +#if UNITTEST + +using NUnit.Framework; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the priority queue class + [TestFixture] + internal class PairPriorityQueueTest { + + /// Tests to ensure the count property is properly updated + [Test] + public void TestCount() { + PairPriorityQueue testQueue = new PairPriorityQueue(); + + Assert.AreEqual(0, testQueue.Count); + testQueue.Enqueue(12.34f, "a"); + Assert.AreEqual(1, testQueue.Count); + testQueue.Enqueue(56.78f, "b"); + Assert.AreEqual(2, testQueue.Count); + testQueue.Dequeue(); + Assert.AreEqual(1, testQueue.Count); + testQueue.Enqueue(9.0f, "c"); + Assert.AreEqual(2, testQueue.Count); + testQueue.Clear(); + Assert.AreEqual(0, testQueue.Count); + } + + /// Tests to ensure that the priority collection actually sorts items + [Test] + public void TestOrdering() { + PairPriorityQueue testQueue = new PairPriorityQueue(); + + testQueue.Enqueue(1.0f, "a"); + testQueue.Enqueue(9.0f, "i"); + testQueue.Enqueue(2.0f, "b"); + testQueue.Enqueue(8.0f, "h"); + testQueue.Enqueue(3.0f, "c"); + testQueue.Enqueue(7.0f, "g"); + testQueue.Enqueue(4.0f, "d"); + testQueue.Enqueue(6.0f, "f"); + testQueue.Enqueue(5.0f, "e"); + + Assert.AreEqual("i", testQueue.Dequeue().Item); + Assert.AreEqual("h", testQueue.Dequeue().Item); + Assert.AreEqual("g", testQueue.Dequeue().Item); + Assert.AreEqual("f", testQueue.Dequeue().Item); + Assert.AreEqual("e", testQueue.Dequeue().Item); + Assert.AreEqual("d", testQueue.Dequeue().Item); + Assert.AreEqual("c", testQueue.Dequeue().Item); + Assert.AreEqual("b", testQueue.Dequeue().Item); + Assert.AreEqual("a", testQueue.Dequeue().Item); + } + + /// Tests to ensure that the priority collection's Peek() method works + [Test] + public void TestPeek() { + PairPriorityQueue testQueue = new PairPriorityQueue(); + + testQueue.Enqueue(1.0f, "a"); + testQueue.Enqueue(2.0f, "b"); + testQueue.Enqueue(0.0f, "c"); + + Assert.AreEqual("b", testQueue.Peek().Item); + } + + /// Tests whether the priority collection can copy itself into an array + [Test] + public void TestCopyTo() { + PairPriorityQueue testQueue = new PairPriorityQueue(); + + testQueue.Enqueue(1.0f, "a"); + testQueue.Enqueue(9.0f, "i"); + testQueue.Enqueue(2.0f, "b"); + testQueue.Enqueue(8.0f, "h"); + testQueue.Enqueue(3.0f, "c"); + testQueue.Enqueue(7.0f, "g"); + testQueue.Enqueue(4.0f, "d"); + testQueue.Enqueue(6.0f, "f"); + testQueue.Enqueue(5.0f, "e"); + + PriorityItemPair[] itemArray = new PriorityItemPair[9]; + testQueue.CopyTo(itemArray, 0); + + CollectionAssert.AreEquivalent(testQueue, itemArray); + } + + /// + /// Tests whether the priority collection provides a synchronization root + /// + [Test] + public void TestSyncRoot() { + PairPriorityQueue testQueue = new PairPriorityQueue(); + + // If IsSynchronized returns true, SyncRoot is allowed to be null + if(!testQueue.IsSynchronized) { + lock(testQueue.SyncRoot) { + testQueue.Clear(); + } + } + } + + /// + /// Tests whether the priority collection provides a working type-safe enumerator + /// + [Test] + public void TestEnumerator() { + PairPriorityQueue testQueue = new PairPriorityQueue(); + + testQueue.Enqueue(1.0f, "a"); + testQueue.Enqueue(2.0f, "b"); + testQueue.Enqueue(0.0f, "c"); + + List> testList = + new List>(); + + foreach(PriorityItemPair entry in testQueue) { + testList.Add(entry); + } + + CollectionAssert.AreEquivalent(testQueue, testList); + } + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST diff --git a/Source/Collections/PairPriorityQueue.cs b/Source/Collections/PairPriorityQueue.cs index e99ba32..9a43a89 100644 --- a/Source/Collections/PairPriorityQueue.cs +++ b/Source/Collections/PairPriorityQueue.cs @@ -1,144 +1,143 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; -using System.Collections; - -namespace Nuclex.Support.Collections { - - /// Queue that dequeues items in order of their priority - /// - /// This variant of the priority queue uses an external priority value. If the - /// priority data type implements the IComparable interface, the user does not - /// even - /// - public class PairPriorityQueue : - ICollection, IEnumerable> { - - #region class PairComparer - - /// Compares two priority queue entries based on their priority - private class PairComparer : IComparer> { - - /// Initializes a new entry comparer - /// Comparer used to compare entry priorities - public PairComparer(IComparer priorityComparer) { - this.priorityComparer = priorityComparer; - } - - /// Compares the left entry to the right entry - /// Entry on the left side - /// Entry on the right side - /// The relationship of the two entries - public int Compare( - PriorityItemPair left, - PriorityItemPair right - ) { - return this.priorityComparer.Compare(left.Priority, right.Priority); - } - - /// Comparer used to compare the priorities of the entries - private IComparer priorityComparer; - - } - - #endregion // class EntryComparer - - /// Initializes a new non-intrusive priority queue - public PairPriorityQueue() : this(Comparer.Default) { } - - /// Initializes a new non-intrusive priority queue - /// Comparer used to compare the item priorities - public PairPriorityQueue(IComparer priorityComparer) { - this.internalQueue = new PriorityQueue>( - new PairComparer(priorityComparer) - ); - } - - /// Returns the topmost item in the queue without dequeueing it - /// The topmost item in the queue - public PriorityItemPair Peek() { - return this.internalQueue.Peek(); - } - - /// Takes the item with the highest priority off from the queue - /// The item with the highest priority in the list - public PriorityItemPair Dequeue() { - return this.internalQueue.Dequeue(); - } - - /// Puts an item into the priority queue - /// Priority of the item to be queued - /// Item to be queued - public void Enqueue(TPriority priority, TItem item) { - this.internalQueue.Enqueue( - new PriorityItemPair(priority, item) - ); - } - - /// Removes all items from the priority queue - public void Clear() { - this.internalQueue.Clear(); - } - - /// Total number of items in the priority queue - public int Count { - get { return this.internalQueue.Count; } - } - - /// Copies the contents of the priority queue into an array - /// Array to copy the priority queue into - /// Starting index for the destination array - public void CopyTo(Array array, int index) { - this.internalQueue.CopyTo(array, index); - } - - /// - /// Obtains an object that can be used to synchronize accesses to the priority queue - /// from different threads - /// - public object SyncRoot { - get { return this.internalQueue.SyncRoot; } - } - - /// Whether operations performed on this priority queue are thread safe - public bool IsSynchronized { - get { return this.internalQueue.IsSynchronized; } - } - - /// Returns a typesafe enumerator for the priority queue - /// A new enumerator for the priority queue - public IEnumerator> GetEnumerator() { - return this.internalQueue.GetEnumerator(); - } - - /// Returns an enumerator for the priority queue - /// A new enumerator for the priority queue - IEnumerator IEnumerable.GetEnumerator() { - return this.internalQueue.GetEnumerator(); - } - - /// Intrusive priority queue being wrapped by this class - private PriorityQueue> internalQueue; - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; +using System.Collections; + +namespace Nuclex.Support.Collections { + + /// Queue that dequeues items in order of their priority + /// + /// This variant of the priority queue uses an external priority value. If the + /// priority data type implements the IComparable interface, the user does not + /// even + /// + public class PairPriorityQueue : + ICollection, IEnumerable> { + + #region class PairComparer + + /// Compares two priority queue entries based on their priority + private class PairComparer : IComparer> { + + /// Initializes a new entry comparer + /// Comparer used to compare entry priorities + public PairComparer(IComparer priorityComparer) { + this.priorityComparer = priorityComparer; + } + + /// Compares the left entry to the right entry + /// Entry on the left side + /// Entry on the right side + /// The relationship of the two entries + public int Compare( + PriorityItemPair left, + PriorityItemPair right + ) { + return this.priorityComparer.Compare(left.Priority, right.Priority); + } + + /// Comparer used to compare the priorities of the entries + private IComparer priorityComparer; + + } + + #endregion // class EntryComparer + + /// Initializes a new non-intrusive priority queue + public PairPriorityQueue() : this(Comparer.Default) { } + + /// Initializes a new non-intrusive priority queue + /// Comparer used to compare the item priorities + public PairPriorityQueue(IComparer priorityComparer) { + this.internalQueue = new PriorityQueue>( + new PairComparer(priorityComparer) + ); + } + + /// Returns the topmost item in the queue without dequeueing it + /// The topmost item in the queue + public PriorityItemPair Peek() { + return this.internalQueue.Peek(); + } + + /// Takes the item with the highest priority off from the queue + /// The item with the highest priority in the list + public PriorityItemPair Dequeue() { + return this.internalQueue.Dequeue(); + } + + /// Puts an item into the priority queue + /// Priority of the item to be queued + /// Item to be queued + public void Enqueue(TPriority priority, TItem item) { + this.internalQueue.Enqueue( + new PriorityItemPair(priority, item) + ); + } + + /// Removes all items from the priority queue + public void Clear() { + this.internalQueue.Clear(); + } + + /// Total number of items in the priority queue + public int Count { + get { return this.internalQueue.Count; } + } + + /// Copies the contents of the priority queue into an array + /// Array to copy the priority queue into + /// Starting index for the destination array + public void CopyTo(Array array, int index) { + this.internalQueue.CopyTo(array, index); + } + + /// + /// Obtains an object that can be used to synchronize accesses to the priority queue + /// from different threads + /// + public object SyncRoot { + get { return this.internalQueue.SyncRoot; } + } + + /// Whether operations performed on this priority queue are thread safe + public bool IsSynchronized { + get { return this.internalQueue.IsSynchronized; } + } + + /// Returns a typesafe enumerator for the priority queue + /// A new enumerator for the priority queue + public IEnumerator> GetEnumerator() { + return this.internalQueue.GetEnumerator(); + } + + /// Returns an enumerator for the priority queue + /// A new enumerator for the priority queue + IEnumerator IEnumerable.GetEnumerator() { + return this.internalQueue.GetEnumerator(); + } + + /// Intrusive priority queue being wrapped by this class + private PriorityQueue> internalQueue; + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/Parentable.Test.cs b/Source/Collections/Parentable.Test.cs index 9fc317f..cc6f953 100644 --- a/Source/Collections/Parentable.Test.cs +++ b/Source/Collections/Parentable.Test.cs @@ -1,101 +1,100 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; - -#if UNITTEST - -using NUnit.Framework; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the Parentable class - [TestFixture] - internal class ParentableTest { - - #region class TestParentable - - /// Parentable object that can be the child of an int - private class TestParentable : Parentable { - - /// Initializes a new instance of the parentable test class - public TestParentable() { } - - /// The parent object that owns this instance - public int GetParent() { - return base.Parent; - } - - /// Invoked whenever the instance's owner changes - /// - /// When items are parented for the first time, the oldParent argument will - /// be null. Also, if the element is removed from the collection, the - /// current parent will be null. - /// - /// Previous owner of the instance - protected override void OnParentChanged(int oldParent) { - this.parentChangedCalled = true; - - base.OnParentChanged(oldParent); // to satisfy NCover :-/ - } - - /// Whether the OnParentChanged method has been called - public bool ParentChangedCalled { - get { return this.parentChangedCalled; } - } - - /// Whether the OnParentChanged method has been called - private bool parentChangedCalled; - - } - - #endregion // class TestParentable - - /// - /// Tests whether a parent can be assigned and then retrieved from - /// the parentable object - /// - [Test] - public void TestParentAssignment() { - TestParentable testParentable = new TestParentable(); - - testParentable.SetParent(12345); - Assert.AreEqual(12345, testParentable.GetParent()); - } - - /// - /// Tests whether a parent can be assigned and then retrieved from - /// the parentable object - /// - [Test] - public void TestParentChangedNotification() { - TestParentable testParentable = new TestParentable(); - - testParentable.SetParent(12345); - - Assert.IsTrue(testParentable.ParentChangedCalled); - } - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; + +#if UNITTEST + +using NUnit.Framework; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the Parentable class + [TestFixture] + internal class ParentableTest { + + #region class TestParentable + + /// Parentable object that can be the child of an int + private class TestParentable : Parentable { + + /// Initializes a new instance of the parentable test class + public TestParentable() { } + + /// The parent object that owns this instance + public int GetParent() { + return base.Parent; + } + + /// Invoked whenever the instance's owner changes + /// + /// When items are parented for the first time, the oldParent argument will + /// be null. Also, if the element is removed from the collection, the + /// current parent will be null. + /// + /// Previous owner of the instance + protected override void OnParentChanged(int oldParent) { + this.parentChangedCalled = true; + + base.OnParentChanged(oldParent); // to satisfy NCover :-/ + } + + /// Whether the OnParentChanged method has been called + public bool ParentChangedCalled { + get { return this.parentChangedCalled; } + } + + /// Whether the OnParentChanged method has been called + private bool parentChangedCalled; + + } + + #endregion // class TestParentable + + /// + /// Tests whether a parent can be assigned and then retrieved from + /// the parentable object + /// + [Test] + public void TestParentAssignment() { + TestParentable testParentable = new TestParentable(); + + testParentable.SetParent(12345); + Assert.AreEqual(12345, testParentable.GetParent()); + } + + /// + /// Tests whether a parent can be assigned and then retrieved from + /// the parentable object + /// + [Test] + public void TestParentChangedNotification() { + TestParentable testParentable = new TestParentable(); + + testParentable.SetParent(12345); + + Assert.IsTrue(testParentable.ParentChangedCalled); + } + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST diff --git a/Source/Collections/Parentable.cs b/Source/Collections/Parentable.cs index 7b00196..01bc9d4 100644 --- a/Source/Collections/Parentable.cs +++ b/Source/Collections/Parentable.cs @@ -1,56 +1,55 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; - -namespace Nuclex.Support.Collections { - - /// Base class for objects that can be parented to an owner - /// Type of the parent object - public class Parentable { - - /// The parent object that owns this instance - protected TParent Parent { - get { return this.parent; } - } - - /// Invoked whenever the instance's owner changes - /// - /// When items are parented for the first time, the oldParent argument will - /// be null. Also, if the element is removed from the collection, the - /// current parent will be null. - /// - /// Previous owner of the instance - protected virtual void OnParentChanged(TParent oldParent) { } - - /// Assigns a new parent to this instance - internal void SetParent(TParent parent) { - TParent oldParent = this.parent; - this.parent = parent; - - OnParentChanged(oldParent); - } - - /// Current parent of this object - private TParent parent; - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +namespace Nuclex.Support.Collections { + + /// Base class for objects that can be parented to an owner + /// Type of the parent object + public class Parentable { + + /// The parent object that owns this instance + protected TParent Parent { + get { return this.parent; } + } + + /// Invoked whenever the instance's owner changes + /// + /// When items are parented for the first time, the oldParent argument will + /// be null. Also, if the element is removed from the collection, the + /// current parent will be null. + /// + /// Previous owner of the instance + protected virtual void OnParentChanged(TParent oldParent) { } + + /// Assigns a new parent to this instance + internal void SetParent(TParent parent) { + TParent oldParent = this.parent; + this.parent = parent; + + OnParentChanged(oldParent); + } + + /// Current parent of this object + private TParent parent; + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/ParentingCollection.Test.cs b/Source/Collections/ParentingCollection.Test.cs index 8212aac..abf2ff8 100644 --- a/Source/Collections/ParentingCollection.Test.cs +++ b/Source/Collections/ParentingCollection.Test.cs @@ -1,190 +1,189 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; - -#if UNITTEST - -using NUnit.Framework; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the Parenting Collection class - [TestFixture] - internal class ParentingCollectionTest { - - #region class TestParentable - - /// Parentable object that can be the child of an int - private class TestParentable : Parentable, IDisposable { - - /// Initializes a new instance of the parentable test class - public TestParentable() { } - - /// The parent object that owns this instance - public int GetParent() { - return base.Parent; - } - - /// Immediately releases all resources owned by the item - public void Dispose() { - this.disposeCalled = true; - } - - /// Whether Dispose() has been called on this item - public bool DisposeCalled { - get { return this.disposeCalled; } - } - - /// Whether Dispose() has been called on this item - private bool disposeCalled; - - } - - #endregion // class TestParentable - - #region class TestParentingCollection - - /// Parentable object that can be the child of an int - private class TestParentingCollection : ParentingCollection { - - /// Changes the parent of the collection - /// New parent to assign to the collection - public void SetParent(int parent) { - base.Reparent(parent); - } - - /// Disposes all items contained in the collection - public new void DisposeItems() { - base.DisposeItems(); - } - - } - - #endregion // class TestParentingCollection - - /// - /// Tests whether the parenting collection propagates its parent to an item that - /// is added to the collection after the collection's aprent is already assigned - /// - [Test] - public void TestPropagatePreassignedParent() { - TestParentingCollection testCollection = new TestParentingCollection(); - TestParentable testParentable = new TestParentable(); - - testCollection.SetParent(54321); - testCollection.Add(testParentable); - - Assert.AreEqual(54321, testParentable.GetParent()); - } - - /// - /// Tests whether the parenting collection propagates a new parent to all items - /// contained in it when its parent is changed - /// - [Test] - public void TestPropagateParentChange() { - TestParentingCollection testCollection = new TestParentingCollection(); - TestParentable testParentable = new TestParentable(); - - testCollection.Add(testParentable); - testCollection.SetParent(54321); - - Assert.AreEqual(54321, testParentable.GetParent()); - } - - /// - /// Tests whether the parenting collection propagates its parent to an item that - /// is added to the collection after the collection's aprent is already assigned - /// - [Test] - public void TestPropagateParentOnReplace() { - TestParentingCollection testCollection = new TestParentingCollection(); - TestParentable testParentable1 = new TestParentable(); - TestParentable testParentable2 = new TestParentable(); - - testCollection.SetParent(54321); - testCollection.Add(testParentable1); - testCollection[0] = testParentable2; - - Assert.AreEqual(0, testParentable1.GetParent()); - Assert.AreEqual(54321, testParentable2.GetParent()); - } - - /// - /// Tests whether the parenting collection unsets the parent when an item is removed - /// from the collection - /// - [Test] - public void TestUnsetParentOnRemoveItem() { - TestParentingCollection testCollection = new TestParentingCollection(); - TestParentable testParentable = new TestParentable(); - - testCollection.Add(testParentable); - testCollection.SetParent(54321); - - Assert.AreEqual(54321, testParentable.GetParent()); - - testCollection.RemoveAt(0); - - Assert.AreEqual(0, testParentable.GetParent()); - } - - /// - /// Tests whether the parenting collection unsets the parent when all item are - /// removed from the collection by clearing it - /// - [Test] - public void TestUnsetParentOnClear() { - TestParentingCollection testCollection = new TestParentingCollection(); - TestParentable testParentable = new TestParentable(); - - testCollection.Add(testParentable); - testCollection.SetParent(54321); - - Assert.AreEqual(54321, testParentable.GetParent()); - - testCollection.Clear(); - - Assert.AreEqual(0, testParentable.GetParent()); - } - - /// - /// Tests whether the parenting collection calls Dispose() on all contained items - /// that implement IDisposable when its DisposeItems() method is called - /// - [Test] - public void TestDisposeItems() { - TestParentingCollection testCollection = new TestParentingCollection(); - TestParentable testParentable = new TestParentable(); - - testCollection.Add(testParentable); - - testCollection.DisposeItems(); - - Assert.IsTrue(testParentable.DisposeCalled); - } - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; + +#if UNITTEST + +using NUnit.Framework; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the Parenting Collection class + [TestFixture] + internal class ParentingCollectionTest { + + #region class TestParentable + + /// Parentable object that can be the child of an int + private class TestParentable : Parentable, IDisposable { + + /// Initializes a new instance of the parentable test class + public TestParentable() { } + + /// The parent object that owns this instance + public int GetParent() { + return base.Parent; + } + + /// Immediately releases all resources owned by the item + public void Dispose() { + this.disposeCalled = true; + } + + /// Whether Dispose() has been called on this item + public bool DisposeCalled { + get { return this.disposeCalled; } + } + + /// Whether Dispose() has been called on this item + private bool disposeCalled; + + } + + #endregion // class TestParentable + + #region class TestParentingCollection + + /// Parentable object that can be the child of an int + private class TestParentingCollection : ParentingCollection { + + /// Changes the parent of the collection + /// New parent to assign to the collection + public void SetParent(int parent) { + base.Reparent(parent); + } + + /// Disposes all items contained in the collection + public new void DisposeItems() { + base.DisposeItems(); + } + + } + + #endregion // class TestParentingCollection + + /// + /// Tests whether the parenting collection propagates its parent to an item that + /// is added to the collection after the collection's aprent is already assigned + /// + [Test] + public void TestPropagatePreassignedParent() { + TestParentingCollection testCollection = new TestParentingCollection(); + TestParentable testParentable = new TestParentable(); + + testCollection.SetParent(54321); + testCollection.Add(testParentable); + + Assert.AreEqual(54321, testParentable.GetParent()); + } + + /// + /// Tests whether the parenting collection propagates a new parent to all items + /// contained in it when its parent is changed + /// + [Test] + public void TestPropagateParentChange() { + TestParentingCollection testCollection = new TestParentingCollection(); + TestParentable testParentable = new TestParentable(); + + testCollection.Add(testParentable); + testCollection.SetParent(54321); + + Assert.AreEqual(54321, testParentable.GetParent()); + } + + /// + /// Tests whether the parenting collection propagates its parent to an item that + /// is added to the collection after the collection's aprent is already assigned + /// + [Test] + public void TestPropagateParentOnReplace() { + TestParentingCollection testCollection = new TestParentingCollection(); + TestParentable testParentable1 = new TestParentable(); + TestParentable testParentable2 = new TestParentable(); + + testCollection.SetParent(54321); + testCollection.Add(testParentable1); + testCollection[0] = testParentable2; + + Assert.AreEqual(0, testParentable1.GetParent()); + Assert.AreEqual(54321, testParentable2.GetParent()); + } + + /// + /// Tests whether the parenting collection unsets the parent when an item is removed + /// from the collection + /// + [Test] + public void TestUnsetParentOnRemoveItem() { + TestParentingCollection testCollection = new TestParentingCollection(); + TestParentable testParentable = new TestParentable(); + + testCollection.Add(testParentable); + testCollection.SetParent(54321); + + Assert.AreEqual(54321, testParentable.GetParent()); + + testCollection.RemoveAt(0); + + Assert.AreEqual(0, testParentable.GetParent()); + } + + /// + /// Tests whether the parenting collection unsets the parent when all item are + /// removed from the collection by clearing it + /// + [Test] + public void TestUnsetParentOnClear() { + TestParentingCollection testCollection = new TestParentingCollection(); + TestParentable testParentable = new TestParentable(); + + testCollection.Add(testParentable); + testCollection.SetParent(54321); + + Assert.AreEqual(54321, testParentable.GetParent()); + + testCollection.Clear(); + + Assert.AreEqual(0, testParentable.GetParent()); + } + + /// + /// Tests whether the parenting collection calls Dispose() on all contained items + /// that implement IDisposable when its DisposeItems() method is called + /// + [Test] + public void TestDisposeItems() { + TestParentingCollection testCollection = new TestParentingCollection(); + TestParentable testParentable = new TestParentable(); + + testCollection.Add(testParentable); + + testCollection.DisposeItems(); + + Assert.IsTrue(testParentable.DisposeCalled); + } + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST diff --git a/Source/Collections/ParentingCollection.cs b/Source/Collections/ParentingCollection.cs index dcd4925..77851c5 100644 --- a/Source/Collections/ParentingCollection.cs +++ b/Source/Collections/ParentingCollection.cs @@ -1,115 +1,114 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.ObjectModel; - -namespace Nuclex.Support.Collections { - - /// Collection that automatically assigns an owner to all its elements - /// - /// This collection automatically assigns a parent object to elements that - /// are managed in it. The elements have to derive from the Parentable<> - /// base class. - /// - /// Type of the parent object to assign to items - /// Type of the items being managed in the collection - public class ParentingCollection : Collection - where TItem : Parentable { - - /// Reparents all elements in the collection - /// New parent to take ownership of the items - protected void Reparent(TParent parent) { - this.parent = parent; - - for(int index = 0; index < Count; ++index) - base[index].SetParent(parent); - } - - /// Clears all elements from the collection - protected override void ClearItems() { - for(int index = 0; index < Count; ++index) - base[index].SetParent(default(TParent)); - - base.ClearItems(); - } - - /// Inserts a new element into the collection - /// Index at which to insert the element - /// Item to be inserted - protected override void InsertItem(int index, TItem item) { - base.InsertItem(index, item); - item.SetParent(this.parent); - } - - /// Removes an element from the collection - /// Index of the element to remove - protected override void RemoveItem(int index) { - base[index].SetParent(default(TParent)); - base.RemoveItem(index); - } - - /// Takes over a new element that is directly assigned - /// Index of the element that was assigned - /// New item - protected override void SetItem(int index, TItem item) { - base[index].SetParent(default(TParent)); - base.SetItem(index, item); - item.SetParent(this.parent); - } - - /// Disposes all items contained in the collection - /// - /// - /// This method is intended to support collections that need to dispose their - /// items. It will unparent all of the collection's items and call Dispose() - /// on any item that implements IDisposable. - /// - /// - /// Do not call this method from your destructor as it will access the - /// contained items in order to unparent and to Dispose() them, which leads - /// to undefined behavior since the object might have already been collected - /// by the GC. Call it only if your object is being manually disposed. - /// - /// - protected void DisposeItems() { - - // Dispose all the items in the collection that implement IDisposable, - // starting from the last item in the assumption that this is the fastest - // way to empty a list without causing excessive shiftings in the array. - for(int index = base.Count - 1; index >= 0; --index) { - IDisposable disposable = base[index] as IDisposable; - - // If the item is disposable, destroy it now - if(disposable != null) { - disposable.Dispose(); - } - } - - base.ClearItems(); - - } - - /// Parent this collection currently belongs to - private TParent parent; - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.ObjectModel; + +namespace Nuclex.Support.Collections { + + /// Collection that automatically assigns an owner to all its elements + /// + /// This collection automatically assigns a parent object to elements that + /// are managed in it. The elements have to derive from the Parentable<> + /// base class. + /// + /// Type of the parent object to assign to items + /// Type of the items being managed in the collection + public class ParentingCollection : Collection + where TItem : Parentable { + + /// Reparents all elements in the collection + /// New parent to take ownership of the items + protected void Reparent(TParent parent) { + this.parent = parent; + + for(int index = 0; index < Count; ++index) + base[index].SetParent(parent); + } + + /// Clears all elements from the collection + protected override void ClearItems() { + for(int index = 0; index < Count; ++index) + base[index].SetParent(default(TParent)); + + base.ClearItems(); + } + + /// Inserts a new element into the collection + /// Index at which to insert the element + /// Item to be inserted + protected override void InsertItem(int index, TItem item) { + base.InsertItem(index, item); + item.SetParent(this.parent); + } + + /// Removes an element from the collection + /// Index of the element to remove + protected override void RemoveItem(int index) { + base[index].SetParent(default(TParent)); + base.RemoveItem(index); + } + + /// Takes over a new element that is directly assigned + /// Index of the element that was assigned + /// New item + protected override void SetItem(int index, TItem item) { + base[index].SetParent(default(TParent)); + base.SetItem(index, item); + item.SetParent(this.parent); + } + + /// Disposes all items contained in the collection + /// + /// + /// This method is intended to support collections that need to dispose their + /// items. It will unparent all of the collection's items and call Dispose() + /// on any item that implements IDisposable. + /// + /// + /// Do not call this method from your destructor as it will access the + /// contained items in order to unparent and to Dispose() them, which leads + /// to undefined behavior since the object might have already been collected + /// by the GC. Call it only if your object is being manually disposed. + /// + /// + protected void DisposeItems() { + + // Dispose all the items in the collection that implement IDisposable, + // starting from the last item in the assumption that this is the fastest + // way to empty a list without causing excessive shiftings in the array. + for(int index = base.Count - 1; index >= 0; --index) { + IDisposable disposable = base[index] as IDisposable; + + // If the item is disposable, destroy it now + if(disposable != null) { + disposable.Dispose(); + } + } + + base.ClearItems(); + + } + + /// Parent this collection currently belongs to + private TParent parent; + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/Pool.Test.cs b/Source/Collections/Pool.Test.cs index 2c6346c..d808b82 100644 --- a/Source/Collections/Pool.Test.cs +++ b/Source/Collections/Pool.Test.cs @@ -1,118 +1,117 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.Collections.Generic; - -using NUnit.Framework; - -namespace Nuclex.Support.Collections { - - /// Unit tests for the Pool class - [TestFixture] - internal class PoolTest { - - #region class TestClass - - /// Used to test the pool - private class TestClass : IRecyclable { - - /// Returns the object to its initial state - public void Recycle() { - this.Recycled = true; - } - - /// Whether the instance has been recycled - public bool Recycled; - - } - - #endregion // class TestClass - - #region class NoDefaultConstructor - - /// Used to test the pool - private class NoDefaultConstructor { - - /// Private constructor so no instances can be created - private NoDefaultConstructor() { } - - } - - #endregion // class NoDefaultConstructor - - /// - /// Verifies that the pool can return newly constructed objects - /// - [Test] - public void NewInstancesCanBeObtained() { - Pool pool = new Pool(); - Assert.IsNotNull(pool.Get()); - } - - /// - /// Verifies that an exception is thrown if the pool's default instance creator is used - /// on a type that doesn't have a default constructor - /// - [Test] - public void UsingDefaultInstanceCreatorRequiresDefaultConstructor() { - Assert.Throws( - delegate() { new Pool(); } - ); - } - - /// - /// Tests whether the pool can redeem objects that are no longer used - /// - [Test] - public void InstancesCanBeRedeemed() { - Pool pool = new Pool(); - pool.Redeem(new TestClass()); - } - - /// - /// Tests whether the Recycle() method is called at the appropriate time - /// - [Test] - public void RedeemedItemsWillBeRecycled() { - Pool pool = new Pool(); - TestClass x = new TestClass(); - - Assert.IsFalse(x.Recycled); - pool.Redeem(x); - Assert.IsTrue(x.Recycled); - } - - /// Verifies that the pool's Capacity is applied correctly - [Test] - public void PoolCapacityCanBeAdjusted() { - Pool pool = new Pool(123); - Assert.AreEqual(123, pool.Capacity); - pool.Capacity = 321; - Assert.AreEqual(321, pool.Capacity); - } - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.Collections.Generic; + +using NUnit.Framework; + +namespace Nuclex.Support.Collections { + + /// Unit tests for the Pool class + [TestFixture] + internal class PoolTest { + + #region class TestClass + + /// Used to test the pool + private class TestClass : IRecyclable { + + /// Returns the object to its initial state + public void Recycle() { + this.Recycled = true; + } + + /// Whether the instance has been recycled + public bool Recycled; + + } + + #endregion // class TestClass + + #region class NoDefaultConstructor + + /// Used to test the pool + private class NoDefaultConstructor { + + /// Private constructor so no instances can be created + private NoDefaultConstructor() { } + + } + + #endregion // class NoDefaultConstructor + + /// + /// Verifies that the pool can return newly constructed objects + /// + [Test] + public void NewInstancesCanBeObtained() { + Pool pool = new Pool(); + Assert.IsNotNull(pool.Get()); + } + + /// + /// Verifies that an exception is thrown if the pool's default instance creator is used + /// on a type that doesn't have a default constructor + /// + [Test] + public void UsingDefaultInstanceCreatorRequiresDefaultConstructor() { + Assert.Throws( + delegate() { new Pool(); } + ); + } + + /// + /// Tests whether the pool can redeem objects that are no longer used + /// + [Test] + public void InstancesCanBeRedeemed() { + Pool pool = new Pool(); + pool.Redeem(new TestClass()); + } + + /// + /// Tests whether the Recycle() method is called at the appropriate time + /// + [Test] + public void RedeemedItemsWillBeRecycled() { + Pool pool = new Pool(); + TestClass x = new TestClass(); + + Assert.IsFalse(x.Recycled); + pool.Redeem(x); + Assert.IsTrue(x.Recycled); + } + + /// Verifies that the pool's Capacity is applied correctly + [Test] + public void PoolCapacityCanBeAdjusted() { + Pool pool = new Pool(123); + Assert.AreEqual(123, pool.Capacity); + pool.Capacity = 321; + Assert.AreEqual(321, pool.Capacity); + } + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST diff --git a/Source/Collections/Pool.cs b/Source/Collections/Pool.cs index 79d0ecc..1404cc2 100644 --- a/Source/Collections/Pool.cs +++ b/Source/Collections/Pool.cs @@ -1,175 +1,174 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; - -namespace Nuclex.Support.Collections { - - /// Pool that recycles objects in order to avoid garbage build-up - /// Type of objects being pooled - /// - /// - /// Use this class to recycle objects instead of letting them become garbage, - /// creating new instances each time. The Pool class is designed to either be - /// used on its own or as a building block for a static class that wraps it. - /// - /// - /// Special care has to be taken to revert the entire state of a recycled - /// object when it is returned to the pool. For example, events will need to - /// have their subscriber lists emptied to avoid sending out events to the - /// wrong subscribers and accumulating more and more subscribers each time - /// they are reused. - /// - /// - /// To simplify such cleanup, pooled objects can implement the IRecyclable - /// interface. When an object is returned to the pool, the pool will - /// automatically call its IRecyclable.Recycle() method. - /// - /// - public class Pool { - - /// Default number of recyclable objects the pool will store - public const int DefaultPoolSize = 64; - - /// Initializes a new pool using the default capacity - public Pool() : this(DefaultPoolSize, null, null) { } - - /// Initializes a new pool using the default capacity - /// Delegate that will be used to create new items - public Pool(Func createNewDelegate) : - this(DefaultPoolSize, createNewDelegate, null) { } - - /// Initializes a new pool using the default capacity - /// Delegate that will be used to create new items - /// Delegate that will be used to recycle items - public Pool(Func createNewDelegate, Action recycleDelegate) : - this(DefaultPoolSize, createNewDelegate, recycleDelegate) { } - - /// Initializes a new pool using a user-specified capacity - /// Capacity of the pool - public Pool(int capacity) : - this(capacity, null, null) { } - - /// Initializes a new pool using a user-specified capacity - /// Capacity of the pool - /// Delegate that will be used to create new items - public Pool(int capacity, Func createNewDelegate) : - this(capacity, createNewDelegate, null) { } - - /// Initializes a new pool using a user-specified capacity - /// Capacity of the pool - /// Delegate that will be used to create new items - /// Delegate that will be used to recycle items - public Pool(int capacity, Func createNewDelegate, Action recycleDelegate) { - Capacity = capacity; - - if(createNewDelegate == null) { - if(!typeof(TItem).HasDefaultConstructor()) { - throw new ArgumentException( - "Type " + typeof(TItem).Name + " has no default constructor and " + - "requires a custom 'create instance' delegate" - ); - } - createNewDelegate = new Func(Activator.CreateInstance); - } - if(recycleDelegate == null) { - recycleDelegate = new Action(callRecycleIfSupported); - } - - this.createNewDelegate = createNewDelegate; - this.recycleDelegate = recycleDelegate; - } - - /// - /// Returns a new or recycled instance of the types managed by the pool - /// - /// A new or recycled instance - public TItem Get() { - lock(this) { - if(this.items.Count > 0) { - return this.items.Dequeue(); - } else { - return this.createNewDelegate(); - } - } - } - - /// - /// Redeems an instance that is no longer used to be recycled by the pool - /// - /// The instance that will be redeemed - public void Redeem(TItem item) { - - // Call Recycle() when the object is redeemed (instead of when it leaves - // the pool again) in order to eliminate any references the object may hold - // to other objects. - this.recycleDelegate(item); - - lock(this) { - if(this.items.Count < this.capacity) { - this.items.Enqueue(item); - } - } - - } - - /// Number of objects the pool can retain - /// - /// Changing this value causes the pool to be emtpied. It is recommended that - /// you only read the pool's capacity, never change it. - /// - public int Capacity { - get { return this.capacity; } - set { - this.capacity = value; - this.items = new Queue(value); - } - } - - /// - /// Calls the Recycle() method on an objects if it implements - /// the IRecyclable interface - /// - /// - /// Object whose Recycle() method will be called if supported by the object - /// - private static void callRecycleIfSupported(TItem item) { - IRecyclable recycleable = item as IRecyclable; - if(recycleable != null) { - recycleable.Recycle(); - } - } - - /// Objects being retained for recycling - private Queue items; - /// Capacity of the pool - /// - /// Required because the Queue class doesn't allow this value to be retrieved - /// - private int capacity; - /// Delegate used to create new instances of the pool's type - private Func createNewDelegate; - /// Delegate used to recycle instances - private Action recycleDelegate; - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; + +namespace Nuclex.Support.Collections { + + /// Pool that recycles objects in order to avoid garbage build-up + /// Type of objects being pooled + /// + /// + /// Use this class to recycle objects instead of letting them become garbage, + /// creating new instances each time. The Pool class is designed to either be + /// used on its own or as a building block for a static class that wraps it. + /// + /// + /// Special care has to be taken to revert the entire state of a recycled + /// object when it is returned to the pool. For example, events will need to + /// have their subscriber lists emptied to avoid sending out events to the + /// wrong subscribers and accumulating more and more subscribers each time + /// they are reused. + /// + /// + /// To simplify such cleanup, pooled objects can implement the IRecyclable + /// interface. When an object is returned to the pool, the pool will + /// automatically call its IRecyclable.Recycle() method. + /// + /// + public class Pool { + + /// Default number of recyclable objects the pool will store + public const int DefaultPoolSize = 64; + + /// Initializes a new pool using the default capacity + public Pool() : this(DefaultPoolSize, null, null) { } + + /// Initializes a new pool using the default capacity + /// Delegate that will be used to create new items + public Pool(Func createNewDelegate) : + this(DefaultPoolSize, createNewDelegate, null) { } + + /// Initializes a new pool using the default capacity + /// Delegate that will be used to create new items + /// Delegate that will be used to recycle items + public Pool(Func createNewDelegate, Action recycleDelegate) : + this(DefaultPoolSize, createNewDelegate, recycleDelegate) { } + + /// Initializes a new pool using a user-specified capacity + /// Capacity of the pool + public Pool(int capacity) : + this(capacity, null, null) { } + + /// Initializes a new pool using a user-specified capacity + /// Capacity of the pool + /// Delegate that will be used to create new items + public Pool(int capacity, Func createNewDelegate) : + this(capacity, createNewDelegate, null) { } + + /// Initializes a new pool using a user-specified capacity + /// Capacity of the pool + /// Delegate that will be used to create new items + /// Delegate that will be used to recycle items + public Pool(int capacity, Func createNewDelegate, Action recycleDelegate) { + Capacity = capacity; + + if(createNewDelegate == null) { + if(!typeof(TItem).HasDefaultConstructor()) { + throw new ArgumentException( + "Type " + typeof(TItem).Name + " has no default constructor and " + + "requires a custom 'create instance' delegate" + ); + } + createNewDelegate = new Func(Activator.CreateInstance); + } + if(recycleDelegate == null) { + recycleDelegate = new Action(callRecycleIfSupported); + } + + this.createNewDelegate = createNewDelegate; + this.recycleDelegate = recycleDelegate; + } + + /// + /// Returns a new or recycled instance of the types managed by the pool + /// + /// A new or recycled instance + public TItem Get() { + lock(this) { + if(this.items.Count > 0) { + return this.items.Dequeue(); + } else { + return this.createNewDelegate(); + } + } + } + + /// + /// Redeems an instance that is no longer used to be recycled by the pool + /// + /// The instance that will be redeemed + public void Redeem(TItem item) { + + // Call Recycle() when the object is redeemed (instead of when it leaves + // the pool again) in order to eliminate any references the object may hold + // to other objects. + this.recycleDelegate(item); + + lock(this) { + if(this.items.Count < this.capacity) { + this.items.Enqueue(item); + } + } + + } + + /// Number of objects the pool can retain + /// + /// Changing this value causes the pool to be emtpied. It is recommended that + /// you only read the pool's capacity, never change it. + /// + public int Capacity { + get { return this.capacity; } + set { + this.capacity = value; + this.items = new Queue(value); + } + } + + /// + /// Calls the Recycle() method on an objects if it implements + /// the IRecyclable interface + /// + /// + /// Object whose Recycle() method will be called if supported by the object + /// + private static void callRecycleIfSupported(TItem item) { + IRecyclable recycleable = item as IRecyclable; + if(recycleable != null) { + recycleable.Recycle(); + } + } + + /// Objects being retained for recycling + private Queue items; + /// Capacity of the pool + /// + /// Required because the Queue class doesn't allow this value to be retrieved + /// + private int capacity; + /// Delegate used to create new instances of the pool's type + private Func createNewDelegate; + /// Delegate used to recycle instances + private Action recycleDelegate; + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/PriorityItemPair.Test.cs b/Source/Collections/PriorityItemPair.Test.cs index 4726816..d41b980 100644 --- a/Source/Collections/PriorityItemPair.Test.cs +++ b/Source/Collections/PriorityItemPair.Test.cs @@ -1,100 +1,99 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.Collections.Generic; - -using NUnit.Framework; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the Priority/Item pair class - [TestFixture] - internal class PriorityItemPairTest { - - #region class ToStringNullReturner - - /// Test class in which ToString() can return null - private class ToStringNullReturner { - - /// - /// Returns a System.String that represents the current System.Object - /// - /// A System.String that represents the current System.Object - public override string ToString() { return null; } - - } - - #endregion // class ToStringNullReturner - - /// Tests whether the pair's default constructor works - [Test] - public void TestDefaultConstructor() { - new PriorityItemPair(); - } - - /// Tests whether the priority can be retrieved from the pair - [Test] - public void TestPriorityRetrieval() { - PriorityItemPair testPair = new PriorityItemPair( - 12345, "hello world" - ); - - Assert.AreEqual(12345, testPair.Priority); - } - - /// Tests whether the item can be retrieved from the pair - [Test] - public void TestItemRetrieval() { - PriorityItemPair testPair = new PriorityItemPair( - 12345, "hello world" - ); - - Assert.AreEqual("hello world", testPair.Item); - } - - /// Tests whether the ToString() methods works with valid strings - [Test] - public void TestToStringWithValidStrings() { - PriorityItemPair testPair = new PriorityItemPair( - "hello", "world" - ); - - Assert.AreEqual("[hello, world]", testPair.ToString()); - } - - /// Tests whether the ToString() methods works with null strings - [Test] - public void TestToStringWithNullStrings() { - PriorityItemPair testPair = - new PriorityItemPair( - new ToStringNullReturner(), new ToStringNullReturner() - ); - - Assert.AreEqual("[, ]", testPair.ToString()); - } - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.Collections.Generic; + +using NUnit.Framework; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the Priority/Item pair class + [TestFixture] + internal class PriorityItemPairTest { + + #region class ToStringNullReturner + + /// Test class in which ToString() can return null + private class ToStringNullReturner { + + /// + /// Returns a System.String that represents the current System.Object + /// + /// A System.String that represents the current System.Object + public override string ToString() { return null; } + + } + + #endregion // class ToStringNullReturner + + /// Tests whether the pair's default constructor works + [Test] + public void TestDefaultConstructor() { + new PriorityItemPair(); + } + + /// Tests whether the priority can be retrieved from the pair + [Test] + public void TestPriorityRetrieval() { + PriorityItemPair testPair = new PriorityItemPair( + 12345, "hello world" + ); + + Assert.AreEqual(12345, testPair.Priority); + } + + /// Tests whether the item can be retrieved from the pair + [Test] + public void TestItemRetrieval() { + PriorityItemPair testPair = new PriorityItemPair( + 12345, "hello world" + ); + + Assert.AreEqual("hello world", testPair.Item); + } + + /// Tests whether the ToString() methods works with valid strings + [Test] + public void TestToStringWithValidStrings() { + PriorityItemPair testPair = new PriorityItemPair( + "hello", "world" + ); + + Assert.AreEqual("[hello, world]", testPair.ToString()); + } + + /// Tests whether the ToString() methods works with null strings + [Test] + public void TestToStringWithNullStrings() { + PriorityItemPair testPair = + new PriorityItemPair( + new ToStringNullReturner(), new ToStringNullReturner() + ); + + Assert.AreEqual("[, ]", testPair.ToString()); + } + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST diff --git a/Source/Collections/PriorityItemPair.cs b/Source/Collections/PriorityItemPair.cs index b31d1ca..d430d76 100644 --- a/Source/Collections/PriorityItemPair.cs +++ b/Source/Collections/PriorityItemPair.cs @@ -1,75 +1,74 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Text; - -namespace Nuclex.Support.Collections { - - /// An pair of a priority and an item - public struct PriorityItemPair { - - /// Initializes a new priority / item pair - /// Priority of the item in the pair - /// Item to be stored in the pair - public PriorityItemPair(TPriority priority, TItem item) { - this.Priority = priority; - this.Item = item; - } - - /// Priority assigned to this priority / item pair - public TPriority Priority; - /// Item contained in this priority / item pair - public TItem Item; - - /// Converts the priority / item pair into a string - /// A string describing the priority / item pair - public override string ToString() { - int length = 4; - - // Convert the priority value into a string or use the empty string - // constant if the ToString() overload returns null - string priorityString = this.Priority.ToString(); - if(priorityString != null) - length += priorityString.Length; - else - priorityString = string.Empty; - - // Convert the item value into a string or use the empty string - // constant if the ToString() overload returns null - string itemString = this.Item.ToString(); - if(itemString != null) - length += itemString.Length; - else - itemString = string.Empty; - - // Concatenate priority and item into a single string - StringBuilder builder = new StringBuilder(length); - builder.Append('['); - builder.Append(priorityString); - builder.Append(", "); - builder.Append(itemString); - builder.Append(']'); - return builder.ToString(); - } - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Text; + +namespace Nuclex.Support.Collections { + + /// An pair of a priority and an item + public struct PriorityItemPair { + + /// Initializes a new priority / item pair + /// Priority of the item in the pair + /// Item to be stored in the pair + public PriorityItemPair(TPriority priority, TItem item) { + this.Priority = priority; + this.Item = item; + } + + /// Priority assigned to this priority / item pair + public TPriority Priority; + /// Item contained in this priority / item pair + public TItem Item; + + /// Converts the priority / item pair into a string + /// A string describing the priority / item pair + public override string ToString() { + int length = 4; + + // Convert the priority value into a string or use the empty string + // constant if the ToString() overload returns null + string priorityString = this.Priority.ToString(); + if(priorityString != null) + length += priorityString.Length; + else + priorityString = string.Empty; + + // Convert the item value into a string or use the empty string + // constant if the ToString() overload returns null + string itemString = this.Item.ToString(); + if(itemString != null) + length += itemString.Length; + else + itemString = string.Empty; + + // Concatenate priority and item into a single string + StringBuilder builder = new StringBuilder(length); + builder.Append('['); + builder.Append(priorityString); + builder.Append(", "); + builder.Append(itemString); + builder.Append(']'); + return builder.ToString(); + } + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/PriorityQueue.Test.cs b/Source/Collections/PriorityQueue.Test.cs index 2b18351..241c8c2 100644 --- a/Source/Collections/PriorityQueue.Test.cs +++ b/Source/Collections/PriorityQueue.Test.cs @@ -1,158 +1,157 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; - -#if UNITTEST - -using NUnit.Framework; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the priority queue class - [TestFixture] - internal class PriorityQueueTest { - - #region class FloatComparer - - /// Comparer for two floating point values - private class FloatComparer : IComparer { - - /// The default instance of this comparer - public static readonly FloatComparer Default = new FloatComparer(); - - /// Compares two floating points against each other - /// First float to compare - /// Second float to compare - /// The relationship of the two floats to each other - public int Compare(float left, float right) { - return Math.Sign(left - right); - } - - } - - #endregion // class FloatComparer - - /// Tests to ensure the count property is properly updated - [Test] - public void TestCount() { - PriorityQueue testQueue = new PriorityQueue(FloatComparer.Default); - - Assert.AreEqual(0, testQueue.Count); - testQueue.Enqueue(12.34f); - Assert.AreEqual(1, testQueue.Count); - testQueue.Enqueue(56.78f); - Assert.AreEqual(2, testQueue.Count); - testQueue.Dequeue(); - Assert.AreEqual(1, testQueue.Count); - testQueue.Enqueue(9.0f); - Assert.AreEqual(2, testQueue.Count); - testQueue.Clear(); - Assert.AreEqual(0, testQueue.Count); - } - - /// Tests to ensure that the priority collection actually sorts items - [Test] - public void TestOrdering() { - PriorityQueue testQueue = new PriorityQueue(FloatComparer.Default); - - testQueue.Enqueue(1.0f); - testQueue.Enqueue(9.0f); - testQueue.Enqueue(2.0f); - testQueue.Enqueue(8.0f); - testQueue.Enqueue(3.0f); - testQueue.Enqueue(7.0f); - testQueue.Enqueue(4.0f); - testQueue.Enqueue(6.0f); - testQueue.Enqueue(5.0f); - - Assert.AreEqual(9.0f, testQueue.Dequeue()); - Assert.AreEqual(8.0f, testQueue.Dequeue()); - Assert.AreEqual(7.0f, testQueue.Dequeue()); - Assert.AreEqual(6.0f, testQueue.Dequeue()); - Assert.AreEqual(5.0f, testQueue.Dequeue()); - Assert.AreEqual(4.0f, testQueue.Dequeue()); - Assert.AreEqual(3.0f, testQueue.Dequeue()); - Assert.AreEqual(2.0f, testQueue.Dequeue()); - Assert.AreEqual(1.0f, testQueue.Dequeue()); - } - -#if DEBUG - /// - /// Tests whether the priority queue's enumerators are invalidated when the queue's - /// contents are modified - /// - [Test] - public void TestEnumeratorInvalidationOnModify() { - PriorityQueue testQueue = new PriorityQueue(); - IEnumerator testQueueEnumerator = testQueue.GetEnumerator(); - - testQueue.Enqueue(123); - - Assert.Throws( - delegate() { testQueueEnumerator.MoveNext(); } - ); - } -#endif - - /// - /// Verifies that an exception is thrown when Peek() is called on an empty queue - /// - [Test] - public void TestPeekEmptyQueue() { - PriorityQueue testQueue = new PriorityQueue(); - Assert.Throws( - delegate() { testQueue.Peek(); } - ); - } - - /// - /// Verifies that an exception is thrown when Dequeue() is called on an empty queue - /// - [Test] - public void TestDequeueEmptyQueue() { - PriorityQueue testQueue = new PriorityQueue(); - Assert.Throws( - delegate() { testQueue.Dequeue(); } - ); - } - - /// - /// Verifies that the priority queue can handle large amounts of data - /// - [Test] - public void TestLargeQueue() { - PriorityQueue testQueue = new PriorityQueue(); - List testList = new List(); - - for(int index = 0; index < 1000; ++index) { - testQueue.Enqueue(index * 2); - testList.Add(index * 2); - } - - CollectionAssert.AreEquivalent(testList, testQueue); - } - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; + +#if UNITTEST + +using NUnit.Framework; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the priority queue class + [TestFixture] + internal class PriorityQueueTest { + + #region class FloatComparer + + /// Comparer for two floating point values + private class FloatComparer : IComparer { + + /// The default instance of this comparer + public static readonly FloatComparer Default = new FloatComparer(); + + /// Compares two floating points against each other + /// First float to compare + /// Second float to compare + /// The relationship of the two floats to each other + public int Compare(float left, float right) { + return Math.Sign(left - right); + } + + } + + #endregion // class FloatComparer + + /// Tests to ensure the count property is properly updated + [Test] + public void TestCount() { + PriorityQueue testQueue = new PriorityQueue(FloatComparer.Default); + + Assert.AreEqual(0, testQueue.Count); + testQueue.Enqueue(12.34f); + Assert.AreEqual(1, testQueue.Count); + testQueue.Enqueue(56.78f); + Assert.AreEqual(2, testQueue.Count); + testQueue.Dequeue(); + Assert.AreEqual(1, testQueue.Count); + testQueue.Enqueue(9.0f); + Assert.AreEqual(2, testQueue.Count); + testQueue.Clear(); + Assert.AreEqual(0, testQueue.Count); + } + + /// Tests to ensure that the priority collection actually sorts items + [Test] + public void TestOrdering() { + PriorityQueue testQueue = new PriorityQueue(FloatComparer.Default); + + testQueue.Enqueue(1.0f); + testQueue.Enqueue(9.0f); + testQueue.Enqueue(2.0f); + testQueue.Enqueue(8.0f); + testQueue.Enqueue(3.0f); + testQueue.Enqueue(7.0f); + testQueue.Enqueue(4.0f); + testQueue.Enqueue(6.0f); + testQueue.Enqueue(5.0f); + + Assert.AreEqual(9.0f, testQueue.Dequeue()); + Assert.AreEqual(8.0f, testQueue.Dequeue()); + Assert.AreEqual(7.0f, testQueue.Dequeue()); + Assert.AreEqual(6.0f, testQueue.Dequeue()); + Assert.AreEqual(5.0f, testQueue.Dequeue()); + Assert.AreEqual(4.0f, testQueue.Dequeue()); + Assert.AreEqual(3.0f, testQueue.Dequeue()); + Assert.AreEqual(2.0f, testQueue.Dequeue()); + Assert.AreEqual(1.0f, testQueue.Dequeue()); + } + +#if DEBUG + /// + /// Tests whether the priority queue's enumerators are invalidated when the queue's + /// contents are modified + /// + [Test] + public void TestEnumeratorInvalidationOnModify() { + PriorityQueue testQueue = new PriorityQueue(); + IEnumerator testQueueEnumerator = testQueue.GetEnumerator(); + + testQueue.Enqueue(123); + + Assert.Throws( + delegate() { testQueueEnumerator.MoveNext(); } + ); + } +#endif + + /// + /// Verifies that an exception is thrown when Peek() is called on an empty queue + /// + [Test] + public void TestPeekEmptyQueue() { + PriorityQueue testQueue = new PriorityQueue(); + Assert.Throws( + delegate() { testQueue.Peek(); } + ); + } + + /// + /// Verifies that an exception is thrown when Dequeue() is called on an empty queue + /// + [Test] + public void TestDequeueEmptyQueue() { + PriorityQueue testQueue = new PriorityQueue(); + Assert.Throws( + delegate() { testQueue.Dequeue(); } + ); + } + + /// + /// Verifies that the priority queue can handle large amounts of data + /// + [Test] + public void TestLargeQueue() { + PriorityQueue testQueue = new PriorityQueue(); + List testList = new List(); + + for(int index = 0; index < 1000; ++index) { + testQueue.Enqueue(index * 2); + testList.Add(index * 2); + } + + CollectionAssert.AreEquivalent(testList, testQueue); + } + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST diff --git a/Source/Collections/PriorityQueue.cs b/Source/Collections/PriorityQueue.cs index 696ac53..ac5cf31 100644 --- a/Source/Collections/PriorityQueue.cs +++ b/Source/Collections/PriorityQueue.cs @@ -1,285 +1,284 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; -using System.Collections; - -namespace Nuclex.Support.Collections { - - /// Queue that dequeues items in order of their priority - public class PriorityQueue : ICollection, IEnumerable { - - #region class Enumerator - - /// Enumerates all items contained in a priority queue - private class Enumerator : IEnumerator { - - /// Initializes a new priority queue enumerator - /// Priority queue to be enumerated - public Enumerator(PriorityQueue priorityQueue) { - this.priorityQueue = priorityQueue; - Reset(); - } - - /// Resets the enumerator to its initial state - public void Reset() { - this.index = -1; -#if DEBUG - this.expectedVersion = this.priorityQueue.version; -#endif - } - - /// The current item being enumerated - TItem IEnumerator.Current { - get { -#if DEBUG - checkVersion(); -#endif - return this.priorityQueue.heap[index]; - } - } - - /// Moves to the next item in the priority queue - /// True if a next item was found, false if the end has been reached - public bool MoveNext() { -#if DEBUG - checkVersion(); -#endif - if(this.index + 1 == this.priorityQueue.count) - return false; - - ++this.index; - - return true; - } - - /// Releases all resources used by the enumerator - public void Dispose() { } - -#if DEBUG - /// Ensures that the priority queue has not changed - private void checkVersion() { - if(this.expectedVersion != this.priorityQueue.version) - throw new InvalidOperationException("Priority queue has been modified"); - } -#endif - - /// The current item being enumerated - object IEnumerator.Current { - get { -#if DEBUG - checkVersion(); -#endif - return this.priorityQueue.heap[index]; - } - } - - /// Index of the current item in the priority queue - private int index; - /// The priority queue whose items this instance enumerates - private PriorityQueue priorityQueue; -#if DEBUG - /// Expected version of the priority queue - private int expectedVersion; -#endif - - } - - #endregion // class Enumerator - - /// - /// Initializes a new priority queue using IComparable for comparing items - /// - public PriorityQueue() : this(Comparer.Default) { } - - /// Initializes a new priority queue - /// Comparer to use for ordering the items - public PriorityQueue(IComparer comparer) { - this.comparer = comparer; - this.capacity = 15; // 15 is equal to 4 complete levels - this.heap = new TItem[this.capacity]; - } - - /// Returns the topmost item in the queue without dequeueing it - /// The topmost item in the queue - public TItem Peek() { - if(this.count == 0) { - throw new InvalidOperationException("No items queued"); - } - - return this.heap[0]; - } - - /// Takes the item with the highest priority off from the queue - /// The item with the highest priority in the list - /// When the queue is empty - public TItem Dequeue() { - if(this.count == 0) { - throw new InvalidOperationException("No items available to dequeue"); - } - - TItem result = this.heap[0]; - --this.count; - trickleDown(0, this.heap[this.count]); -#if DEBUG - ++this.version; -#endif - return result; - } - - /// Puts an item into the priority queue - /// Item to be queued - public void Enqueue(TItem item) { - if(this.count == capacity) - growHeap(); - - ++this.count; - bubbleUp(this.count - 1, item); -#if DEBUG - ++this.version; -#endif - } - - /// Removes all items from the priority queue - public void Clear() { - this.count = 0; -#if DEBUG - ++this.version; -#endif - } - - - /// Total number of items in the priority queue - public int Count { - get { return this.count; } - } - - /// Copies the contents of the priority queue into an array - /// Array to copy the priority queue into - /// Starting index for the destination array - public void CopyTo(Array array, int index) { - Array.Copy(this.heap, 0, array, index, this.count); - } - - /// - /// Obtains an object that can be used to synchronize accesses to the priority queue - /// from different threads - /// - public object SyncRoot { - get { return this; } - } - - /// Whether operations performed on this priority queue are thread safe - public bool IsSynchronized { - get { return false; } - } - - /// Returns a typesafe enumerator for the priority queue - /// A new enumerator for the priority queue - public IEnumerator GetEnumerator() { - return new Enumerator(this); - } - - /// Moves an item upwards in the heap tree - /// Index of the item to be moved - /// Item to be moved - private void bubbleUp(int index, TItem item) { - int parent = getParent(index); - - // Note: (index > 0) means there is a parent - while((index > 0) && (this.comparer.Compare(this.heap[parent], item) < 0)) { - this.heap[index] = this.heap[parent]; - index = parent; - parent = getParent(index); - } - - this.heap[index] = item; - } - - /// Move the item downwards in the heap tree - /// Index of the item to be moved - /// Item to be moved - private void trickleDown(int index, TItem item) { - int child = getLeftChild(index); - - while(child < this.count) { - - bool needsToBeMoved = - ((child + 1) < this.count) && - (this.comparer.Compare(heap[child], this.heap[child + 1]) < 0); - - if(needsToBeMoved) - ++child; - - this.heap[index] = this.heap[child]; - index = child; - child = getLeftChild(index); - - } - - bubbleUp(index, item); - } - - /// Obtains the left child item in the heap tree - /// Index of the item whose left child to return - /// The left child item of the provided parent item - private int getLeftChild(int index) { - return (index * 2) + 1; - } - - /// Calculates the parent entry of the item on the heap - /// Index of the item whose parent to calculate - /// The index of the parent to the specified item - private int getParent(int index) { - return (index - 1) / 2; - } - - /// Increases the size of the priority collection's heap - private void growHeap() { - this.capacity = (capacity * 2) + 1; - - TItem[] newHeap = new TItem[this.capacity]; - Array.Copy(this.heap, 0, newHeap, 0, this.count); - this.heap = newHeap; - } - - /// Returns an enumerator for the priority queue - /// A new enumerator for the priority queue - IEnumerator IEnumerable.GetEnumerator() { - return new Enumerator(this); - } - - /// Comparer used to order the items in the priority queue - private IComparer comparer; - /// Total number of items in the priority queue - private int count; - /// Available space in the priority queue - private int capacity; - /// Tree containing the items in the priority queue - private TItem[] heap; -#if DEBUG - /// Incremented whenever the priority queue is modified - private int version; -#endif - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; +using System.Collections; + +namespace Nuclex.Support.Collections { + + /// Queue that dequeues items in order of their priority + public class PriorityQueue : ICollection, IEnumerable { + + #region class Enumerator + + /// Enumerates all items contained in a priority queue + private class Enumerator : IEnumerator { + + /// Initializes a new priority queue enumerator + /// Priority queue to be enumerated + public Enumerator(PriorityQueue priorityQueue) { + this.priorityQueue = priorityQueue; + Reset(); + } + + /// Resets the enumerator to its initial state + public void Reset() { + this.index = -1; +#if DEBUG + this.expectedVersion = this.priorityQueue.version; +#endif + } + + /// The current item being enumerated + TItem IEnumerator.Current { + get { +#if DEBUG + checkVersion(); +#endif + return this.priorityQueue.heap[index]; + } + } + + /// Moves to the next item in the priority queue + /// True if a next item was found, false if the end has been reached + public bool MoveNext() { +#if DEBUG + checkVersion(); +#endif + if(this.index + 1 == this.priorityQueue.count) + return false; + + ++this.index; + + return true; + } + + /// Releases all resources used by the enumerator + public void Dispose() { } + +#if DEBUG + /// Ensures that the priority queue has not changed + private void checkVersion() { + if(this.expectedVersion != this.priorityQueue.version) + throw new InvalidOperationException("Priority queue has been modified"); + } +#endif + + /// The current item being enumerated + object IEnumerator.Current { + get { +#if DEBUG + checkVersion(); +#endif + return this.priorityQueue.heap[index]; + } + } + + /// Index of the current item in the priority queue + private int index; + /// The priority queue whose items this instance enumerates + private PriorityQueue priorityQueue; +#if DEBUG + /// Expected version of the priority queue + private int expectedVersion; +#endif + + } + + #endregion // class Enumerator + + /// + /// Initializes a new priority queue using IComparable for comparing items + /// + public PriorityQueue() : this(Comparer.Default) { } + + /// Initializes a new priority queue + /// Comparer to use for ordering the items + public PriorityQueue(IComparer comparer) { + this.comparer = comparer; + this.capacity = 15; // 15 is equal to 4 complete levels + this.heap = new TItem[this.capacity]; + } + + /// Returns the topmost item in the queue without dequeueing it + /// The topmost item in the queue + public TItem Peek() { + if(this.count == 0) { + throw new InvalidOperationException("No items queued"); + } + + return this.heap[0]; + } + + /// Takes the item with the highest priority off from the queue + /// The item with the highest priority in the list + /// When the queue is empty + public TItem Dequeue() { + if(this.count == 0) { + throw new InvalidOperationException("No items available to dequeue"); + } + + TItem result = this.heap[0]; + --this.count; + trickleDown(0, this.heap[this.count]); +#if DEBUG + ++this.version; +#endif + return result; + } + + /// Puts an item into the priority queue + /// Item to be queued + public void Enqueue(TItem item) { + if(this.count == capacity) + growHeap(); + + ++this.count; + bubbleUp(this.count - 1, item); +#if DEBUG + ++this.version; +#endif + } + + /// Removes all items from the priority queue + public void Clear() { + this.count = 0; +#if DEBUG + ++this.version; +#endif + } + + + /// Total number of items in the priority queue + public int Count { + get { return this.count; } + } + + /// Copies the contents of the priority queue into an array + /// Array to copy the priority queue into + /// Starting index for the destination array + public void CopyTo(Array array, int index) { + Array.Copy(this.heap, 0, array, index, this.count); + } + + /// + /// Obtains an object that can be used to synchronize accesses to the priority queue + /// from different threads + /// + public object SyncRoot { + get { return this; } + } + + /// Whether operations performed on this priority queue are thread safe + public bool IsSynchronized { + get { return false; } + } + + /// Returns a typesafe enumerator for the priority queue + /// A new enumerator for the priority queue + public IEnumerator GetEnumerator() { + return new Enumerator(this); + } + + /// Moves an item upwards in the heap tree + /// Index of the item to be moved + /// Item to be moved + private void bubbleUp(int index, TItem item) { + int parent = getParent(index); + + // Note: (index > 0) means there is a parent + while((index > 0) && (this.comparer.Compare(this.heap[parent], item) < 0)) { + this.heap[index] = this.heap[parent]; + index = parent; + parent = getParent(index); + } + + this.heap[index] = item; + } + + /// Move the item downwards in the heap tree + /// Index of the item to be moved + /// Item to be moved + private void trickleDown(int index, TItem item) { + int child = getLeftChild(index); + + while(child < this.count) { + + bool needsToBeMoved = + ((child + 1) < this.count) && + (this.comparer.Compare(heap[child], this.heap[child + 1]) < 0); + + if(needsToBeMoved) + ++child; + + this.heap[index] = this.heap[child]; + index = child; + child = getLeftChild(index); + + } + + bubbleUp(index, item); + } + + /// Obtains the left child item in the heap tree + /// Index of the item whose left child to return + /// The left child item of the provided parent item + private int getLeftChild(int index) { + return (index * 2) + 1; + } + + /// Calculates the parent entry of the item on the heap + /// Index of the item whose parent to calculate + /// The index of the parent to the specified item + private int getParent(int index) { + return (index - 1) / 2; + } + + /// Increases the size of the priority collection's heap + private void growHeap() { + this.capacity = (capacity * 2) + 1; + + TItem[] newHeap = new TItem[this.capacity]; + Array.Copy(this.heap, 0, newHeap, 0, this.count); + this.heap = newHeap; + } + + /// Returns an enumerator for the priority queue + /// A new enumerator for the priority queue + IEnumerator IEnumerable.GetEnumerator() { + return new Enumerator(this); + } + + /// Comparer used to order the items in the priority queue + private IComparer comparer; + /// Total number of items in the priority queue + private int count; + /// Available space in the priority queue + private int capacity; + /// Tree containing the items in the priority queue + private TItem[] heap; +#if DEBUG + /// Incremented whenever the priority queue is modified + private int version; +#endif + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/ReadOnlyCollection.Test.cs b/Source/Collections/ReadOnlyCollection.Test.cs index 9c772dc..316471b 100644 --- a/Source/Collections/ReadOnlyCollection.Test.cs +++ b/Source/Collections/ReadOnlyCollection.Test.cs @@ -1,163 +1,162 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections; -using System.Collections.Generic; - -#if UNITTEST - -using NUnit.Framework; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the read only collection wrapper - [TestFixture] - internal class ReadOnlyCollectionTest { - - /// - /// Verifies that the copy constructor of the read only collection works - /// - [Test] - public void TestCopyConstructor() { - int[] integers = new int[] { 12, 34, 56, 78 }; - ReadOnlyCollection testCollection = new ReadOnlyCollection(integers); - - CollectionAssert.AreEqual(integers, testCollection); - } - - /// Verifies that the IsReadOnly property returns true - [Test] - public void TestIsReadOnly() { - ReadOnlyCollection testCollection = new ReadOnlyCollection(new int[0]); - - Assert.IsTrue(testCollection.IsReadOnly); - } - - /// - /// Verifies that the CopyTo() of the read only collection works - /// - [Test] - public void TestCopyToArray() { - int[] inputIntegers = new int[] { 12, 34, 56, 78 }; - ReadOnlyCollection testCollection = new ReadOnlyCollection(inputIntegers); - - int[] outputIntegers = new int[testCollection.Count]; - testCollection.CopyTo(outputIntegers, 0); - - CollectionAssert.AreEqual(inputIntegers, outputIntegers); - } - - /// - /// Checks whether the Contains() method of the read only collection is able to - /// determine if the collection contains an item - /// - [Test] - public void TestContains() { - int[] integers = new int[] { 1234, 6789 }; - ReadOnlyCollection testCollection = new ReadOnlyCollection(integers); - - Assert.IsTrue(testCollection.Contains(1234)); - Assert.IsFalse(testCollection.Contains(4321)); - } - - /// - /// Ensures that the Add() method of the read only collection throws an exception - /// - [Test] - public void TestThrowOnAdd() { - ReadOnlyCollection testCollection = new ReadOnlyCollection(new int[0]); - Assert.Throws( - delegate() { (testCollection as ICollection).Add(123); } - ); - } - - /// - /// Ensures that the Remove() method of the read only collection throws an exception - /// - [Test] - public void TestThrowOnRemove() { - ReadOnlyCollection testCollection = new ReadOnlyCollection(new int[0]); - Assert.Throws( - delegate() { (testCollection as ICollection).Remove(123); } - ); - } - - /// - /// Ensures that the Clear() method of the read only collection throws an exception - /// - [Test] - public void TestThrowOnClear() { - ReadOnlyCollection testCollection = new ReadOnlyCollection(new int[0]); - Assert.Throws( - delegate() { (testCollection as ICollection).Clear(); } - ); - } - - /// - /// Tests whether the typesafe enumerator of the read only collection is working - /// - [Test] - public void TestTypesafeEnumerator() { - int[] inputIntegers = new int[] { 12, 34, 56, 78 }; - ReadOnlyCollection testCollection = new ReadOnlyCollection(inputIntegers); - - List outputIntegers = new List(); - foreach(int value in testCollection) { - outputIntegers.Add(value); - } - - CollectionAssert.AreEqual(inputIntegers, outputIntegers); - } - - /// - /// Verifies that the CopyTo() of the read only collection works if invoked via - /// the ICollection interface - /// - [Test] - public void TestCopyToArrayViaICollection() { - int[] inputIntegers = new int[] { 12, 34, 56, 78 }; - ReadOnlyCollection testCollection = new ReadOnlyCollection(inputIntegers); - - int[] outputIntegers = new int[testCollection.Count]; - (testCollection as ICollection).CopyTo(outputIntegers, 0); - - CollectionAssert.AreEqual(inputIntegers, outputIntegers); - } - - /// - /// Verifies that the IsSynchronized property and the SyncRoot property are working - /// - [Test] - public void TestSynchronization() { - ReadOnlyCollection testCollection = new ReadOnlyCollection(new int[0]); - - if(!(testCollection as ICollection).IsSynchronized) { - lock((testCollection as ICollection).SyncRoot) { - Assert.AreEqual(0, testCollection.Count); - } - } - } - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections; +using System.Collections.Generic; + +#if UNITTEST + +using NUnit.Framework; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the read only collection wrapper + [TestFixture] + internal class ReadOnlyCollectionTest { + + /// + /// Verifies that the copy constructor of the read only collection works + /// + [Test] + public void TestCopyConstructor() { + int[] integers = new int[] { 12, 34, 56, 78 }; + ReadOnlyCollection testCollection = new ReadOnlyCollection(integers); + + CollectionAssert.AreEqual(integers, testCollection); + } + + /// Verifies that the IsReadOnly property returns true + [Test] + public void TestIsReadOnly() { + ReadOnlyCollection testCollection = new ReadOnlyCollection(new int[0]); + + Assert.IsTrue(testCollection.IsReadOnly); + } + + /// + /// Verifies that the CopyTo() of the read only collection works + /// + [Test] + public void TestCopyToArray() { + int[] inputIntegers = new int[] { 12, 34, 56, 78 }; + ReadOnlyCollection testCollection = new ReadOnlyCollection(inputIntegers); + + int[] outputIntegers = new int[testCollection.Count]; + testCollection.CopyTo(outputIntegers, 0); + + CollectionAssert.AreEqual(inputIntegers, outputIntegers); + } + + /// + /// Checks whether the Contains() method of the read only collection is able to + /// determine if the collection contains an item + /// + [Test] + public void TestContains() { + int[] integers = new int[] { 1234, 6789 }; + ReadOnlyCollection testCollection = new ReadOnlyCollection(integers); + + Assert.IsTrue(testCollection.Contains(1234)); + Assert.IsFalse(testCollection.Contains(4321)); + } + + /// + /// Ensures that the Add() method of the read only collection throws an exception + /// + [Test] + public void TestThrowOnAdd() { + ReadOnlyCollection testCollection = new ReadOnlyCollection(new int[0]); + Assert.Throws( + delegate() { (testCollection as ICollection).Add(123); } + ); + } + + /// + /// Ensures that the Remove() method of the read only collection throws an exception + /// + [Test] + public void TestThrowOnRemove() { + ReadOnlyCollection testCollection = new ReadOnlyCollection(new int[0]); + Assert.Throws( + delegate() { (testCollection as ICollection).Remove(123); } + ); + } + + /// + /// Ensures that the Clear() method of the read only collection throws an exception + /// + [Test] + public void TestThrowOnClear() { + ReadOnlyCollection testCollection = new ReadOnlyCollection(new int[0]); + Assert.Throws( + delegate() { (testCollection as ICollection).Clear(); } + ); + } + + /// + /// Tests whether the typesafe enumerator of the read only collection is working + /// + [Test] + public void TestTypesafeEnumerator() { + int[] inputIntegers = new int[] { 12, 34, 56, 78 }; + ReadOnlyCollection testCollection = new ReadOnlyCollection(inputIntegers); + + List outputIntegers = new List(); + foreach(int value in testCollection) { + outputIntegers.Add(value); + } + + CollectionAssert.AreEqual(inputIntegers, outputIntegers); + } + + /// + /// Verifies that the CopyTo() of the read only collection works if invoked via + /// the ICollection interface + /// + [Test] + public void TestCopyToArrayViaICollection() { + int[] inputIntegers = new int[] { 12, 34, 56, 78 }; + ReadOnlyCollection testCollection = new ReadOnlyCollection(inputIntegers); + + int[] outputIntegers = new int[testCollection.Count]; + (testCollection as ICollection).CopyTo(outputIntegers, 0); + + CollectionAssert.AreEqual(inputIntegers, outputIntegers); + } + + /// + /// Verifies that the IsSynchronized property and the SyncRoot property are working + /// + [Test] + public void TestSynchronization() { + ReadOnlyCollection testCollection = new ReadOnlyCollection(new int[0]); + + if(!(testCollection as ICollection).IsSynchronized) { + lock((testCollection as ICollection).SyncRoot) { + Assert.AreEqual(0, testCollection.Count); + } + } + } + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST diff --git a/Source/Collections/ReadOnlyCollection.cs b/Source/Collections/ReadOnlyCollection.cs index 92d4bae..cd9bfdf 100644 --- a/Source/Collections/ReadOnlyCollection.cs +++ b/Source/Collections/ReadOnlyCollection.cs @@ -1,140 +1,139 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Nuclex.Support.Collections { - - /// Wraps a Collection and prevents users from modifying it - /// Type of items to manage in the Collection - public class ReadOnlyCollection : - ICollection, - ICollection { - - /// Initializes a new read-only Collection wrapper - /// Collection that will be wrapped - public ReadOnlyCollection(ICollection collection) { - this.typedCollection = collection; - this.objectCollection = (collection as ICollection); - } - - /// Determines whether the List contains the specified item - /// Item that will be checked for - /// True if the specified item is contained in the List - public bool Contains(TItem item) { - return this.typedCollection.Contains(item); - } - - /// Copies the contents of the List into an array - /// Array the List will be copied into - /// - /// Starting index at which to begin filling the destination array - /// - public void CopyTo(TItem[] array, int arrayIndex) { - this.typedCollection.CopyTo(array, arrayIndex); - } - - /// The number of items current contained in the List - public int Count { - get { return this.typedCollection.Count; } - } - - /// Whether the List is write-protected - public bool IsReadOnly { - get { return true; } - } - - /// Returns a new enumerator over the contents of the List - /// The new List contents enumerator - public IEnumerator GetEnumerator() { - return this.typedCollection.GetEnumerator(); - } - - #region ICollection<> implementation - - /// Adds an item to the end of the List - /// Item that will be added to the List - void ICollection.Add(TItem item) { - throw new NotSupportedException( - "Adding items is not supported by the read-only List" - ); - } - - /// Removes all items from the List - void ICollection.Clear() { - throw new NotSupportedException( - "Clearing is not supported by the read-only List" - ); - } - - /// Removes the specified item from the List - /// Item that will be removed from the List - /// True of the specified item was found in the List and removed - bool ICollection.Remove(TItem item) { - throw new NotSupportedException( - "Removing items is not supported by the read-only List" - ); - } - - #endregion - - #region IEnumerable implementation - - /// Returns a new enumerator over the contents of the List - /// The new List contents enumerator - IEnumerator IEnumerable.GetEnumerator() { - return this.objectCollection.GetEnumerator(); - } - - #endregion - - #region ICollection implementation - - /// Copies the contents of the List into an array - /// Array the List will be copied into - /// - /// Starting index at which to begin filling the destination array - /// - void ICollection.CopyTo(Array array, int index) { - this.objectCollection.CopyTo(array, index); - } - - /// Whether the List is synchronized for multi-threaded usage - bool ICollection.IsSynchronized { - get { return this.objectCollection.IsSynchronized; } - } - - /// Synchronization root on which the List locks - object ICollection.SyncRoot { - get { return this.objectCollection.SyncRoot; } - } - - #endregion - - /// The wrapped Collection under its type-safe interface - private ICollection typedCollection; - /// The wrapped Collection under its object interface - private ICollection objectCollection; - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Nuclex.Support.Collections { + + /// Wraps a Collection and prevents users from modifying it + /// Type of items to manage in the Collection + public class ReadOnlyCollection : + ICollection, + ICollection { + + /// Initializes a new read-only Collection wrapper + /// Collection that will be wrapped + public ReadOnlyCollection(ICollection collection) { + this.typedCollection = collection; + this.objectCollection = (collection as ICollection); + } + + /// Determines whether the List contains the specified item + /// Item that will be checked for + /// True if the specified item is contained in the List + public bool Contains(TItem item) { + return this.typedCollection.Contains(item); + } + + /// Copies the contents of the List into an array + /// Array the List will be copied into + /// + /// Starting index at which to begin filling the destination array + /// + public void CopyTo(TItem[] array, int arrayIndex) { + this.typedCollection.CopyTo(array, arrayIndex); + } + + /// The number of items current contained in the List + public int Count { + get { return this.typedCollection.Count; } + } + + /// Whether the List is write-protected + public bool IsReadOnly { + get { return true; } + } + + /// Returns a new enumerator over the contents of the List + /// The new List contents enumerator + public IEnumerator GetEnumerator() { + return this.typedCollection.GetEnumerator(); + } + + #region ICollection<> implementation + + /// Adds an item to the end of the List + /// Item that will be added to the List + void ICollection.Add(TItem item) { + throw new NotSupportedException( + "Adding items is not supported by the read-only List" + ); + } + + /// Removes all items from the List + void ICollection.Clear() { + throw new NotSupportedException( + "Clearing is not supported by the read-only List" + ); + } + + /// Removes the specified item from the List + /// Item that will be removed from the List + /// True of the specified item was found in the List and removed + bool ICollection.Remove(TItem item) { + throw new NotSupportedException( + "Removing items is not supported by the read-only List" + ); + } + + #endregion + + #region IEnumerable implementation + + /// Returns a new enumerator over the contents of the List + /// The new List contents enumerator + IEnumerator IEnumerable.GetEnumerator() { + return this.objectCollection.GetEnumerator(); + } + + #endregion + + #region ICollection implementation + + /// Copies the contents of the List into an array + /// Array the List will be copied into + /// + /// Starting index at which to begin filling the destination array + /// + void ICollection.CopyTo(Array array, int index) { + this.objectCollection.CopyTo(array, index); + } + + /// Whether the List is synchronized for multi-threaded usage + bool ICollection.IsSynchronized { + get { return this.objectCollection.IsSynchronized; } + } + + /// Synchronization root on which the List locks + object ICollection.SyncRoot { + get { return this.objectCollection.SyncRoot; } + } + + #endregion + + /// The wrapped Collection under its type-safe interface + private ICollection typedCollection; + /// The wrapped Collection under its object interface + private ICollection objectCollection; + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/ReadOnlyDictionary.Test.cs b/Source/Collections/ReadOnlyDictionary.Test.cs index f246989..fc05e5f 100644 --- a/Source/Collections/ReadOnlyDictionary.Test.cs +++ b/Source/Collections/ReadOnlyDictionary.Test.cs @@ -1,510 +1,509 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Runtime.Serialization.Formatters.Binary; - -using NUnit.Framework; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the read only dictionary wrapper - [TestFixture] - internal class ReadOnlyDictionaryTest { - - /// - /// Verifies that the copy constructor of the read only dictionary works - /// - [Test] - public void TestCopyConstructor() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - CollectionAssert.AreEqual(numbers, testDictionary); - } - - /// Verifies that the IsReadOnly property returns true - [Test] - public void TestIsReadOnly() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - Assert.IsTrue(testDictionary.IsReadOnly); - } - - /// - /// Checks whether the Contains() method of the read only dictionary is able to - /// determine if the dictionary contains an item - /// - [Test] - public void TestContains() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - Assert.IsTrue( - testDictionary.Contains(new KeyValuePair(42, "forty-two")) - ); - Assert.IsFalse( - testDictionary.Contains(new KeyValuePair(24, "twenty-four")) - ); - } - - /// - /// Checks whether the Contains() method of the read only dictionary is able to - /// determine if the dictionary contains a key - /// - [Test] - public void TestContainsKey() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - Assert.IsTrue(testDictionary.ContainsKey(42)); - Assert.IsFalse(testDictionary.ContainsKey(24)); - } - - /// - /// Verifies that the CopyTo() of the read only dictionary works - /// - [Test] - public void TestCopyToArray() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - KeyValuePair[] items = new KeyValuePair[numbers.Count]; - - testDictionary.CopyTo(items, 0); - - CollectionAssert.AreEqual(numbers, items); - } - - /// - /// Tests whether the typesafe enumerator of the read only dictionary is working - /// - [Test] - public void TestTypesafeEnumerator() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - List> outputItems = new List>(); - foreach(KeyValuePair item in testDictionary) { - outputItems.Add(item); - } - - CollectionAssert.AreEqual(numbers, outputItems); - } - - /// - /// Tests whether the keys collection of the read only dictionary can be queried - /// - [Test] - public void TestGetKeysCollection() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - ICollection inputKeys = numbers.Keys; - ICollection keys = testDictionary.Keys; - CollectionAssert.AreEqual(inputKeys, keys); - } - - /// - /// Tests whether the values collection of the read only dictionary can be queried - /// - [Test] - public void TestGetValuesCollection() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - ICollection inputValues = numbers.Values; - ICollection values = testDictionary.Values; - CollectionAssert.AreEqual(inputValues, values); - } - - /// - /// Tests whether the TryGetValue() method of the read only dictionary is working - /// - [Test] - public void TestTryGetValue() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - string value; - - Assert.IsTrue(testDictionary.TryGetValue(42, out value)); - Assert.AreEqual("forty-two", value); - - Assert.IsFalse(testDictionary.TryGetValue(24, out value)); - Assert.AreEqual(null, value); - } - - /// - /// Tests whether the retrieval of values using the indexer of the read only - /// dictionary is working - /// - [Test] - public void TestRetrieveValueByIndexer() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - Assert.AreEqual("forty-two", testDictionary[42]); - } - - /// - /// Tests whether an exception is thrown if the indexer of the read only dictionary - /// is used to attempt to retrieve a non-existing value - /// - [Test] - public void TestThrowOnRetrieveNonExistingValueByIndexer() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - Assert.Throws( - delegate() { Console.WriteLine(testDictionary[24]); } - ); - } - - /// - /// Checks whether the read only dictionary will throw an exception if its - /// Add() method is called via the generic IDictionary<> interface - /// - [Test] - public void TestThrowOnAddViaGenericIDictionary() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - Assert.Throws( - delegate() { (testDictionary as IDictionary).Add(10, "ten"); } - ); - } - - /// - /// Checks whether the read only dictionary will throw an exception if its - /// Remove() method is called via the generic IDictionary<> interface - /// - [Test] - public void TestThrowOnRemoveViaGenericIDictionary() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - Assert.Throws( - delegate() { (testDictionary as IDictionary).Remove(3); } - ); - } - - /// - /// Tests whether the TryGetValue() method of the read only dictionary is working - /// - [Test] - public void TestRetrieveValueByIndexerViaGenericIDictionary() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - Assert.AreEqual("forty-two", (testDictionary as IDictionary)[42]); - } - - /// - /// Checks whether the read only dictionary will throw an exception if its - /// indexer is used to insert an item via the generic IDictionar<> interface - /// - [Test] - public void TestThrowOnReplaceByIndexerViaGenericIDictionary() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - Assert.Throws( - delegate() { (testDictionary as IDictionary)[24] = "twenty-four"; } - ); - } - - /// - /// Checks whether the read only dictionary will throw an exception if its - /// Clear() method is called via the IDictionary interface - /// - [Test] - public void TestThrowOnClearViaIDictionary() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - Assert.Throws( - delegate() { (testDictionary as IDictionary).Clear(); } - ); - } - - /// - /// Checks whether the read only dictionary will throw an exception if its - /// Add() method is called via the IDictionary interface - /// - [Test] - public void TestThrowOnAddViaIDictionary() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - Assert.Throws( - delegate() { (testDictionary as IDictionary).Add(24, "twenty-four"); } - ); - } - - /// - /// Checks whether the Contains() method of the read only dictionary is able to - /// determine if the dictionary contains an item via the IDictionary interface - /// - [Test] - public void TestContainsViaIDictionary() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - Assert.IsTrue((testDictionary as IDictionary).Contains(42)); - Assert.IsFalse((testDictionary as IDictionary).Contains(24)); - } - - /// - /// Checks whether the GetEnumerator() method of the read only dictionary returns - /// a working enumerator if accessed via the IDictionary interface - /// - [Test] - public void TestEnumeratorViaIDictionary() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - Dictionary outputNumbers = new Dictionary(); - foreach(DictionaryEntry entry in (testDictionary as IDictionary)) { - (outputNumbers as IDictionary).Add(entry.Key, entry.Value); - } - - CollectionAssert.AreEquivalent(numbers, outputNumbers); - } - - /// - /// Checks whether the IsFixedSize property of the read only dictionary returns - /// the expected result for a read only dictionary based on a dynamic dictionary - /// - [Test] - public void TestIsFixedSizeViaIList() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - Assert.IsFalse((testDictionary as IDictionary).IsFixedSize); - } - - /// - /// Tests whether the keys collection of the read only dictionary can be queried - /// via the IDictionary interface - /// - [Test] - public void TestGetKeysCollectionViaIDictionary() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - ICollection inputKeys = (numbers as IDictionary).Keys; - ICollection keys = (testDictionary as IDictionary).Keys; - CollectionAssert.AreEqual(inputKeys, keys); - } - - /// - /// Tests whether the values collection of the read only dictionary can be queried - /// via the IDictionary interface - /// - [Test] - public void TestGetValuesCollectionViaIDictionary() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - ICollection inputValues = (numbers as IDictionary).Values; - ICollection values = (testDictionary as IDictionary).Values; - CollectionAssert.AreEqual(inputValues, values); - } - - /// - /// Checks whether the read only dictionary will throw an exception if its - /// Remove() method is called via the IDictionary interface - /// - [Test] - public void TestThrowOnRemoveViaIDictionary() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - Assert.Throws( - delegate() { (testDictionary as IDictionary).Remove(3); } - ); - } - - /// - /// Tests whether the retrieval of values using the indexer of the read only - /// dictionary is working via the IDictionary interface - /// - [Test] - public void TestRetrieveValueByIndexerViaIDictionary() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - Assert.AreEqual("forty-two", (testDictionary as IDictionary)[42]); - } - - /// - /// Checks whether the read only dictionary will throw an exception if its - /// indexer is used to insert an item via the IDictionary interface - /// - [Test] - public void TestThrowOnReplaceByIndexerViaIDictionary() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - Assert.Throws( - delegate() { (testDictionary as IDictionary)[24] = "twenty-four"; } - ); - } - - /// - /// Checks whether the read only dictionary will throw an exception if its - /// Add() method is used via the generic ICollection<> interface - /// - [Test] - public void TestThrowOnAddViaGenericICollection() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - Assert.Throws( - delegate() { - (testDictionary as ICollection>).Add( - new KeyValuePair(24, "twenty-four") - ); - } - ); - } - - /// - /// Checks whether the read only dictionary will throw an exception if its - /// Clear() method is used via the generic ICollection<> interface - /// - [Test] - public void TestThrowOnClearViaGenericICollection() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - Assert.Throws( - delegate() { (testDictionary as ICollection>).Clear(); } - ); - } - - /// - /// Checks whether the read only dictionary will throw an exception if its - /// Remove() method is used via the generic ICollection<> interface - /// - [Test] - public void TestThrowOnRemoveViaGenericICollection() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - Assert.Throws( - delegate() { - (testDictionary as ICollection>).Remove( - new KeyValuePair(42, "fourty-two") - ); - } - ); - } - - /// - /// Verifies that the CopyTo() of the read only dictionary works when called - /// via the the ICollection interface - /// - [Test] - public void TestCopyToArrayViaICollection() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - DictionaryEntry[] entries = new DictionaryEntry[numbers.Count]; - (testDictionary as ICollection).CopyTo(entries, 0); - - KeyValuePair[] items = new KeyValuePair[numbers.Count]; - for(int index = 0; index < entries.Length; ++index) { - items[index] = new KeyValuePair( - (int)entries[index].Key, (string)entries[index].Value - ); - } - CollectionAssert.AreEquivalent(numbers, items); - } - - /// - /// Verifies that the IsSynchronized property and the SyncRoot property are working - /// - [Test] - public void TestSynchronization() { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary = makeReadOnly(numbers); - - if(!(testDictionary as ICollection).IsSynchronized) { - lock((testDictionary as ICollection).SyncRoot) { - Assert.AreEqual(numbers.Count, testDictionary.Count); - } - } - } - - /// - /// Test whether the read only dictionary can be serialized - /// - [Test] - public void TestSerialization() { - BinaryFormatter formatter = new BinaryFormatter(); - - using(MemoryStream memory = new MemoryStream()) { - Dictionary numbers = createTestDictionary(); - ReadOnlyDictionary testDictionary1 = makeReadOnly(numbers); - - formatter.Serialize(memory, testDictionary1); - memory.Position = 0; - object testDictionary2 = formatter.Deserialize(memory); - - CollectionAssert.AreEquivalent(testDictionary1, (IEnumerable)testDictionary2); - } - } - - /// - /// Creates a new read-only dictionary filled with some values for testing - /// - /// The newly created read-only dictionary - private static Dictionary createTestDictionary() { - Dictionary numbers = new Dictionary(); - numbers.Add(1, "one"); - numbers.Add(2, "two"); - numbers.Add(3, "three"); - numbers.Add(42, "forty-two"); - return new Dictionary(numbers); - } - - /// - /// Creates a new read-only dictionary filled with some values for testing - /// - /// The newly created read-only dictionary - private static ReadOnlyDictionary makeReadOnly( - IDictionary dictionary - ) { - return new ReadOnlyDictionary(dictionary); - } - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; + +using NUnit.Framework; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the read only dictionary wrapper + [TestFixture] + internal class ReadOnlyDictionaryTest { + + /// + /// Verifies that the copy constructor of the read only dictionary works + /// + [Test] + public void TestCopyConstructor() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + CollectionAssert.AreEqual(numbers, testDictionary); + } + + /// Verifies that the IsReadOnly property returns true + [Test] + public void TestIsReadOnly() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + Assert.IsTrue(testDictionary.IsReadOnly); + } + + /// + /// Checks whether the Contains() method of the read only dictionary is able to + /// determine if the dictionary contains an item + /// + [Test] + public void TestContains() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + Assert.IsTrue( + testDictionary.Contains(new KeyValuePair(42, "forty-two")) + ); + Assert.IsFalse( + testDictionary.Contains(new KeyValuePair(24, "twenty-four")) + ); + } + + /// + /// Checks whether the Contains() method of the read only dictionary is able to + /// determine if the dictionary contains a key + /// + [Test] + public void TestContainsKey() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + Assert.IsTrue(testDictionary.ContainsKey(42)); + Assert.IsFalse(testDictionary.ContainsKey(24)); + } + + /// + /// Verifies that the CopyTo() of the read only dictionary works + /// + [Test] + public void TestCopyToArray() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + KeyValuePair[] items = new KeyValuePair[numbers.Count]; + + testDictionary.CopyTo(items, 0); + + CollectionAssert.AreEqual(numbers, items); + } + + /// + /// Tests whether the typesafe enumerator of the read only dictionary is working + /// + [Test] + public void TestTypesafeEnumerator() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + List> outputItems = new List>(); + foreach(KeyValuePair item in testDictionary) { + outputItems.Add(item); + } + + CollectionAssert.AreEqual(numbers, outputItems); + } + + /// + /// Tests whether the keys collection of the read only dictionary can be queried + /// + [Test] + public void TestGetKeysCollection() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + ICollection inputKeys = numbers.Keys; + ICollection keys = testDictionary.Keys; + CollectionAssert.AreEqual(inputKeys, keys); + } + + /// + /// Tests whether the values collection of the read only dictionary can be queried + /// + [Test] + public void TestGetValuesCollection() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + ICollection inputValues = numbers.Values; + ICollection values = testDictionary.Values; + CollectionAssert.AreEqual(inputValues, values); + } + + /// + /// Tests whether the TryGetValue() method of the read only dictionary is working + /// + [Test] + public void TestTryGetValue() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + string value; + + Assert.IsTrue(testDictionary.TryGetValue(42, out value)); + Assert.AreEqual("forty-two", value); + + Assert.IsFalse(testDictionary.TryGetValue(24, out value)); + Assert.AreEqual(null, value); + } + + /// + /// Tests whether the retrieval of values using the indexer of the read only + /// dictionary is working + /// + [Test] + public void TestRetrieveValueByIndexer() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + Assert.AreEqual("forty-two", testDictionary[42]); + } + + /// + /// Tests whether an exception is thrown if the indexer of the read only dictionary + /// is used to attempt to retrieve a non-existing value + /// + [Test] + public void TestThrowOnRetrieveNonExistingValueByIndexer() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + Assert.Throws( + delegate() { Console.WriteLine(testDictionary[24]); } + ); + } + + /// + /// Checks whether the read only dictionary will throw an exception if its + /// Add() method is called via the generic IDictionary<> interface + /// + [Test] + public void TestThrowOnAddViaGenericIDictionary() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + Assert.Throws( + delegate() { (testDictionary as IDictionary).Add(10, "ten"); } + ); + } + + /// + /// Checks whether the read only dictionary will throw an exception if its + /// Remove() method is called via the generic IDictionary<> interface + /// + [Test] + public void TestThrowOnRemoveViaGenericIDictionary() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + Assert.Throws( + delegate() { (testDictionary as IDictionary).Remove(3); } + ); + } + + /// + /// Tests whether the TryGetValue() method of the read only dictionary is working + /// + [Test] + public void TestRetrieveValueByIndexerViaGenericIDictionary() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + Assert.AreEqual("forty-two", (testDictionary as IDictionary)[42]); + } + + /// + /// Checks whether the read only dictionary will throw an exception if its + /// indexer is used to insert an item via the generic IDictionar<> interface + /// + [Test] + public void TestThrowOnReplaceByIndexerViaGenericIDictionary() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + Assert.Throws( + delegate() { (testDictionary as IDictionary)[24] = "twenty-four"; } + ); + } + + /// + /// Checks whether the read only dictionary will throw an exception if its + /// Clear() method is called via the IDictionary interface + /// + [Test] + public void TestThrowOnClearViaIDictionary() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + Assert.Throws( + delegate() { (testDictionary as IDictionary).Clear(); } + ); + } + + /// + /// Checks whether the read only dictionary will throw an exception if its + /// Add() method is called via the IDictionary interface + /// + [Test] + public void TestThrowOnAddViaIDictionary() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + Assert.Throws( + delegate() { (testDictionary as IDictionary).Add(24, "twenty-four"); } + ); + } + + /// + /// Checks whether the Contains() method of the read only dictionary is able to + /// determine if the dictionary contains an item via the IDictionary interface + /// + [Test] + public void TestContainsViaIDictionary() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + Assert.IsTrue((testDictionary as IDictionary).Contains(42)); + Assert.IsFalse((testDictionary as IDictionary).Contains(24)); + } + + /// + /// Checks whether the GetEnumerator() method of the read only dictionary returns + /// a working enumerator if accessed via the IDictionary interface + /// + [Test] + public void TestEnumeratorViaIDictionary() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + Dictionary outputNumbers = new Dictionary(); + foreach(DictionaryEntry entry in (testDictionary as IDictionary)) { + (outputNumbers as IDictionary).Add(entry.Key, entry.Value); + } + + CollectionAssert.AreEquivalent(numbers, outputNumbers); + } + + /// + /// Checks whether the IsFixedSize property of the read only dictionary returns + /// the expected result for a read only dictionary based on a dynamic dictionary + /// + [Test] + public void TestIsFixedSizeViaIList() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + Assert.IsFalse((testDictionary as IDictionary).IsFixedSize); + } + + /// + /// Tests whether the keys collection of the read only dictionary can be queried + /// via the IDictionary interface + /// + [Test] + public void TestGetKeysCollectionViaIDictionary() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + ICollection inputKeys = (numbers as IDictionary).Keys; + ICollection keys = (testDictionary as IDictionary).Keys; + CollectionAssert.AreEqual(inputKeys, keys); + } + + /// + /// Tests whether the values collection of the read only dictionary can be queried + /// via the IDictionary interface + /// + [Test] + public void TestGetValuesCollectionViaIDictionary() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + ICollection inputValues = (numbers as IDictionary).Values; + ICollection values = (testDictionary as IDictionary).Values; + CollectionAssert.AreEqual(inputValues, values); + } + + /// + /// Checks whether the read only dictionary will throw an exception if its + /// Remove() method is called via the IDictionary interface + /// + [Test] + public void TestThrowOnRemoveViaIDictionary() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + Assert.Throws( + delegate() { (testDictionary as IDictionary).Remove(3); } + ); + } + + /// + /// Tests whether the retrieval of values using the indexer of the read only + /// dictionary is working via the IDictionary interface + /// + [Test] + public void TestRetrieveValueByIndexerViaIDictionary() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + Assert.AreEqual("forty-two", (testDictionary as IDictionary)[42]); + } + + /// + /// Checks whether the read only dictionary will throw an exception if its + /// indexer is used to insert an item via the IDictionary interface + /// + [Test] + public void TestThrowOnReplaceByIndexerViaIDictionary() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + Assert.Throws( + delegate() { (testDictionary as IDictionary)[24] = "twenty-four"; } + ); + } + + /// + /// Checks whether the read only dictionary will throw an exception if its + /// Add() method is used via the generic ICollection<> interface + /// + [Test] + public void TestThrowOnAddViaGenericICollection() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + Assert.Throws( + delegate() { + (testDictionary as ICollection>).Add( + new KeyValuePair(24, "twenty-four") + ); + } + ); + } + + /// + /// Checks whether the read only dictionary will throw an exception if its + /// Clear() method is used via the generic ICollection<> interface + /// + [Test] + public void TestThrowOnClearViaGenericICollection() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + Assert.Throws( + delegate() { (testDictionary as ICollection>).Clear(); } + ); + } + + /// + /// Checks whether the read only dictionary will throw an exception if its + /// Remove() method is used via the generic ICollection<> interface + /// + [Test] + public void TestThrowOnRemoveViaGenericICollection() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + Assert.Throws( + delegate() { + (testDictionary as ICollection>).Remove( + new KeyValuePair(42, "fourty-two") + ); + } + ); + } + + /// + /// Verifies that the CopyTo() of the read only dictionary works when called + /// via the the ICollection interface + /// + [Test] + public void TestCopyToArrayViaICollection() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + DictionaryEntry[] entries = new DictionaryEntry[numbers.Count]; + (testDictionary as ICollection).CopyTo(entries, 0); + + KeyValuePair[] items = new KeyValuePair[numbers.Count]; + for(int index = 0; index < entries.Length; ++index) { + items[index] = new KeyValuePair( + (int)entries[index].Key, (string)entries[index].Value + ); + } + CollectionAssert.AreEquivalent(numbers, items); + } + + /// + /// Verifies that the IsSynchronized property and the SyncRoot property are working + /// + [Test] + public void TestSynchronization() { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary = makeReadOnly(numbers); + + if(!(testDictionary as ICollection).IsSynchronized) { + lock((testDictionary as ICollection).SyncRoot) { + Assert.AreEqual(numbers.Count, testDictionary.Count); + } + } + } + + /// + /// Test whether the read only dictionary can be serialized + /// + [Test] + public void TestSerialization() { + BinaryFormatter formatter = new BinaryFormatter(); + + using(MemoryStream memory = new MemoryStream()) { + Dictionary numbers = createTestDictionary(); + ReadOnlyDictionary testDictionary1 = makeReadOnly(numbers); + + formatter.Serialize(memory, testDictionary1); + memory.Position = 0; + object testDictionary2 = formatter.Deserialize(memory); + + CollectionAssert.AreEquivalent(testDictionary1, (IEnumerable)testDictionary2); + } + } + + /// + /// Creates a new read-only dictionary filled with some values for testing + /// + /// The newly created read-only dictionary + private static Dictionary createTestDictionary() { + Dictionary numbers = new Dictionary(); + numbers.Add(1, "one"); + numbers.Add(2, "two"); + numbers.Add(3, "three"); + numbers.Add(42, "forty-two"); + return new Dictionary(numbers); + } + + /// + /// Creates a new read-only dictionary filled with some values for testing + /// + /// The newly created read-only dictionary + private static ReadOnlyDictionary makeReadOnly( + IDictionary dictionary + ) { + return new ReadOnlyDictionary(dictionary); + } + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST diff --git a/Source/Collections/ReadOnlyDictionary.cs b/Source/Collections/ReadOnlyDictionary.cs index 3026922..ee40189 100644 --- a/Source/Collections/ReadOnlyDictionary.cs +++ b/Source/Collections/ReadOnlyDictionary.cs @@ -1,407 +1,406 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; -using System.Collections; -using System.Runtime.Serialization; - -namespace Nuclex.Support.Collections { - - /// Wraps a dictionary and prevents users from modifying it - /// Type of the keys used in the dictionary - /// Type of the values used in the dictionary -#if !NO_SERIALIZATION - [Serializable] -#endif - public class ReadOnlyDictionary : -#if !NO_SERIALIZATION - ISerializable, - IDeserializationCallback, -#endif - IDictionary, - IDictionary { - -#if !NO_SERIALIZATION - - #region class SerializedDictionary - - /// - /// Dictionary wrapped used to reconstruct a serialized read only dictionary - /// - private class SerializedDictionary : Dictionary { - - /// - /// Initializes a new instance of the System.WeakReference class, using deserialized - /// data from the specified serialization and stream objects. - /// - /// - /// An object that holds all the data needed to serialize or deserialize the - /// current System.WeakReference object. - /// - /// - /// (Reserved) Describes the source and destination of the serialized stream - /// specified by info. - /// - /// - /// The info parameter is null. - /// - public SerializedDictionary(SerializationInfo info, StreamingContext context) : - base(info, context) { } - - } - - #endregion // class SerializedDictionary - - /// - /// Initializes a new instance of the System.WeakReference class, using deserialized - /// data from the specified serialization and stream objects. - /// - /// - /// An object that holds all the data needed to serialize or deserialize the - /// current System.WeakReference object. - /// - /// - /// (Reserved) Describes the source and destination of the serialized stream - /// specified by info. - /// - /// - /// The info parameter is null. - /// - protected ReadOnlyDictionary(SerializationInfo info, StreamingContext context) : - this(new SerializedDictionary(info, context)) { } - -#endif // !NO_SERIALIZATION - - /// Initializes a new read-only dictionary wrapper - /// Dictionary that will be wrapped - public ReadOnlyDictionary(IDictionary dictionary) { - this.typedDictionary = dictionary; - this.objectDictionary = (this.typedDictionary as IDictionary); - } - - /// Whether the directory is write-protected - public bool IsReadOnly { - get { return true; } - } - - /// - /// Determines whether the specified KeyValuePair is contained in the Dictionary - /// - /// KeyValuePair that will be checked for - /// True if the provided KeyValuePair was contained in the Dictionary - public bool Contains(KeyValuePair item) { - return this.typedDictionary.Contains(item); - } - - /// Determines whether the Dictionary contains the specified key - /// Key that will be checked for - /// - /// True if an entry with the specified key was contained in the Dictionary - /// - public bool ContainsKey(KeyType key) { - return this.typedDictionary.ContainsKey(key); - } - - /// Copies the contents of the Dictionary into an array - /// Array the Dictionary will be copied into - /// - /// Starting index at which to begin filling the destination array - /// - public void CopyTo(KeyValuePair[] array, int arrayIndex) { - this.typedDictionary.CopyTo(array, arrayIndex); - } - - /// Number of elements contained in the Dictionary - public int Count { - get { return this.typedDictionary.Count; } - } - - /// Creates a new enumerator for the Dictionary - /// The new Dictionary enumerator - public IEnumerator> GetEnumerator() { - return this.typedDictionary.GetEnumerator(); - } - - /// Collection of all keys contained in the Dictionary - public ICollection Keys { - get { - if(this.readonlyKeyCollection == null) { - this.readonlyKeyCollection = new ReadOnlyCollection( - this.typedDictionary.Keys - ); - } - - return this.readonlyKeyCollection; - } - } - - /// Collection of all values contained in the Dictionary - public ICollection Values { - get { - if(this.readonlyValueCollection == null) { - this.readonlyValueCollection = new ReadOnlyCollection( - this.typedDictionary.Values - ); - } - - return this.readonlyValueCollection; - } - } - - /// - /// Attempts to retrieve the item with the specified key from the Dictionary - /// - /// Key of the item to attempt to retrieve - /// - /// Output parameter that will receive the key upon successful completion - /// - /// - /// True if the item was found and has been placed in the output parameter - /// - public bool TryGetValue(KeyType key, out ValueType value) { - return this.typedDictionary.TryGetValue(key, out value); - } - - /// Accesses an item in the Dictionary by its key - /// Key of the item that will be accessed - public ValueType this[KeyType key] { - get { return this.typedDictionary[key]; } - } - - #region IDictionary<,> implementation - - /// Inserts an item into the Dictionary - /// Key under which to add the new item - /// Item that will be added to the Dictionary - void IDictionary.Add(KeyType key, ValueType value) { - throw new NotSupportedException( - "Adding items is not supported by the read-only Dictionary" - ); - } - - /// Removes the item with the specified key from the Dictionary - /// Key of the elementes that will be removed - /// True if an item with the specified key was found and removed - bool IDictionary.Remove(KeyType key) { - throw new NotSupportedException( - "Removing items is not supported by the read-only Dictionary" - ); - } - - /// Accesses an item in the Dictionary by its key - /// Key of the item that will be accessed - ValueType IDictionary.this[KeyType key] { - get { return this.typedDictionary[key]; } - set { - throw new NotSupportedException( - "Assigning items is not supported in a read-only Dictionary" - ); - } - } - - #endregion - - #region IEnumerable implementation - - /// Returns a new object enumerator for the Dictionary - /// The new object enumerator - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { - return (this.typedDictionary as IEnumerable).GetEnumerator(); - } - - #endregion - - #region IDictionary implementation - - /// Removes all items from the Dictionary - void IDictionary.Clear() { - throw new NotSupportedException( - "Clearing is not supported in a read-only Dictionary" - ); - } - - /// Adds an item into the Dictionary - /// Key under which the item will be added - /// Item that will be added - void IDictionary.Add(object key, object value) { - throw new NotSupportedException( - "Adding items is not supported in a read-only Dictionary" - ); - } - - /// Determines whether the specified key exists in the Dictionary - /// Key that will be checked for - /// True if an item with the specified key exists in the Dictionary - bool IDictionary.Contains(object key) { - return this.objectDictionary.Contains(key); - } - - /// Whether the size of the Dictionary is fixed - bool IDictionary.IsFixedSize { - get { return this.objectDictionary.IsFixedSize; } - } - - /// Returns a collection of all keys in the Dictionary - ICollection IDictionary.Keys { - get { - if(this.readonlyKeyCollection == null) { - this.readonlyKeyCollection = new ReadOnlyCollection( - this.typedDictionary.Keys - ); - } - - return this.readonlyKeyCollection; - } - } - - /// Returns a collection of all values stored in the Dictionary - ICollection IDictionary.Values { - get { - if(this.readonlyValueCollection == null) { - this.readonlyValueCollection = new ReadOnlyCollection( - this.typedDictionary.Values - ); - } - - return this.readonlyValueCollection; - } - } - - /// Removes an item from the Dictionary - /// Key of the item that will be removed - void IDictionary.Remove(object key) { - throw new NotSupportedException( - "Removing is not supported by the read-only Dictionary" - ); - } - - /// Accesses an item in the Dictionary by its key - /// Key of the item that will be accessed - /// The item with the specified key - object IDictionary.this[object key] { - get { return this.objectDictionary[key]; } - set { - throw new NotSupportedException( - "Assigning items is not supported by the read-only Dictionary" - ); - } - } - - #endregion // IDictionary implementation - - #region IDictionaryEnumerator implementation - - /// Returns a new entry enumerator for the dictionary - /// The new entry enumerator - IDictionaryEnumerator IDictionary.GetEnumerator() { - return this.objectDictionary.GetEnumerator(); - } - - #endregion // IDictionaryEnumerator implementation - - #region ICollection<> implementation - - /// Inserts an already prepared element into the Dictionary - /// Prepared element that will be added to the Dictionary - void ICollection>.Add( - KeyValuePair item - ) { - throw new NotSupportedException( - "Adding items is not supported by the read-only Dictionary" - ); - } - - /// Removes all items from the Dictionary - void ICollection>.Clear() { - throw new NotSupportedException( - "Clearing is not supported in a read-only Dictionary" - ); - } - - /// Removes all items from the Dictionary - /// Item that will be removed from the Dictionary - bool ICollection>.Remove( - KeyValuePair itemToRemove - ) { - throw new NotSupportedException( - "Removing items is not supported in a read-only Dictionary" - ); - } - - #endregion - - #region ICollection implementation - - /// Copies the contents of the Dictionary into an array - /// Array the Dictionary contents will be copied into - /// - /// Starting index at which to begin filling the destination array - /// - void ICollection.CopyTo(Array array, int index) { - this.objectDictionary.CopyTo(array, index); - } - - /// Whether the Dictionary is synchronized for multi-threaded usage - bool ICollection.IsSynchronized { - get { return this.objectDictionary.IsSynchronized; } - } - - /// Synchronization root on which the Dictionary locks - object ICollection.SyncRoot { - get { return this.objectDictionary.SyncRoot; } - } - - #endregion - -#if !NO_SERIALIZATION - #region ISerializable implementation - - /// Serializes the Dictionary - /// - /// Provides the container into which the Dictionary will serialize itself - /// - /// - /// Contextual informations about the serialization environment - /// - void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) { - (this.typedDictionary as ISerializable).GetObjectData(info, context); - } - - /// Called after all objects have been successfully deserialized - /// Nicht unterstützt - void IDeserializationCallback.OnDeserialization(object sender) { - (this.typedDictionary as IDeserializationCallback).OnDeserialization(sender); - } - - #endregion -#endif //!NO_SERIALIZATION - - /// The wrapped Dictionary under its type-safe interface - private IDictionary typedDictionary; - /// The wrapped Dictionary under its object interface - private IDictionary objectDictionary; - /// ReadOnly wrapper for the keys collection of the Dictionary - private ReadOnlyCollection readonlyKeyCollection; - /// ReadOnly wrapper for the values collection of the Dictionary - private ReadOnlyCollection readonlyValueCollection; - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; +using System.Collections; +using System.Runtime.Serialization; + +namespace Nuclex.Support.Collections { + + /// Wraps a dictionary and prevents users from modifying it + /// Type of the keys used in the dictionary + /// Type of the values used in the dictionary +#if !NO_SERIALIZATION + [Serializable] +#endif + public class ReadOnlyDictionary : +#if !NO_SERIALIZATION + ISerializable, + IDeserializationCallback, +#endif + IDictionary, + IDictionary { + +#if !NO_SERIALIZATION + + #region class SerializedDictionary + + /// + /// Dictionary wrapped used to reconstruct a serialized read only dictionary + /// + private class SerializedDictionary : Dictionary { + + /// + /// Initializes a new instance of the System.WeakReference class, using deserialized + /// data from the specified serialization and stream objects. + /// + /// + /// An object that holds all the data needed to serialize or deserialize the + /// current System.WeakReference object. + /// + /// + /// (Reserved) Describes the source and destination of the serialized stream + /// specified by info. + /// + /// + /// The info parameter is null. + /// + public SerializedDictionary(SerializationInfo info, StreamingContext context) : + base(info, context) { } + + } + + #endregion // class SerializedDictionary + + /// + /// Initializes a new instance of the System.WeakReference class, using deserialized + /// data from the specified serialization and stream objects. + /// + /// + /// An object that holds all the data needed to serialize or deserialize the + /// current System.WeakReference object. + /// + /// + /// (Reserved) Describes the source and destination of the serialized stream + /// specified by info. + /// + /// + /// The info parameter is null. + /// + protected ReadOnlyDictionary(SerializationInfo info, StreamingContext context) : + this(new SerializedDictionary(info, context)) { } + +#endif // !NO_SERIALIZATION + + /// Initializes a new read-only dictionary wrapper + /// Dictionary that will be wrapped + public ReadOnlyDictionary(IDictionary dictionary) { + this.typedDictionary = dictionary; + this.objectDictionary = (this.typedDictionary as IDictionary); + } + + /// Whether the directory is write-protected + public bool IsReadOnly { + get { return true; } + } + + /// + /// Determines whether the specified KeyValuePair is contained in the Dictionary + /// + /// KeyValuePair that will be checked for + /// True if the provided KeyValuePair was contained in the Dictionary + public bool Contains(KeyValuePair item) { + return this.typedDictionary.Contains(item); + } + + /// Determines whether the Dictionary contains the specified key + /// Key that will be checked for + /// + /// True if an entry with the specified key was contained in the Dictionary + /// + public bool ContainsKey(KeyType key) { + return this.typedDictionary.ContainsKey(key); + } + + /// Copies the contents of the Dictionary into an array + /// Array the Dictionary will be copied into + /// + /// Starting index at which to begin filling the destination array + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) { + this.typedDictionary.CopyTo(array, arrayIndex); + } + + /// Number of elements contained in the Dictionary + public int Count { + get { return this.typedDictionary.Count; } + } + + /// Creates a new enumerator for the Dictionary + /// The new Dictionary enumerator + public IEnumerator> GetEnumerator() { + return this.typedDictionary.GetEnumerator(); + } + + /// Collection of all keys contained in the Dictionary + public ICollection Keys { + get { + if(this.readonlyKeyCollection == null) { + this.readonlyKeyCollection = new ReadOnlyCollection( + this.typedDictionary.Keys + ); + } + + return this.readonlyKeyCollection; + } + } + + /// Collection of all values contained in the Dictionary + public ICollection Values { + get { + if(this.readonlyValueCollection == null) { + this.readonlyValueCollection = new ReadOnlyCollection( + this.typedDictionary.Values + ); + } + + return this.readonlyValueCollection; + } + } + + /// + /// Attempts to retrieve the item with the specified key from the Dictionary + /// + /// Key of the item to attempt to retrieve + /// + /// Output parameter that will receive the key upon successful completion + /// + /// + /// True if the item was found and has been placed in the output parameter + /// + public bool TryGetValue(KeyType key, out ValueType value) { + return this.typedDictionary.TryGetValue(key, out value); + } + + /// Accesses an item in the Dictionary by its key + /// Key of the item that will be accessed + public ValueType this[KeyType key] { + get { return this.typedDictionary[key]; } + } + + #region IDictionary<,> implementation + + /// Inserts an item into the Dictionary + /// Key under which to add the new item + /// Item that will be added to the Dictionary + void IDictionary.Add(KeyType key, ValueType value) { + throw new NotSupportedException( + "Adding items is not supported by the read-only Dictionary" + ); + } + + /// Removes the item with the specified key from the Dictionary + /// Key of the elementes that will be removed + /// True if an item with the specified key was found and removed + bool IDictionary.Remove(KeyType key) { + throw new NotSupportedException( + "Removing items is not supported by the read-only Dictionary" + ); + } + + /// Accesses an item in the Dictionary by its key + /// Key of the item that will be accessed + ValueType IDictionary.this[KeyType key] { + get { return this.typedDictionary[key]; } + set { + throw new NotSupportedException( + "Assigning items is not supported in a read-only Dictionary" + ); + } + } + + #endregion + + #region IEnumerable implementation + + /// Returns a new object enumerator for the Dictionary + /// The new object enumerator + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { + return (this.typedDictionary as IEnumerable).GetEnumerator(); + } + + #endregion + + #region IDictionary implementation + + /// Removes all items from the Dictionary + void IDictionary.Clear() { + throw new NotSupportedException( + "Clearing is not supported in a read-only Dictionary" + ); + } + + /// Adds an item into the Dictionary + /// Key under which the item will be added + /// Item that will be added + void IDictionary.Add(object key, object value) { + throw new NotSupportedException( + "Adding items is not supported in a read-only Dictionary" + ); + } + + /// Determines whether the specified key exists in the Dictionary + /// Key that will be checked for + /// True if an item with the specified key exists in the Dictionary + bool IDictionary.Contains(object key) { + return this.objectDictionary.Contains(key); + } + + /// Whether the size of the Dictionary is fixed + bool IDictionary.IsFixedSize { + get { return this.objectDictionary.IsFixedSize; } + } + + /// Returns a collection of all keys in the Dictionary + ICollection IDictionary.Keys { + get { + if(this.readonlyKeyCollection == null) { + this.readonlyKeyCollection = new ReadOnlyCollection( + this.typedDictionary.Keys + ); + } + + return this.readonlyKeyCollection; + } + } + + /// Returns a collection of all values stored in the Dictionary + ICollection IDictionary.Values { + get { + if(this.readonlyValueCollection == null) { + this.readonlyValueCollection = new ReadOnlyCollection( + this.typedDictionary.Values + ); + } + + return this.readonlyValueCollection; + } + } + + /// Removes an item from the Dictionary + /// Key of the item that will be removed + void IDictionary.Remove(object key) { + throw new NotSupportedException( + "Removing is not supported by the read-only Dictionary" + ); + } + + /// Accesses an item in the Dictionary by its key + /// Key of the item that will be accessed + /// The item with the specified key + object IDictionary.this[object key] { + get { return this.objectDictionary[key]; } + set { + throw new NotSupportedException( + "Assigning items is not supported by the read-only Dictionary" + ); + } + } + + #endregion // IDictionary implementation + + #region IDictionaryEnumerator implementation + + /// Returns a new entry enumerator for the dictionary + /// The new entry enumerator + IDictionaryEnumerator IDictionary.GetEnumerator() { + return this.objectDictionary.GetEnumerator(); + } + + #endregion // IDictionaryEnumerator implementation + + #region ICollection<> implementation + + /// Inserts an already prepared element into the Dictionary + /// Prepared element that will be added to the Dictionary + void ICollection>.Add( + KeyValuePair item + ) { + throw new NotSupportedException( + "Adding items is not supported by the read-only Dictionary" + ); + } + + /// Removes all items from the Dictionary + void ICollection>.Clear() { + throw new NotSupportedException( + "Clearing is not supported in a read-only Dictionary" + ); + } + + /// Removes all items from the Dictionary + /// Item that will be removed from the Dictionary + bool ICollection>.Remove( + KeyValuePair itemToRemove + ) { + throw new NotSupportedException( + "Removing items is not supported in a read-only Dictionary" + ); + } + + #endregion + + #region ICollection implementation + + /// Copies the contents of the Dictionary into an array + /// Array the Dictionary contents will be copied into + /// + /// Starting index at which to begin filling the destination array + /// + void ICollection.CopyTo(Array array, int index) { + this.objectDictionary.CopyTo(array, index); + } + + /// Whether the Dictionary is synchronized for multi-threaded usage + bool ICollection.IsSynchronized { + get { return this.objectDictionary.IsSynchronized; } + } + + /// Synchronization root on which the Dictionary locks + object ICollection.SyncRoot { + get { return this.objectDictionary.SyncRoot; } + } + + #endregion + +#if !NO_SERIALIZATION + #region ISerializable implementation + + /// Serializes the Dictionary + /// + /// Provides the container into which the Dictionary will serialize itself + /// + /// + /// Contextual informations about the serialization environment + /// + void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) { + (this.typedDictionary as ISerializable).GetObjectData(info, context); + } + + /// Called after all objects have been successfully deserialized + /// Nicht unterstützt + void IDeserializationCallback.OnDeserialization(object sender) { + (this.typedDictionary as IDeserializationCallback).OnDeserialization(sender); + } + + #endregion +#endif //!NO_SERIALIZATION + + /// The wrapped Dictionary under its type-safe interface + private IDictionary typedDictionary; + /// The wrapped Dictionary under its object interface + private IDictionary objectDictionary; + /// ReadOnly wrapper for the keys collection of the Dictionary + private ReadOnlyCollection readonlyKeyCollection; + /// ReadOnly wrapper for the values collection of the Dictionary + private ReadOnlyCollection readonlyValueCollection; + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/ReadOnlyList.Test.cs b/Source/Collections/ReadOnlyList.Test.cs index 6032a7a..886048b 100644 --- a/Source/Collections/ReadOnlyList.Test.cs +++ b/Source/Collections/ReadOnlyList.Test.cs @@ -1,381 +1,380 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections; -using System.Collections.Generic; - -#if UNITTEST - -using NUnit.Framework; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the read only list wrapper - [TestFixture] - internal class ReadOnlyListTest { - - /// - /// Verifies that the copy constructor of the read only list works - /// - [Test] - public void TestCopyConstructor() { - int[] integers = new int[] { 12, 34, 56, 78 }; - ReadOnlyList testList = new ReadOnlyList(integers); - - CollectionAssert.AreEqual(integers, testList); - } - - /// Verifies that the IsReadOnly property returns true - [Test] - public void TestIsReadOnly() { - ReadOnlyList testList = new ReadOnlyList(new int[0]); - - Assert.IsTrue(testList.IsReadOnly); - } - - /// - /// Verifies that the CopyTo() of the read only list works - /// - [Test] - public void TestCopyToArray() { - int[] inputIntegers = new int[] { 12, 34, 56, 78 }; - ReadOnlyList testList = new ReadOnlyList(inputIntegers); - - int[] outputIntegers = new int[testList.Count]; - testList.CopyTo(outputIntegers, 0); - - CollectionAssert.AreEqual(inputIntegers, outputIntegers); - } - - /// - /// Checks whether the Contains() method of the read only list is able to - /// determine if the list contains an item - /// - [Test] - public void TestContains() { - int[] integers = new int[] { 1234, 6789 }; - ReadOnlyList testList = new ReadOnlyList(integers); - - Assert.IsTrue(testList.Contains(1234)); - Assert.IsFalse(testList.Contains(4321)); - } - - /// - /// Checks whether the IndexOf() method of the read only list is able to - /// determine if the index of an item in the list - /// - [Test] - public void TestIndexOf() { - int[] integers = new int[] { 12, 34, 67, 89 }; - ReadOnlyList testList = new ReadOnlyList(integers); - - Assert.AreEqual(0, testList.IndexOf(12)); - Assert.AreEqual(1, testList.IndexOf(34)); - Assert.AreEqual(2, testList.IndexOf(67)); - Assert.AreEqual(3, testList.IndexOf(89)); - } - - /// - /// Checks whether the indexer method of the read only list is able to - /// retrieve items from the list - /// - [Test] - public void TestRetrieveByIndexer() { - int[] integers = new int[] { 12, 34, 67, 89 }; - ReadOnlyList testList = new ReadOnlyList(integers); - - Assert.AreEqual(12, testList[0]); - Assert.AreEqual(34, testList[1]); - Assert.AreEqual(67, testList[2]); - Assert.AreEqual(89, testList[3]); - } - - /// - /// Checks whether the read only list will throw an exception if its Insert() method - /// is called via the generic IList<> interface - /// - [Test] - public void TestThrowOnInsertViaGenericIList() { - ReadOnlyList testList = new ReadOnlyList(new int[0]); - Assert.Throws( - delegate() { (testList as IList).Insert(0, 12345); } - ); - } - - /// - /// Checks whether the read only list will throw an exception if its RemoveAt() method - /// is called via the generic IList<> interface - /// - [Test] - public void TestThrowOnRemoveViaGenericIList() { - ReadOnlyList testList = new ReadOnlyList(new int[1]); - Assert.Throws( - delegate() { (testList as IList).RemoveAt(0); } - ); - } - - /// - /// Checks whether the indexer method of the read only list will throw an exception - /// if it is attempted to be used for replacing an item - /// - [Test] - public void TestRetrieveByIndexerViaGenericIList() { - int[] integers = new int[] { 12, 34, 67, 89 }; - ReadOnlyList testList = new ReadOnlyList(integers); - - Assert.AreEqual(12, (testList as IList)[0]); - Assert.AreEqual(34, (testList as IList)[1]); - Assert.AreEqual(67, (testList as IList)[2]); - Assert.AreEqual(89, (testList as IList)[3]); - } - - /// - /// Checks whether the indexer method of the read only list will throw an exception - /// if it is attempted to be used for replacing an item - /// - [Test] - public void TestThrowOnReplaceByIndexerViaGenericIList() { - ReadOnlyList testList = new ReadOnlyList(new int[1]); - - Assert.Throws( - delegate() { (testList as IList)[0] = 12345; } - ); - } - - /// - /// Checks whether the read only list will throw an exception if its Add() method - /// is called via the generic ICollection<> interface - /// - [Test] - public void TestThrowOnAddViaGenericICollection() { - ReadOnlyList testList = new ReadOnlyList(new int[0]); - Assert.Throws( - delegate() { (testList as ICollection).Add(12345); } - ); - } - - /// - /// Checks whether the read only list will throw an exception if its Clear() method - /// is called via the generic ICollection<> interface - /// - [Test] - public void TestThrowOnClearViaGenericICollection() { - ReadOnlyList testList = new ReadOnlyList(new int[1]); - Assert.Throws( - delegate() { (testList as ICollection).Clear(); } - ); - } - - /// - /// Checks whether the read only list will throw an exception if its Remove() method - /// is called via the generic ICollection<> interface - /// - [Test] - public void TestThrowOnRemoveViaGenericICollection() { - int[] integers = new int[] { 12, 34, 67, 89 }; - ReadOnlyList testList = new ReadOnlyList(integers); - - Assert.Throws( - delegate() { (testList as ICollection).Remove(89); } - ); - } - - /// - /// Tests whether the typesafe enumerator of the read only list is working - /// - [Test] - public void TestTypesafeEnumerator() { - int[] inputIntegers = new int[] { 12, 34, 56, 78 }; - ReadOnlyList testList = new ReadOnlyList(inputIntegers); - - List outputIntegers = new List(); - foreach(int value in testList) { - outputIntegers.Add(value); - } - - CollectionAssert.AreEqual(inputIntegers, outputIntegers); - } - - /// - /// Checks whether the read only list will throw an exception if its Clear() method - /// is called via the IList interface - /// - [Test] - public void TestThrowOnClearViaIList() { - ReadOnlyList testList = new ReadOnlyList(new int[1]); - Assert.Throws( - delegate() { (testList as IList).Clear(); } - ); - } - - /// - /// Checks whether the read only list will throw an exception if its Add() method - /// is called via the IList interface - /// - [Test] - public void TestThrowOnAddViaIList() { - ReadOnlyList testList = new ReadOnlyList(new int[0]); - Assert.Throws( - delegate() { (testList as IList).Add(12345); } - ); - } - - /// - /// Checks whether the Contains() method of the read only list is able to - /// determine if the list contains an item - /// - [Test] - public void TestContainsViaIList() { - int[] integers = new int[] { 1234, 6789 }; - ReadOnlyList testList = new ReadOnlyList(integers); - - Assert.IsTrue((testList as IList).Contains(1234)); - Assert.IsFalse((testList as IList).Contains(4321)); - } - - /// - /// Checks whether the IndexOf() method of the read only list is able to - /// determine if the index of an item in the list - /// - [Test] - public void TestIndexOfViaIList() { - int[] integers = new int[] { 12, 34, 67, 89 }; - ReadOnlyList testList = new ReadOnlyList(integers); - - Assert.AreEqual(0, (testList as IList).IndexOf(12)); - Assert.AreEqual(1, (testList as IList).IndexOf(34)); - Assert.AreEqual(2, (testList as IList).IndexOf(67)); - Assert.AreEqual(3, (testList as IList).IndexOf(89)); - } - - /// - /// Checks whether the read only list will throw an exception if its Insert() method - /// is called via the IList interface - /// - [Test] - public void TestThrowOnInsertViaIList() { - ReadOnlyList testList = new ReadOnlyList(new int[0]); - Assert.Throws( - delegate() { (testList as IList).Insert(0, 12345); } - ); - } - - /// - /// Checks whether the IsFixedSize property of the read only list returns the - /// expected result for a read only list based on a fixed array - /// - [Test] - public void TestIsFixedSizeViaIList() { - int[] integers = new int[] { 12, 34, 67, 89 }; - ReadOnlyList testList = new ReadOnlyList(integers); - - Assert.IsTrue((testList as IList).IsFixedSize); - } - - /// - /// Checks whether the read only list will throw an exception if its Remove() method - /// is called via the IList interface - /// - [Test] - public void TestThrowOnRemoveViaIList() { - int[] integers = new int[] { 1234, 6789 }; - ReadOnlyList testList = new ReadOnlyList(integers); - - Assert.Throws( - delegate() { (testList as IList).Remove(6789); } - ); - } - - /// - /// Checks whether the read only list will throw an exception if its Remove() method - /// is called via the IList interface - /// - [Test] - public void TestThrowOnRemoveAtViaIList() { - ReadOnlyList testList = new ReadOnlyList(new int[1]); - - Assert.Throws( - delegate() { (testList as IList).RemoveAt(0); } - ); - } - - /// - /// Checks whether the indexer method of the read only list will throw an exception - /// if it is attempted to be used for replacing an item - /// - [Test] - public void TestRetrieveByIndexerViaIList() { - int[] integers = new int[] { 12, 34, 67, 89 }; - ReadOnlyList testList = new ReadOnlyList(integers); - - Assert.AreEqual(12, (testList as IList)[0]); - Assert.AreEqual(34, (testList as IList)[1]); - Assert.AreEqual(67, (testList as IList)[2]); - Assert.AreEqual(89, (testList as IList)[3]); - } - - /// - /// Checks whether the indexer method of the read only list will throw an exception - /// if it is attempted to be used for replacing an item - /// - [Test] - public void TestThrowOnReplaceByIndexerViaIList() { - ReadOnlyList testList = new ReadOnlyList(new int[1]); - - Assert.Throws( - delegate() { (testList as IList)[0] = 12345; } - ); - } - - /// - /// Verifies that the CopyTo() of the read only list works if invoked via - /// the ICollection interface - /// - [Test] - public void TestCopyToArrayViaICollection() { - int[] inputIntegers = new int[] { 12, 34, 56, 78 }; - ReadOnlyList testList = new ReadOnlyList(inputIntegers); - - int[] outputIntegers = new int[testList.Count]; - (testList as ICollection).CopyTo(outputIntegers, 0); - - CollectionAssert.AreEqual(inputIntegers, outputIntegers); - } - - /// - /// Verifies that the IsSynchronized property and the SyncRoot property are working - /// - [Test] - public void TestSynchronization() { - ReadOnlyList testList = new ReadOnlyList(new int[0]); - - if(!(testList as ICollection).IsSynchronized) { - lock((testList as ICollection).SyncRoot) { - Assert.AreEqual(0, testList.Count); - } - } - } - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections; +using System.Collections.Generic; + +#if UNITTEST + +using NUnit.Framework; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the read only list wrapper + [TestFixture] + internal class ReadOnlyListTest { + + /// + /// Verifies that the copy constructor of the read only list works + /// + [Test] + public void TestCopyConstructor() { + int[] integers = new int[] { 12, 34, 56, 78 }; + ReadOnlyList testList = new ReadOnlyList(integers); + + CollectionAssert.AreEqual(integers, testList); + } + + /// Verifies that the IsReadOnly property returns true + [Test] + public void TestIsReadOnly() { + ReadOnlyList testList = new ReadOnlyList(new int[0]); + + Assert.IsTrue(testList.IsReadOnly); + } + + /// + /// Verifies that the CopyTo() of the read only list works + /// + [Test] + public void TestCopyToArray() { + int[] inputIntegers = new int[] { 12, 34, 56, 78 }; + ReadOnlyList testList = new ReadOnlyList(inputIntegers); + + int[] outputIntegers = new int[testList.Count]; + testList.CopyTo(outputIntegers, 0); + + CollectionAssert.AreEqual(inputIntegers, outputIntegers); + } + + /// + /// Checks whether the Contains() method of the read only list is able to + /// determine if the list contains an item + /// + [Test] + public void TestContains() { + int[] integers = new int[] { 1234, 6789 }; + ReadOnlyList testList = new ReadOnlyList(integers); + + Assert.IsTrue(testList.Contains(1234)); + Assert.IsFalse(testList.Contains(4321)); + } + + /// + /// Checks whether the IndexOf() method of the read only list is able to + /// determine if the index of an item in the list + /// + [Test] + public void TestIndexOf() { + int[] integers = new int[] { 12, 34, 67, 89 }; + ReadOnlyList testList = new ReadOnlyList(integers); + + Assert.AreEqual(0, testList.IndexOf(12)); + Assert.AreEqual(1, testList.IndexOf(34)); + Assert.AreEqual(2, testList.IndexOf(67)); + Assert.AreEqual(3, testList.IndexOf(89)); + } + + /// + /// Checks whether the indexer method of the read only list is able to + /// retrieve items from the list + /// + [Test] + public void TestRetrieveByIndexer() { + int[] integers = new int[] { 12, 34, 67, 89 }; + ReadOnlyList testList = new ReadOnlyList(integers); + + Assert.AreEqual(12, testList[0]); + Assert.AreEqual(34, testList[1]); + Assert.AreEqual(67, testList[2]); + Assert.AreEqual(89, testList[3]); + } + + /// + /// Checks whether the read only list will throw an exception if its Insert() method + /// is called via the generic IList<> interface + /// + [Test] + public void TestThrowOnInsertViaGenericIList() { + ReadOnlyList testList = new ReadOnlyList(new int[0]); + Assert.Throws( + delegate() { (testList as IList).Insert(0, 12345); } + ); + } + + /// + /// Checks whether the read only list will throw an exception if its RemoveAt() method + /// is called via the generic IList<> interface + /// + [Test] + public void TestThrowOnRemoveViaGenericIList() { + ReadOnlyList testList = new ReadOnlyList(new int[1]); + Assert.Throws( + delegate() { (testList as IList).RemoveAt(0); } + ); + } + + /// + /// Checks whether the indexer method of the read only list will throw an exception + /// if it is attempted to be used for replacing an item + /// + [Test] + public void TestRetrieveByIndexerViaGenericIList() { + int[] integers = new int[] { 12, 34, 67, 89 }; + ReadOnlyList testList = new ReadOnlyList(integers); + + Assert.AreEqual(12, (testList as IList)[0]); + Assert.AreEqual(34, (testList as IList)[1]); + Assert.AreEqual(67, (testList as IList)[2]); + Assert.AreEqual(89, (testList as IList)[3]); + } + + /// + /// Checks whether the indexer method of the read only list will throw an exception + /// if it is attempted to be used for replacing an item + /// + [Test] + public void TestThrowOnReplaceByIndexerViaGenericIList() { + ReadOnlyList testList = new ReadOnlyList(new int[1]); + + Assert.Throws( + delegate() { (testList as IList)[0] = 12345; } + ); + } + + /// + /// Checks whether the read only list will throw an exception if its Add() method + /// is called via the generic ICollection<> interface + /// + [Test] + public void TestThrowOnAddViaGenericICollection() { + ReadOnlyList testList = new ReadOnlyList(new int[0]); + Assert.Throws( + delegate() { (testList as ICollection).Add(12345); } + ); + } + + /// + /// Checks whether the read only list will throw an exception if its Clear() method + /// is called via the generic ICollection<> interface + /// + [Test] + public void TestThrowOnClearViaGenericICollection() { + ReadOnlyList testList = new ReadOnlyList(new int[1]); + Assert.Throws( + delegate() { (testList as ICollection).Clear(); } + ); + } + + /// + /// Checks whether the read only list will throw an exception if its Remove() method + /// is called via the generic ICollection<> interface + /// + [Test] + public void TestThrowOnRemoveViaGenericICollection() { + int[] integers = new int[] { 12, 34, 67, 89 }; + ReadOnlyList testList = new ReadOnlyList(integers); + + Assert.Throws( + delegate() { (testList as ICollection).Remove(89); } + ); + } + + /// + /// Tests whether the typesafe enumerator of the read only list is working + /// + [Test] + public void TestTypesafeEnumerator() { + int[] inputIntegers = new int[] { 12, 34, 56, 78 }; + ReadOnlyList testList = new ReadOnlyList(inputIntegers); + + List outputIntegers = new List(); + foreach(int value in testList) { + outputIntegers.Add(value); + } + + CollectionAssert.AreEqual(inputIntegers, outputIntegers); + } + + /// + /// Checks whether the read only list will throw an exception if its Clear() method + /// is called via the IList interface + /// + [Test] + public void TestThrowOnClearViaIList() { + ReadOnlyList testList = new ReadOnlyList(new int[1]); + Assert.Throws( + delegate() { (testList as IList).Clear(); } + ); + } + + /// + /// Checks whether the read only list will throw an exception if its Add() method + /// is called via the IList interface + /// + [Test] + public void TestThrowOnAddViaIList() { + ReadOnlyList testList = new ReadOnlyList(new int[0]); + Assert.Throws( + delegate() { (testList as IList).Add(12345); } + ); + } + + /// + /// Checks whether the Contains() method of the read only list is able to + /// determine if the list contains an item + /// + [Test] + public void TestContainsViaIList() { + int[] integers = new int[] { 1234, 6789 }; + ReadOnlyList testList = new ReadOnlyList(integers); + + Assert.IsTrue((testList as IList).Contains(1234)); + Assert.IsFalse((testList as IList).Contains(4321)); + } + + /// + /// Checks whether the IndexOf() method of the read only list is able to + /// determine if the index of an item in the list + /// + [Test] + public void TestIndexOfViaIList() { + int[] integers = new int[] { 12, 34, 67, 89 }; + ReadOnlyList testList = new ReadOnlyList(integers); + + Assert.AreEqual(0, (testList as IList).IndexOf(12)); + Assert.AreEqual(1, (testList as IList).IndexOf(34)); + Assert.AreEqual(2, (testList as IList).IndexOf(67)); + Assert.AreEqual(3, (testList as IList).IndexOf(89)); + } + + /// + /// Checks whether the read only list will throw an exception if its Insert() method + /// is called via the IList interface + /// + [Test] + public void TestThrowOnInsertViaIList() { + ReadOnlyList testList = new ReadOnlyList(new int[0]); + Assert.Throws( + delegate() { (testList as IList).Insert(0, 12345); } + ); + } + + /// + /// Checks whether the IsFixedSize property of the read only list returns the + /// expected result for a read only list based on a fixed array + /// + [Test] + public void TestIsFixedSizeViaIList() { + int[] integers = new int[] { 12, 34, 67, 89 }; + ReadOnlyList testList = new ReadOnlyList(integers); + + Assert.IsTrue((testList as IList).IsFixedSize); + } + + /// + /// Checks whether the read only list will throw an exception if its Remove() method + /// is called via the IList interface + /// + [Test] + public void TestThrowOnRemoveViaIList() { + int[] integers = new int[] { 1234, 6789 }; + ReadOnlyList testList = new ReadOnlyList(integers); + + Assert.Throws( + delegate() { (testList as IList).Remove(6789); } + ); + } + + /// + /// Checks whether the read only list will throw an exception if its Remove() method + /// is called via the IList interface + /// + [Test] + public void TestThrowOnRemoveAtViaIList() { + ReadOnlyList testList = new ReadOnlyList(new int[1]); + + Assert.Throws( + delegate() { (testList as IList).RemoveAt(0); } + ); + } + + /// + /// Checks whether the indexer method of the read only list will throw an exception + /// if it is attempted to be used for replacing an item + /// + [Test] + public void TestRetrieveByIndexerViaIList() { + int[] integers = new int[] { 12, 34, 67, 89 }; + ReadOnlyList testList = new ReadOnlyList(integers); + + Assert.AreEqual(12, (testList as IList)[0]); + Assert.AreEqual(34, (testList as IList)[1]); + Assert.AreEqual(67, (testList as IList)[2]); + Assert.AreEqual(89, (testList as IList)[3]); + } + + /// + /// Checks whether the indexer method of the read only list will throw an exception + /// if it is attempted to be used for replacing an item + /// + [Test] + public void TestThrowOnReplaceByIndexerViaIList() { + ReadOnlyList testList = new ReadOnlyList(new int[1]); + + Assert.Throws( + delegate() { (testList as IList)[0] = 12345; } + ); + } + + /// + /// Verifies that the CopyTo() of the read only list works if invoked via + /// the ICollection interface + /// + [Test] + public void TestCopyToArrayViaICollection() { + int[] inputIntegers = new int[] { 12, 34, 56, 78 }; + ReadOnlyList testList = new ReadOnlyList(inputIntegers); + + int[] outputIntegers = new int[testList.Count]; + (testList as ICollection).CopyTo(outputIntegers, 0); + + CollectionAssert.AreEqual(inputIntegers, outputIntegers); + } + + /// + /// Verifies that the IsSynchronized property and the SyncRoot property are working + /// + [Test] + public void TestSynchronization() { + ReadOnlyList testList = new ReadOnlyList(new int[0]); + + if(!(testList as ICollection).IsSynchronized) { + lock((testList as ICollection).SyncRoot) { + Assert.AreEqual(0, testList.Count); + } + } + } + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST diff --git a/Source/Collections/ReadOnlyList.cs b/Source/Collections/ReadOnlyList.cs index 1020970..28691b7 100644 --- a/Source/Collections/ReadOnlyList.cs +++ b/Source/Collections/ReadOnlyList.cs @@ -1,258 +1,257 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Nuclex.Support.Collections { - - /// Wraps a list and prevents users from modifying it - /// Type of items to manage in the set - public class ReadOnlyList : IList, IList { - - /// Initializes a new read-only List wrapper - /// List that will be wrapped - public ReadOnlyList(IList list) { - this.typedList = list; - this.objectList = (list as IList); - } - - /// Retrieves the index of an item within the List - /// Item whose index will be returned - /// The zero-based index of the specified item in the List - public int IndexOf(TItem item) { - return this.typedList.IndexOf(item); - } - - /// Accesses the List item with the specified index - /// Zero-based index of the List item that will be accessed - public TItem this[int index] { - get { return this.typedList[index]; } - } - - /// Determines whether the List contains the specified item - /// Item that will be checked for - /// True if the specified item is contained in the List - public bool Contains(TItem item) { - return this.typedList.Contains(item); - } - - /// Copies the contents of the List into an array - /// Array the List will be copied into - /// - /// Starting index at which to begin filling the destination array - /// - public void CopyTo(TItem[] array, int arrayIndex) { - this.typedList.CopyTo(array, arrayIndex); - } - - /// The number of items current contained in the list - public int Count { - get { return this.typedList.Count; } - } - - /// Whether the list is write-protected - public bool IsReadOnly { - get { return true; } - } - - /// Returns a new enumerator over the contents of the list - /// The new list content enumerator - public IEnumerator GetEnumerator() { - return this.typedList.GetEnumerator(); - } - - #region IList<> implementation - - /// Inserts an item into the list - /// Zero-based index before which the item will be inserted - /// Item that will be inserted into the list - void IList.Insert(int index, TItem item) { - throw new NotSupportedException( - "Inserting items is not supported by the read-only list" - ); - } - - /// Removes an item from the list - /// Zero-based index of the item that will be removed - void IList.RemoveAt(int index) { - throw new NotSupportedException( - "Removing items is not supported by the read-only list" - ); - } - - /// Accesses the list item with the specified index - /// Zero-based index of the list item that will be accessed - TItem IList.this[int index] { - get { return this.typedList[index]; } - set { - throw new NotSupportedException( - "Assigning items is not supported by the read-only list" - ); - } - } - - #endregion - - #region ICollection<> implementation - - /// Adds an item to the end of the list - /// Item that will be added to the list - void ICollection.Add(TItem item) { - throw new NotSupportedException( - "Adding items is not supported by the read-only list" - ); - } - - /// Removes all items from the List - void ICollection.Clear() { - throw new NotSupportedException( - "Clearing is not supported by the read-only list" - ); - } - - /// Removes the specified item from the list - /// Item that will be removed from the list - /// True of the specified item was found in the list and removed - bool ICollection.Remove(TItem item) { - throw new NotSupportedException( - "Removing items is not supported by the read-only list" - ); - } - - #endregion - - #region IEnumerable implementation - - /// Returns a new enumerator over the contents of the list - /// The new list content enumerator - IEnumerator IEnumerable.GetEnumerator() { - return this.objectList.GetEnumerator(); - } - - #endregion - - #region IList implementation - - /// Removes all items from the list - void IList.Clear() { - throw new NotSupportedException( - "Clearing is not supported by the read-only list" - ); - } - - /// Adds an item to the end of the list - /// Item that will be added to the list - int IList.Add(object value) { - throw new NotSupportedException( - "Adding items is not supported by the read-only list" - ); - } - - /// Determines whether the List contains the specified item - /// Item that will be checked for - /// True if the specified item is contained in the list - bool IList.Contains(object value) { - return this.objectList.Contains(value); - } - - /// Retrieves the index of an item within the list - /// Item whose index will be returned - /// The zero-based index of the specified item in the list - int IList.IndexOf(object value) { - return this.objectList.IndexOf(value); - } - - /// Inserts an item into the list - /// Zero-based index before which the item will be inserted - /// Item that will be inserted into the list - void IList.Insert(int index, object value) { - throw new NotSupportedException( - "Inserting items is not supported by the read-only list" - ); - } - - /// Whether the size of the list is fixed - bool IList.IsFixedSize { - get { return this.objectList.IsFixedSize; } - } - - /// Removes the specified item from the list - /// Item that will be removed from the list - /// True of the specified item was found in the list and removed - void IList.Remove(object value) { - throw new NotSupportedException( - "Removing items is not supported by the read-only list" - ); - } - - /// Removes an item from the list - /// Zero-based index of the item that will be removed - void IList.RemoveAt(int index) { - throw new NotSupportedException( - "Removing items is not supported by the read-only list" - ); - } - - /// Accesses the list item with the specified index - /// Zero-based index of the list item that will be accessed - object IList.this[int index] { - get { return this.objectList[index]; } - set { - throw new NotSupportedException( - "Assigning items is not supported by the read-only list" - ); - } - } - - #endregion - - #region ICollection implementation - - /// Copies the contents of the list into an array - /// Array the list will be copied into - /// - /// Starting index at which to begin filling the destination array - /// - void ICollection.CopyTo(Array array, int index) { - this.objectList.CopyTo(array, index); - } - - /// Whether the list is synchronized for multi-threaded usage - bool ICollection.IsSynchronized { - get { return this.objectList.IsSynchronized; } - } - - /// Synchronization root on which the list locks - object ICollection.SyncRoot { - get { return this.objectList.SyncRoot; } - } - - #endregion - - /// The wrapped list under its type-safe interface - private IList typedList; - /// The wrapped list under its object interface - private IList objectList; - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Nuclex.Support.Collections { + + /// Wraps a list and prevents users from modifying it + /// Type of items to manage in the set + public class ReadOnlyList : IList, IList { + + /// Initializes a new read-only List wrapper + /// List that will be wrapped + public ReadOnlyList(IList list) { + this.typedList = list; + this.objectList = (list as IList); + } + + /// Retrieves the index of an item within the List + /// Item whose index will be returned + /// The zero-based index of the specified item in the List + public int IndexOf(TItem item) { + return this.typedList.IndexOf(item); + } + + /// Accesses the List item with the specified index + /// Zero-based index of the List item that will be accessed + public TItem this[int index] { + get { return this.typedList[index]; } + } + + /// Determines whether the List contains the specified item + /// Item that will be checked for + /// True if the specified item is contained in the List + public bool Contains(TItem item) { + return this.typedList.Contains(item); + } + + /// Copies the contents of the List into an array + /// Array the List will be copied into + /// + /// Starting index at which to begin filling the destination array + /// + public void CopyTo(TItem[] array, int arrayIndex) { + this.typedList.CopyTo(array, arrayIndex); + } + + /// The number of items current contained in the list + public int Count { + get { return this.typedList.Count; } + } + + /// Whether the list is write-protected + public bool IsReadOnly { + get { return true; } + } + + /// Returns a new enumerator over the contents of the list + /// The new list content enumerator + public IEnumerator GetEnumerator() { + return this.typedList.GetEnumerator(); + } + + #region IList<> implementation + + /// Inserts an item into the list + /// Zero-based index before which the item will be inserted + /// Item that will be inserted into the list + void IList.Insert(int index, TItem item) { + throw new NotSupportedException( + "Inserting items is not supported by the read-only list" + ); + } + + /// Removes an item from the list + /// Zero-based index of the item that will be removed + void IList.RemoveAt(int index) { + throw new NotSupportedException( + "Removing items is not supported by the read-only list" + ); + } + + /// Accesses the list item with the specified index + /// Zero-based index of the list item that will be accessed + TItem IList.this[int index] { + get { return this.typedList[index]; } + set { + throw new NotSupportedException( + "Assigning items is not supported by the read-only list" + ); + } + } + + #endregion + + #region ICollection<> implementation + + /// Adds an item to the end of the list + /// Item that will be added to the list + void ICollection.Add(TItem item) { + throw new NotSupportedException( + "Adding items is not supported by the read-only list" + ); + } + + /// Removes all items from the List + void ICollection.Clear() { + throw new NotSupportedException( + "Clearing is not supported by the read-only list" + ); + } + + /// Removes the specified item from the list + /// Item that will be removed from the list + /// True of the specified item was found in the list and removed + bool ICollection.Remove(TItem item) { + throw new NotSupportedException( + "Removing items is not supported by the read-only list" + ); + } + + #endregion + + #region IEnumerable implementation + + /// Returns a new enumerator over the contents of the list + /// The new list content enumerator + IEnumerator IEnumerable.GetEnumerator() { + return this.objectList.GetEnumerator(); + } + + #endregion + + #region IList implementation + + /// Removes all items from the list + void IList.Clear() { + throw new NotSupportedException( + "Clearing is not supported by the read-only list" + ); + } + + /// Adds an item to the end of the list + /// Item that will be added to the list + int IList.Add(object value) { + throw new NotSupportedException( + "Adding items is not supported by the read-only list" + ); + } + + /// Determines whether the List contains the specified item + /// Item that will be checked for + /// True if the specified item is contained in the list + bool IList.Contains(object value) { + return this.objectList.Contains(value); + } + + /// Retrieves the index of an item within the list + /// Item whose index will be returned + /// The zero-based index of the specified item in the list + int IList.IndexOf(object value) { + return this.objectList.IndexOf(value); + } + + /// Inserts an item into the list + /// Zero-based index before which the item will be inserted + /// Item that will be inserted into the list + void IList.Insert(int index, object value) { + throw new NotSupportedException( + "Inserting items is not supported by the read-only list" + ); + } + + /// Whether the size of the list is fixed + bool IList.IsFixedSize { + get { return this.objectList.IsFixedSize; } + } + + /// Removes the specified item from the list + /// Item that will be removed from the list + /// True of the specified item was found in the list and removed + void IList.Remove(object value) { + throw new NotSupportedException( + "Removing items is not supported by the read-only list" + ); + } + + /// Removes an item from the list + /// Zero-based index of the item that will be removed + void IList.RemoveAt(int index) { + throw new NotSupportedException( + "Removing items is not supported by the read-only list" + ); + } + + /// Accesses the list item with the specified index + /// Zero-based index of the list item that will be accessed + object IList.this[int index] { + get { return this.objectList[index]; } + set { + throw new NotSupportedException( + "Assigning items is not supported by the read-only list" + ); + } + } + + #endregion + + #region ICollection implementation + + /// Copies the contents of the list into an array + /// Array the list will be copied into + /// + /// Starting index at which to begin filling the destination array + /// + void ICollection.CopyTo(Array array, int index) { + this.objectList.CopyTo(array, index); + } + + /// Whether the list is synchronized for multi-threaded usage + bool ICollection.IsSynchronized { + get { return this.objectList.IsSynchronized; } + } + + /// Synchronization root on which the list locks + object ICollection.SyncRoot { + get { return this.objectList.SyncRoot; } + } + + #endregion + + /// The wrapped list under its type-safe interface + private IList typedList; + /// The wrapped list under its object interface + private IList objectList; + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/ReadOnlySet.Test.cs b/Source/Collections/ReadOnlySet.Test.cs index a4787e1..78ab330 100644 --- a/Source/Collections/ReadOnlySet.Test.cs +++ b/Source/Collections/ReadOnlySet.Test.cs @@ -1,208 +1,207 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if !NO_SETS - -#if UNITTEST - -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Runtime.Serialization.Formatters.Binary; - -using NUnit.Framework; -using NMock; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the observable set wrapper - [TestFixture] - internal class ReadOnlySetTest { - - /// Called before each test is run - [SetUp] - public void Setup() { - this.set = new HashSet(); - this.readOnlySet = new ReadOnlySet(this.set); - } - - /// - /// Verifies that the observable set has a default constructor - /// - [Test] - public void HasDefaultConstructor() { - Assert.IsNotNull(new ReadOnlySet(new HashSet())); - } - - /// - /// Verifies that an exception is thrown upon any attempt to add items - /// to a read-only set - /// - [Test] - public void AddingThrowsException() { - Assert.Throws( - delegate() { ((ISet)this.readOnlySet).Add(123); } - ); - } - - /// - /// Verifies that an exception is thrown upon any attempt to remove items - /// from a read-only set - /// - [Test] - public void RemovingThrowsException() { - Assert.Throws( - delegate() { ((ISet)this.readOnlySet).Remove(123); } - ); - } - - /// - /// Verifies that an exception is thrown upon any attempt to except - /// the set with another set - /// - [Test] - public void ExceptingThrowsException() { - Assert.Throws( - delegate() { ((ISet)this.readOnlySet).ExceptWith(null); } - ); - } - - /// - /// Verifies that an exception is thrown upon any attempt to intersect - /// the set with another set - /// - [Test] - public void InsersectThrowsException() { - Assert.Throws( - delegate() { ((ISet)this.readOnlySet).IntersectWith(null); } - ); - } - - /// - /// Verifies that it's possible to determine whether a set is a proper subset - /// or superset of another set - /// - [Test] - public void CanDetermineProperSubsetAndSuperset() { - this.set.Add(1); - this.set.Add(2); - this.set.Add(3); - - var set2 = new HashSet() { 1, 3 }; - - Assert.IsTrue(this.readOnlySet.IsProperSupersetOf(set2)); - Assert.IsTrue(set2.IsProperSubsetOf(this.readOnlySet)); - - set2.Add(2); - - Assert.IsFalse(this.readOnlySet.IsProperSupersetOf(set2)); - Assert.IsFalse(set2.IsProperSubsetOf(this.readOnlySet)); - } - - /// - /// Verifies that it's possible to determine whether a set is a subset - /// or a superset of another set - /// - [Test] - public void CanDetermineSubsetAndSuperset() { - this.set.Add(1); - this.set.Add(2); - this.set.Add(3); - - var set2 = new HashSet() { 1, 2, 3 }; - - Assert.IsTrue(this.readOnlySet.IsSupersetOf(set2)); - Assert.IsTrue(set2.IsSubsetOf(this.readOnlySet)); - - set2.Add(4); - - Assert.IsFalse(this.readOnlySet.IsSupersetOf(set2)); - Assert.IsFalse(set2.IsSubsetOf(this.readOnlySet)); - } - - /// - /// Verifies that a set can determine if another set overlaps with it - /// - [Test] - public void CanDetermineOverlap() { - this.set.Add(1); - this.set.Add(3); - this.set.Add(5); - - var set2 = new HashSet() { 3 }; - - Assert.IsTrue(this.readOnlySet.Overlaps(set2)); - Assert.IsTrue(set2.Overlaps(this.readOnlySet)); - } - - /// - /// Verifies that a set can determine if another set contains the same elements - /// - [Test] - public void CanDetermineSetEquality() { - this.set.Add(1); - this.set.Add(3); - this.set.Add(5); - - var set2 = new HashSet() { 3, 1, 5 }; - - Assert.IsTrue(this.readOnlySet.SetEquals(set2)); - Assert.IsTrue(set2.SetEquals(this.readOnlySet)); - - this.set.Add(7); - - Assert.IsFalse(this.readOnlySet.SetEquals(set2)); - Assert.IsFalse(set2.SetEquals(this.readOnlySet)); - } - - /// - /// Verifies that any attempt to symmetrically except a read-only set - /// causes an exception - /// - [Test] - public void SymmetricallyExceptingThrowsException() { - Assert.Throws( - delegate() { ((ISet)this.readOnlySet).SymmetricExceptWith(null); } - ); - } - - /// - /// Verifies that any attempt to union a read-only set causes an exception - /// - [Test] - public void UnioningThrowsException() { - Assert.Throws( - delegate() { ((ISet)this.readOnlySet).UnionWith(null); } - ); - } - - /// Set being wrapped in a read-only set - private ISet set; - /// Read-only wrapper around the set - private ReadOnlySet readOnlySet; - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST - -#endif // !NO_SETS +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if !NO_SETS + +#if UNITTEST + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; + +using NUnit.Framework; +using NMock; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the observable set wrapper + [TestFixture] + internal class ReadOnlySetTest { + + /// Called before each test is run + [SetUp] + public void Setup() { + this.set = new HashSet(); + this.readOnlySet = new ReadOnlySet(this.set); + } + + /// + /// Verifies that the observable set has a default constructor + /// + [Test] + public void HasDefaultConstructor() { + Assert.IsNotNull(new ReadOnlySet(new HashSet())); + } + + /// + /// Verifies that an exception is thrown upon any attempt to add items + /// to a read-only set + /// + [Test] + public void AddingThrowsException() { + Assert.Throws( + delegate() { ((ISet)this.readOnlySet).Add(123); } + ); + } + + /// + /// Verifies that an exception is thrown upon any attempt to remove items + /// from a read-only set + /// + [Test] + public void RemovingThrowsException() { + Assert.Throws( + delegate() { ((ISet)this.readOnlySet).Remove(123); } + ); + } + + /// + /// Verifies that an exception is thrown upon any attempt to except + /// the set with another set + /// + [Test] + public void ExceptingThrowsException() { + Assert.Throws( + delegate() { ((ISet)this.readOnlySet).ExceptWith(null); } + ); + } + + /// + /// Verifies that an exception is thrown upon any attempt to intersect + /// the set with another set + /// + [Test] + public void InsersectThrowsException() { + Assert.Throws( + delegate() { ((ISet)this.readOnlySet).IntersectWith(null); } + ); + } + + /// + /// Verifies that it's possible to determine whether a set is a proper subset + /// or superset of another set + /// + [Test] + public void CanDetermineProperSubsetAndSuperset() { + this.set.Add(1); + this.set.Add(2); + this.set.Add(3); + + var set2 = new HashSet() { 1, 3 }; + + Assert.IsTrue(this.readOnlySet.IsProperSupersetOf(set2)); + Assert.IsTrue(set2.IsProperSubsetOf(this.readOnlySet)); + + set2.Add(2); + + Assert.IsFalse(this.readOnlySet.IsProperSupersetOf(set2)); + Assert.IsFalse(set2.IsProperSubsetOf(this.readOnlySet)); + } + + /// + /// Verifies that it's possible to determine whether a set is a subset + /// or a superset of another set + /// + [Test] + public void CanDetermineSubsetAndSuperset() { + this.set.Add(1); + this.set.Add(2); + this.set.Add(3); + + var set2 = new HashSet() { 1, 2, 3 }; + + Assert.IsTrue(this.readOnlySet.IsSupersetOf(set2)); + Assert.IsTrue(set2.IsSubsetOf(this.readOnlySet)); + + set2.Add(4); + + Assert.IsFalse(this.readOnlySet.IsSupersetOf(set2)); + Assert.IsFalse(set2.IsSubsetOf(this.readOnlySet)); + } + + /// + /// Verifies that a set can determine if another set overlaps with it + /// + [Test] + public void CanDetermineOverlap() { + this.set.Add(1); + this.set.Add(3); + this.set.Add(5); + + var set2 = new HashSet() { 3 }; + + Assert.IsTrue(this.readOnlySet.Overlaps(set2)); + Assert.IsTrue(set2.Overlaps(this.readOnlySet)); + } + + /// + /// Verifies that a set can determine if another set contains the same elements + /// + [Test] + public void CanDetermineSetEquality() { + this.set.Add(1); + this.set.Add(3); + this.set.Add(5); + + var set2 = new HashSet() { 3, 1, 5 }; + + Assert.IsTrue(this.readOnlySet.SetEquals(set2)); + Assert.IsTrue(set2.SetEquals(this.readOnlySet)); + + this.set.Add(7); + + Assert.IsFalse(this.readOnlySet.SetEquals(set2)); + Assert.IsFalse(set2.SetEquals(this.readOnlySet)); + } + + /// + /// Verifies that any attempt to symmetrically except a read-only set + /// causes an exception + /// + [Test] + public void SymmetricallyExceptingThrowsException() { + Assert.Throws( + delegate() { ((ISet)this.readOnlySet).SymmetricExceptWith(null); } + ); + } + + /// + /// Verifies that any attempt to union a read-only set causes an exception + /// + [Test] + public void UnioningThrowsException() { + Assert.Throws( + delegate() { ((ISet)this.readOnlySet).UnionWith(null); } + ); + } + + /// Set being wrapped in a read-only set + private ISet set; + /// Read-only wrapper around the set + private ReadOnlySet readOnlySet; + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST + +#endif // !NO_SETS diff --git a/Source/Collections/ReadOnlySet.cs b/Source/Collections/ReadOnlySet.cs index 5c5f4eb..adbdeb7 100644 --- a/Source/Collections/ReadOnlySet.cs +++ b/Source/Collections/ReadOnlySet.cs @@ -1,216 +1,215 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections; -using System.Collections.Generic; - -#if !NO_SETS - -namespace Nuclex.Support.Collections { - - /// Wraps a set and prevents it from being modified - /// Type of items to manage in the set - public class ReadOnlySet : ISet, ICollection { - - /// - /// Initializes a new observable set forwarding operations to the specified set - /// - /// Set operations will be forwarded to - public ReadOnlySet(ISet set) { - this.set = set; - } - - /// - /// Determines whether the current set is a proper (strict) subset of a collection - /// - /// Collection against which the set will be tested - /// True if the set is a proper subset of the specified collection - public bool IsProperSubsetOf(IEnumerable other) { - return this.set.IsProperSubsetOf(other); - } - - /// - /// Determines whether the current set is a proper (strict) superset of a collection - /// - /// Collection against which the set will be tested - /// True if the set is a proper superset of the specified collection - public bool IsProperSupersetOf(IEnumerable other) { - return this.set.IsProperSupersetOf(other); - } - - /// Determines whether the current set is a subset of a collection - /// Collection against which the set will be tested - /// True if the set is a subset of the specified collection - public bool IsSubsetOf(IEnumerable other) { - return this.set.IsSubsetOf(other); - } - - /// Determines whether the current set is a superset of a collection - /// Collection against which the set will be tested - /// True if the set is a superset of the specified collection - public bool IsSupersetOf(IEnumerable other) { - return this.set.IsSupersetOf(other); - } - - /// - /// Determines if the set shares at least one common element with the collection - /// - /// Collection the set will be tested against - /// - /// True if the set shares at least one common element with the collection - /// - public bool Overlaps(IEnumerable other) { - return this.set.Overlaps(other); - } - - /// - /// Determines whether the set contains the same elements as the specified collection - /// - /// Collection the set will be tested against - /// True if the set contains the same elements as the collection - public bool SetEquals(IEnumerable other) { - return this.set.SetEquals(other); - } - - /// Determines whether the set contains the specified item - /// Item the set will be tested for - /// True if the set contains the specified item - public bool Contains(TItem item) { - return this.set.Contains(item); - } - - /// Copies the contents of the set into an array - /// Array the set's contents will be copied to - /// - /// Index in the array the first copied element will be written to - /// - public void CopyTo(TItem[] array, int arrayIndex) { - this.set.CopyTo(array, arrayIndex); - } - - /// Counts the number of items contained in the set - public int Count { - get { return this.set.Count; } - } - - /// Determines whether the set is readonly - public bool IsReadOnly { - get { return true; } - } - - - /// Creates an enumerator for the set's contents - /// A new enumerator for the sets contents - public IEnumerator GetEnumerator() { - return this.set.GetEnumerator(); - } - - /// Creates an enumerator for the set's contents - /// A new enumerator for the sets contents - IEnumerator IEnumerable.GetEnumerator() { - return this.set.GetEnumerator(); - } - - /// - /// Modifies the current set so that it contains only elements that are present either - /// in the current set or in the specified collection, but not both - /// - /// Collection the set will be excepted with - void ISet.SymmetricExceptWith(IEnumerable other) { - throw new NotSupportedException( - "Excepting is not supported by the read-only set" - ); - } - - /// - /// Modifies the current set so that it contains all elements that are present in both - /// the current set and in the specified collection - /// - /// Collection an union will be built with - void ISet.UnionWith(IEnumerable other) { - throw new NotSupportedException( - "Unioning is not supported by the read-only set" - ); - } - - /// Removes all items from the set - public void Clear() { - throw new NotSupportedException( - "Clearing is not supported by the read-only set" - ); - } - - /// Removes an item from the set - /// Item that will be removed from the set - /// - /// True if the item was contained in the set and is now removed - /// - bool ICollection.Remove(TItem item) { - throw new NotSupportedException( - "Removing items is not supported by the read-only set" - ); - } - - /// Adds an item to the set - /// Item that will be added to the set - /// - /// True if the element was added, false if it was already contained in the set - /// - bool ISet.Add(TItem item) { - throw new NotSupportedException( - "Adding items is not supported by the read-only set" - ); - } - - /// Removes all elements that are contained in the collection - /// Collection whose elements will be removed from this set - void ISet.ExceptWith(IEnumerable other) { - throw new NotSupportedException( - "Excepting items is not supported by the read-only set" - ); - } - - /// - /// Only keeps those elements in this set that are contained in the collection - /// - /// Other set this set will be filtered by - void ISet.IntersectWith(IEnumerable other) { - throw new NotSupportedException( - "Intersecting items is not supported by the read-only set" - ); - } - - /// Adds an item to the set - /// Item that will be added to the set - void ICollection.Add(TItem item) { - throw new NotSupportedException( - "Adding is not supported by the read-only set" - ); - } - - /// The set being wrapped - private ISet set; - - } - -} // namespace Nuclex.Support.Collections - -#endif // !NO_SETS +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections; +using System.Collections.Generic; + +#if !NO_SETS + +namespace Nuclex.Support.Collections { + + /// Wraps a set and prevents it from being modified + /// Type of items to manage in the set + public class ReadOnlySet : ISet, ICollection { + + /// + /// Initializes a new observable set forwarding operations to the specified set + /// + /// Set operations will be forwarded to + public ReadOnlySet(ISet set) { + this.set = set; + } + + /// + /// Determines whether the current set is a proper (strict) subset of a collection + /// + /// Collection against which the set will be tested + /// True if the set is a proper subset of the specified collection + public bool IsProperSubsetOf(IEnumerable other) { + return this.set.IsProperSubsetOf(other); + } + + /// + /// Determines whether the current set is a proper (strict) superset of a collection + /// + /// Collection against which the set will be tested + /// True if the set is a proper superset of the specified collection + public bool IsProperSupersetOf(IEnumerable other) { + return this.set.IsProperSupersetOf(other); + } + + /// Determines whether the current set is a subset of a collection + /// Collection against which the set will be tested + /// True if the set is a subset of the specified collection + public bool IsSubsetOf(IEnumerable other) { + return this.set.IsSubsetOf(other); + } + + /// Determines whether the current set is a superset of a collection + /// Collection against which the set will be tested + /// True if the set is a superset of the specified collection + public bool IsSupersetOf(IEnumerable other) { + return this.set.IsSupersetOf(other); + } + + /// + /// Determines if the set shares at least one common element with the collection + /// + /// Collection the set will be tested against + /// + /// True if the set shares at least one common element with the collection + /// + public bool Overlaps(IEnumerable other) { + return this.set.Overlaps(other); + } + + /// + /// Determines whether the set contains the same elements as the specified collection + /// + /// Collection the set will be tested against + /// True if the set contains the same elements as the collection + public bool SetEquals(IEnumerable other) { + return this.set.SetEquals(other); + } + + /// Determines whether the set contains the specified item + /// Item the set will be tested for + /// True if the set contains the specified item + public bool Contains(TItem item) { + return this.set.Contains(item); + } + + /// Copies the contents of the set into an array + /// Array the set's contents will be copied to + /// + /// Index in the array the first copied element will be written to + /// + public void CopyTo(TItem[] array, int arrayIndex) { + this.set.CopyTo(array, arrayIndex); + } + + /// Counts the number of items contained in the set + public int Count { + get { return this.set.Count; } + } + + /// Determines whether the set is readonly + public bool IsReadOnly { + get { return true; } + } + + + /// Creates an enumerator for the set's contents + /// A new enumerator for the sets contents + public IEnumerator GetEnumerator() { + return this.set.GetEnumerator(); + } + + /// Creates an enumerator for the set's contents + /// A new enumerator for the sets contents + IEnumerator IEnumerable.GetEnumerator() { + return this.set.GetEnumerator(); + } + + /// + /// Modifies the current set so that it contains only elements that are present either + /// in the current set or in the specified collection, but not both + /// + /// Collection the set will be excepted with + void ISet.SymmetricExceptWith(IEnumerable other) { + throw new NotSupportedException( + "Excepting is not supported by the read-only set" + ); + } + + /// + /// Modifies the current set so that it contains all elements that are present in both + /// the current set and in the specified collection + /// + /// Collection an union will be built with + void ISet.UnionWith(IEnumerable other) { + throw new NotSupportedException( + "Unioning is not supported by the read-only set" + ); + } + + /// Removes all items from the set + public void Clear() { + throw new NotSupportedException( + "Clearing is not supported by the read-only set" + ); + } + + /// Removes an item from the set + /// Item that will be removed from the set + /// + /// True if the item was contained in the set and is now removed + /// + bool ICollection.Remove(TItem item) { + throw new NotSupportedException( + "Removing items is not supported by the read-only set" + ); + } + + /// Adds an item to the set + /// Item that will be added to the set + /// + /// True if the element was added, false if it was already contained in the set + /// + bool ISet.Add(TItem item) { + throw new NotSupportedException( + "Adding items is not supported by the read-only set" + ); + } + + /// Removes all elements that are contained in the collection + /// Collection whose elements will be removed from this set + void ISet.ExceptWith(IEnumerable other) { + throw new NotSupportedException( + "Excepting items is not supported by the read-only set" + ); + } + + /// + /// Only keeps those elements in this set that are contained in the collection + /// + /// Other set this set will be filtered by + void ISet.IntersectWith(IEnumerable other) { + throw new NotSupportedException( + "Intersecting items is not supported by the read-only set" + ); + } + + /// Adds an item to the set + /// Item that will be added to the set + void ICollection.Add(TItem item) { + throw new NotSupportedException( + "Adding is not supported by the read-only set" + ); + } + + /// The set being wrapped + private ISet set; + + } + +} // namespace Nuclex.Support.Collections + +#endif // !NO_SETS diff --git a/Source/Collections/ReverseComparer.Test.cs b/Source/Collections/ReverseComparer.Test.cs index 5446b75..728ad57 100644 --- a/Source/Collections/ReverseComparer.Test.cs +++ b/Source/Collections/ReverseComparer.Test.cs @@ -1,117 +1,116 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; - -#if UNITTEST - -using NUnit.Framework; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the ReverseComparer helper class - [TestFixture] - internal class ReverseComparerTest { - - #region class FortyTwoComparer - - /// Special comparer in which 42 is larger than everything - private class FortyTwoComparer : IComparer { - - /// Compares the left value to the right value - /// Value on the left side - /// Value on the right side - /// The relationship of the two values - public int Compare(int left, int right) { - - // Is there a 42 in the arguments? - if(left == 42) { - if(right == 42) { - return 0; // both are equal - } else { - return +1; // left is larger - } - } else if(right == 42) { - return -1; // right is larger - } - - // No 42 encountered, proceed as normal - return Math.Sign(left - right); - - } - - } - - #endregion // class FortyTwoComparer - - /// - /// Tests whether the default constructor of the reverse comparer works - /// - [Test] - public void TestDefaultConstructor() { - new ReverseComparer(); - } - - /// - /// Tests whether the full constructor of the reverse comparer works - /// - [Test] - public void TestFullConstructor() { - new ReverseComparer(new FortyTwoComparer()); - } - - /// - /// Tests whether the full constructor of the reverse comparer works - /// - [Test] - public void TestReversedDefaultComparer() { - Comparer comparer = Comparer.Default; - ReverseComparer reverseComparer = new ReverseComparer(comparer); - - Assert.Greater(0, comparer.Compare(10, 20)); - Assert.Less(0, comparer.Compare(20, 10)); - - Assert.Less(0, reverseComparer.Compare(10, 20)); - Assert.Greater(0, reverseComparer.Compare(20, 10)); - } - - /// - /// Tests whether the full constructor of the reverse comparer works - /// - [Test] - public void TestReversedCustomComparer() { - FortyTwoComparer fortyTwoComparer = new FortyTwoComparer(); - ReverseComparer reverseFortyTwoComparer = new ReverseComparer( - fortyTwoComparer - ); - - Assert.Less(0, fortyTwoComparer.Compare(42, 84)); - Assert.Greater(0, fortyTwoComparer.Compare(84, 42)); - - Assert.Greater(0, reverseFortyTwoComparer.Compare(42, 84)); - Assert.Less(0, reverseFortyTwoComparer.Compare(84, 42)); - } - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; + +#if UNITTEST + +using NUnit.Framework; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the ReverseComparer helper class + [TestFixture] + internal class ReverseComparerTest { + + #region class FortyTwoComparer + + /// Special comparer in which 42 is larger than everything + private class FortyTwoComparer : IComparer { + + /// Compares the left value to the right value + /// Value on the left side + /// Value on the right side + /// The relationship of the two values + public int Compare(int left, int right) { + + // Is there a 42 in the arguments? + if(left == 42) { + if(right == 42) { + return 0; // both are equal + } else { + return +1; // left is larger + } + } else if(right == 42) { + return -1; // right is larger + } + + // No 42 encountered, proceed as normal + return Math.Sign(left - right); + + } + + } + + #endregion // class FortyTwoComparer + + /// + /// Tests whether the default constructor of the reverse comparer works + /// + [Test] + public void TestDefaultConstructor() { + new ReverseComparer(); + } + + /// + /// Tests whether the full constructor of the reverse comparer works + /// + [Test] + public void TestFullConstructor() { + new ReverseComparer(new FortyTwoComparer()); + } + + /// + /// Tests whether the full constructor of the reverse comparer works + /// + [Test] + public void TestReversedDefaultComparer() { + Comparer comparer = Comparer.Default; + ReverseComparer reverseComparer = new ReverseComparer(comparer); + + Assert.Greater(0, comparer.Compare(10, 20)); + Assert.Less(0, comparer.Compare(20, 10)); + + Assert.Less(0, reverseComparer.Compare(10, 20)); + Assert.Greater(0, reverseComparer.Compare(20, 10)); + } + + /// + /// Tests whether the full constructor of the reverse comparer works + /// + [Test] + public void TestReversedCustomComparer() { + FortyTwoComparer fortyTwoComparer = new FortyTwoComparer(); + ReverseComparer reverseFortyTwoComparer = new ReverseComparer( + fortyTwoComparer + ); + + Assert.Less(0, fortyTwoComparer.Compare(42, 84)); + Assert.Greater(0, fortyTwoComparer.Compare(84, 42)); + + Assert.Greater(0, reverseFortyTwoComparer.Compare(42, 84)); + Assert.Less(0, reverseFortyTwoComparer.Compare(84, 42)); + } + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST diff --git a/Source/Collections/ReverseComparer.cs b/Source/Collections/ReverseComparer.cs index 97b070e..2a3cee7 100644 --- a/Source/Collections/ReverseComparer.cs +++ b/Source/Collections/ReverseComparer.cs @@ -1,56 +1,55 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; - -namespace Nuclex.Support.Collections { - - /// - /// Compares two values in reverse or reverses the output of another comparer - /// - /// Type of values to be compared - public class ReverseComparer : IComparer { - - /// Initializes a new reverse comparer - public ReverseComparer() : this(Comparer.Default) { } - - /// - /// Initializes the comparer to provide the inverse results of another comparer - /// - /// Comparer whose results will be inversed - public ReverseComparer(IComparer comparerToReverse) { - this.comparer = comparerToReverse; - } - - /// Compares the left value to the right value - /// Value on the left side - /// Value on the right side - /// The relationship of the two values - public int Compare(TCompared left, TCompared right) { - return this.comparer.Compare(right, left); // intentionally reversed - } - - /// The default comparer from the .NET framework - private IComparer comparer; - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; + +namespace Nuclex.Support.Collections { + + /// + /// Compares two values in reverse or reverses the output of another comparer + /// + /// Type of values to be compared + public class ReverseComparer : IComparer { + + /// Initializes a new reverse comparer + public ReverseComparer() : this(Comparer.Default) { } + + /// + /// Initializes the comparer to provide the inverse results of another comparer + /// + /// Comparer whose results will be inversed + public ReverseComparer(IComparer comparerToReverse) { + this.comparer = comparerToReverse; + } + + /// Compares the left value to the right value + /// Value on the left side + /// Value on the right side + /// The relationship of the two values + public int Compare(TCompared left, TCompared right) { + return this.comparer.Compare(right, left); // intentionally reversed + } + + /// The default comparer from the .NET framework + private IComparer comparer; + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/SortableBindingList.Test.cs b/Source/Collections/SortableBindingList.Test.cs index 0ebab8c..e943614 100644 --- a/Source/Collections/SortableBindingList.Test.cs +++ b/Source/Collections/SortableBindingList.Test.cs @@ -1,120 +1,119 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Reflection; - -#if UNITTEST - -using NUnit.Framework; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the SortableBindingList class - [TestFixture] - internal class SortableBindingListTest { - - #region class TestRecord - - /// Dummy record used to test the sortable binding list - private class TestRecord { - - /// A property of type integer - public int IntegerValue { get; set; } - - /// A property of type string - public string StringValue { get; set; } - - /// A property of type float - public float FloatValue { get; set; } - - } - - #endregion // class TestRecord - - /// Verifies that the sortable binding list is default constructible - [Test] - public void HasDefaultConstructor() { - Assert.DoesNotThrow( - delegate () { new SortableBindingList(); } - ); - } - - /// - /// Tests whether the sortable binding list can copy an existing list - /// when being constructed - /// - [Test] - public void HasEnumerableConstructor() { - var items = new List() { - new TestRecord() { IntegerValue = 123 }, - new TestRecord() { IntegerValue = 456 } - }; - - var testList = new SortableBindingList(items); - - Assert.AreEqual(2, testList.Count); - Assert.AreSame(items[0], testList[0]); - Assert.AreSame(items[1], testList[1]); - } - - /// Verifies that the sortable binding list supports sorting - [Test] - public void SupportsSorting() { - var testList = new SortableBindingList(); - IBindingList testListAsBindingList = testList; - - Assert.IsTrue(testListAsBindingList.SupportsSorting); - } - - /// - /// Tests whether the sortable binding list can sort its elements by different properties - /// - [Test] - public void CanSortItems() { - var items = new List() { - new TestRecord() { IntegerValue = 456 }, - new TestRecord() { IntegerValue = 789 }, - new TestRecord() { IntegerValue = 123 } - }; - - var testList = new SortableBindingList(items); - IBindingList testListAsBindingList = testList; - - PropertyDescriptor integerValuePropertyDescriptor = ( - TypeDescriptor.GetProperties(typeof(TestRecord))[nameof(TestRecord.IntegerValue)] - ); - testListAsBindingList.ApplySort( - integerValuePropertyDescriptor, ListSortDirection.Ascending - ); - - Assert.AreEqual(3, testList.Count); - Assert.AreEqual(123, testList[0].IntegerValue); - Assert.AreEqual(456, testList[1].IntegerValue); - Assert.AreEqual(789, testList[2].IntegerValue); - } - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; + +#if UNITTEST + +using NUnit.Framework; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the SortableBindingList class + [TestFixture] + internal class SortableBindingListTest { + + #region class TestRecord + + /// Dummy record used to test the sortable binding list + private class TestRecord { + + /// A property of type integer + public int IntegerValue { get; set; } + + /// A property of type string + public string StringValue { get; set; } + + /// A property of type float + public float FloatValue { get; set; } + + } + + #endregion // class TestRecord + + /// Verifies that the sortable binding list is default constructible + [Test] + public void HasDefaultConstructor() { + Assert.DoesNotThrow( + delegate () { new SortableBindingList(); } + ); + } + + /// + /// Tests whether the sortable binding list can copy an existing list + /// when being constructed + /// + [Test] + public void HasEnumerableConstructor() { + var items = new List() { + new TestRecord() { IntegerValue = 123 }, + new TestRecord() { IntegerValue = 456 } + }; + + var testList = new SortableBindingList(items); + + Assert.AreEqual(2, testList.Count); + Assert.AreSame(items[0], testList[0]); + Assert.AreSame(items[1], testList[1]); + } + + /// Verifies that the sortable binding list supports sorting + [Test] + public void SupportsSorting() { + var testList = new SortableBindingList(); + IBindingList testListAsBindingList = testList; + + Assert.IsTrue(testListAsBindingList.SupportsSorting); + } + + /// + /// Tests whether the sortable binding list can sort its elements by different properties + /// + [Test] + public void CanSortItems() { + var items = new List() { + new TestRecord() { IntegerValue = 456 }, + new TestRecord() { IntegerValue = 789 }, + new TestRecord() { IntegerValue = 123 } + }; + + var testList = new SortableBindingList(items); + IBindingList testListAsBindingList = testList; + + PropertyDescriptor integerValuePropertyDescriptor = ( + TypeDescriptor.GetProperties(typeof(TestRecord))[nameof(TestRecord.IntegerValue)] + ); + testListAsBindingList.ApplySort( + integerValuePropertyDescriptor, ListSortDirection.Ascending + ); + + Assert.AreEqual(3, testList.Count); + Assert.AreEqual(123, testList[0].IntegerValue); + Assert.AreEqual(456, testList[1].IntegerValue); + Assert.AreEqual(789, testList[2].IntegerValue); + } + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST diff --git a/Source/Collections/SortableBindingList.cs b/Source/Collections/SortableBindingList.cs index 65010ba..f2c0e2c 100644 --- a/Source/Collections/SortableBindingList.cs +++ b/Source/Collections/SortableBindingList.cs @@ -1,223 +1,222 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.Reflection; - -namespace Nuclex.Support.Collections { - - /// Variant of BindingList that supports sorting - /// Type of items the binding list will contain - public class SortableBindingList : BindingList { - - #region class PropertyComparer - - /// Compares two elements based on a single preselected property - private class PropertyComparer : IComparer { - - /// Initializes a new property comparer for the specified property - /// Property based on which elements should be compared - /// Direction in which elements should be sorted - public PropertyComparer(PropertyDescriptor property, ListSortDirection direction) { - this.propertyDescriptor = property; - - Type comparerForPropertyType = typeof(Comparer<>).MakeGenericType(property.PropertyType); - this.comparer = (IComparer)comparerForPropertyType.InvokeMember( - "Default", - BindingFlags.Static | BindingFlags.GetProperty | BindingFlags.Public, - null, // binder - null, // target (none since method is static) - null // argument array - ); - - SetListSortDirection(direction); - } - - /// Compares two elements based on the comparer's chosen property - /// First element for the comparison - /// Second element for the comparison - /// The relationship of the two elements to each other - public int Compare(TElement first, TElement second) { - return this.comparer.Compare( - this.propertyDescriptor.GetValue(first), - this.propertyDescriptor.GetValue(second) - ) * this.reverse; - } - - /// Selects the property based on which elements should be compared - /// Descriptor for the property to use for comparison - private void SetPropertyDescriptor(PropertyDescriptor descriptor) { - this.propertyDescriptor = descriptor; - } - - /// Changes the sort direction - /// New sort direction - private void SetListSortDirection(ListSortDirection direction) { - this.reverse = direction == ListSortDirection.Ascending ? 1 : -1; - } - - /// Updtes the sorted proeprty and the sort direction - /// Property based on which elements will be sorted - /// Direction in which elements will be sorted - public void SetPropertyAndDirection( - PropertyDescriptor descriptor, ListSortDirection direction - ) { - SetPropertyDescriptor(descriptor); - SetListSortDirection(direction); - } - - /// The default comparer for the type of the chosen property - private readonly IComparer comparer; - /// Descriptor for the chosen property - private PropertyDescriptor propertyDescriptor; - /// - /// Either positive or negative 1 to change the sign of the comparison result - /// - private int reverse; - - } - - #endregion // class PropertyComparer - - /// Initializes a new BindingList with support for sorting - public SortableBindingList() : base(new List()) { - this.comparers = new Dictionary(); - } - - /// - /// Initializes a sortable BindingList, copying the contents of an existing list - /// - /// Existing list whose contents will be shallo-wcopied - public SortableBindingList(IEnumerable enumeration) : - base(new List(enumeration)) { - this.comparers = new Dictionary(); - } - - /// - /// Used by BindingList implementation to check whether sorting is supported - /// - protected override bool SupportsSortingCore { - get { return true; } - } - - /// - /// Used by BindingList implementation to check whether the list is currently sorted - /// - protected override bool IsSortedCore { - get { return this.isSorted; } - } - - /// - /// Used by BindingList implementation to track the property the list is sorted by - /// - protected override PropertyDescriptor SortPropertyCore { - get { return this.propertyDescriptor; } - } - - /// - /// Used by BindingList implementation to track the direction in which the list is sortd - /// - protected override ListSortDirection SortDirectionCore { - get { return this.listSortDirection; } - } - - /// - /// Used by BindingList implementation to check whether the list supports searching - /// - protected override bool SupportsSearchingCore { - get { return true; } - } - - /// - /// Used by BindingList implementation to sort the elements in the backing collection - /// - protected override void ApplySortCore(PropertyDescriptor property, ListSortDirection direction) { - - // Obtain a property comparer that sorts on the attributes the SortableBindingList - // has been configured for its sort order - PropertyComparer comparer; - { - Type propertyType = property.PropertyType; - - if(!this.comparers.TryGetValue(propertyType, out comparer)) { - comparer = new PropertyComparer(property, direction); - this.comparers.Add(propertyType, comparer); - } - - // Direction may need to be updated - comparer.SetPropertyAndDirection(property, direction); - } - - // Check to see if our base class is using a standard List<> in which case - // we'll sneakily use the downcast to call the List<>.Sort() method, otherwise - // there's still our own quicksort implementation for IList<>. - List itemsAsList = this.Items as List; - if(itemsAsList != null) { - itemsAsList.Sort(comparer); - } else { - this.Items.QuickSort(0, this.Items.Count, comparer); // from IListExtensions - } - - this.propertyDescriptor = property; - this.listSortDirection = direction; - this.isSorted = true; - - OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); - } - - /// Used by BindingList implementation to undo any sorting that took place - protected override void RemoveSortCore() { - this.isSorted = false; - this.propertyDescriptor = base.SortPropertyCore; - this.listSortDirection = base.SortDirectionCore; - - OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); - } - - /// - /// Used by BindingList implementation to run a search on any of the element's properties - /// - protected override int FindCore(PropertyDescriptor property, object key) { - int count = this.Count; - for(int index = 0; index < count; ++index) { - TElement element = this[index]; - if(property.GetValue(element).Equals(key)) { - return index; - } - } - - return -1; - } - - /// Cached property comparers, created for each element property as needed - private readonly Dictionary comparers; - /// Whether the binding list is currently sorted - private bool isSorted; - /// Direction in which the binding list is currently sorted - private ListSortDirection listSortDirection; - /// Descriptor for the property by which the binding list is currently sorted - private PropertyDescriptor propertyDescriptor; - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; + +namespace Nuclex.Support.Collections { + + /// Variant of BindingList that supports sorting + /// Type of items the binding list will contain + public class SortableBindingList : BindingList { + + #region class PropertyComparer + + /// Compares two elements based on a single preselected property + private class PropertyComparer : IComparer { + + /// Initializes a new property comparer for the specified property + /// Property based on which elements should be compared + /// Direction in which elements should be sorted + public PropertyComparer(PropertyDescriptor property, ListSortDirection direction) { + this.propertyDescriptor = property; + + Type comparerForPropertyType = typeof(Comparer<>).MakeGenericType(property.PropertyType); + this.comparer = (IComparer)comparerForPropertyType.InvokeMember( + "Default", + BindingFlags.Static | BindingFlags.GetProperty | BindingFlags.Public, + null, // binder + null, // target (none since method is static) + null // argument array + ); + + SetListSortDirection(direction); + } + + /// Compares two elements based on the comparer's chosen property + /// First element for the comparison + /// Second element for the comparison + /// The relationship of the two elements to each other + public int Compare(TElement first, TElement second) { + return this.comparer.Compare( + this.propertyDescriptor.GetValue(first), + this.propertyDescriptor.GetValue(second) + ) * this.reverse; + } + + /// Selects the property based on which elements should be compared + /// Descriptor for the property to use for comparison + private void SetPropertyDescriptor(PropertyDescriptor descriptor) { + this.propertyDescriptor = descriptor; + } + + /// Changes the sort direction + /// New sort direction + private void SetListSortDirection(ListSortDirection direction) { + this.reverse = direction == ListSortDirection.Ascending ? 1 : -1; + } + + /// Updtes the sorted proeprty and the sort direction + /// Property based on which elements will be sorted + /// Direction in which elements will be sorted + public void SetPropertyAndDirection( + PropertyDescriptor descriptor, ListSortDirection direction + ) { + SetPropertyDescriptor(descriptor); + SetListSortDirection(direction); + } + + /// The default comparer for the type of the chosen property + private readonly IComparer comparer; + /// Descriptor for the chosen property + private PropertyDescriptor propertyDescriptor; + /// + /// Either positive or negative 1 to change the sign of the comparison result + /// + private int reverse; + + } + + #endregion // class PropertyComparer + + /// Initializes a new BindingList with support for sorting + public SortableBindingList() : base(new List()) { + this.comparers = new Dictionary(); + } + + /// + /// Initializes a sortable BindingList, copying the contents of an existing list + /// + /// Existing list whose contents will be shallo-wcopied + public SortableBindingList(IEnumerable enumeration) : + base(new List(enumeration)) { + this.comparers = new Dictionary(); + } + + /// + /// Used by BindingList implementation to check whether sorting is supported + /// + protected override bool SupportsSortingCore { + get { return true; } + } + + /// + /// Used by BindingList implementation to check whether the list is currently sorted + /// + protected override bool IsSortedCore { + get { return this.isSorted; } + } + + /// + /// Used by BindingList implementation to track the property the list is sorted by + /// + protected override PropertyDescriptor SortPropertyCore { + get { return this.propertyDescriptor; } + } + + /// + /// Used by BindingList implementation to track the direction in which the list is sortd + /// + protected override ListSortDirection SortDirectionCore { + get { return this.listSortDirection; } + } + + /// + /// Used by BindingList implementation to check whether the list supports searching + /// + protected override bool SupportsSearchingCore { + get { return true; } + } + + /// + /// Used by BindingList implementation to sort the elements in the backing collection + /// + protected override void ApplySortCore(PropertyDescriptor property, ListSortDirection direction) { + + // Obtain a property comparer that sorts on the attributes the SortableBindingList + // has been configured for its sort order + PropertyComparer comparer; + { + Type propertyType = property.PropertyType; + + if(!this.comparers.TryGetValue(propertyType, out comparer)) { + comparer = new PropertyComparer(property, direction); + this.comparers.Add(propertyType, comparer); + } + + // Direction may need to be updated + comparer.SetPropertyAndDirection(property, direction); + } + + // Check to see if our base class is using a standard List<> in which case + // we'll sneakily use the downcast to call the List<>.Sort() method, otherwise + // there's still our own quicksort implementation for IList<>. + List itemsAsList = this.Items as List; + if(itemsAsList != null) { + itemsAsList.Sort(comparer); + } else { + this.Items.QuickSort(0, this.Items.Count, comparer); // from IListExtensions + } + + this.propertyDescriptor = property; + this.listSortDirection = direction; + this.isSorted = true; + + OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); + } + + /// Used by BindingList implementation to undo any sorting that took place + protected override void RemoveSortCore() { + this.isSorted = false; + this.propertyDescriptor = base.SortPropertyCore; + this.listSortDirection = base.SortDirectionCore; + + OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); + } + + /// + /// Used by BindingList implementation to run a search on any of the element's properties + /// + protected override int FindCore(PropertyDescriptor property, object key) { + int count = this.Count; + for(int index = 0; index < count; ++index) { + TElement element = this[index]; + if(property.GetValue(element).Equals(key)) { + return index; + } + } + + return -1; + } + + /// Cached property comparers, created for each element property as needed + private readonly Dictionary comparers; + /// Whether the binding list is currently sorted + private bool isSorted; + /// Direction in which the binding list is currently sorted + private ListSortDirection listSortDirection; + /// Descriptor for the property by which the binding list is currently sorted + private PropertyDescriptor propertyDescriptor; + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/TransformingReadOnlyCollection.Interfaces.cs b/Source/Collections/TransformingReadOnlyCollection.Interfaces.cs index 78a2cd0..06dd96a 100644 --- a/Source/Collections/TransformingReadOnlyCollection.Interfaces.cs +++ b/Source/Collections/TransformingReadOnlyCollection.Interfaces.cs @@ -1,343 +1,342 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Threading; -using System.Collections; -using System.Collections.Generic; - -namespace Nuclex.Support.Collections { - - // More than 400 lines of code just to implement the .NET collection interfaces. - // Shows the niceties of having to support languages without generics and using - // an inferior design to make collections "more convenient" for the user :/ - - partial class TransformingReadOnlyCollection { - - #region IList Members - - /// - /// Inserts an item to the TransformingReadOnlyCollection at the specified index. - /// - /// - /// The zero-based index at which item should be inserted. - /// - /// - /// The object to insert into the TransformingReadOnlyCollection - /// - /// - /// The TransformingReadOnlyCollection is read-only. - /// - /// - /// index is not a valid index in the TransformingReadOnlyCollection. - /// - void IList.Insert(int index, TExposedItem item) { - throw new NotSupportedException("The collection is ready-only"); - } - - /// - /// Removes the TransformingReadOnlyCollection item at the specified index. - /// - /// The zero-based index of the item to remove. - /// - /// The TransformingReadOnlyCollection is read-only. - /// - /// - /// Index is not a valid index in the TransformingReadOnlyCollection. - /// - void IList.RemoveAt(int index) { - throw new NotSupportedException("The collection is ready-only"); - } - - /// Gets or sets the element at the specified index. - /// The zero-based index of the element to get or set. - /// The element at the specified index. - /// - /// Index is not a valid index in the TransformingReadOnlyCollection. - /// - /// - /// The property is set and the TransformingReadOnlyCollection is read-only - /// - TExposedItem IList.this[int index] { - get { - return this[index]; - } - set { - throw new NotSupportedException("The collection is ready-only"); - } - } - - #endregion - - #region ICollection Members - - /// Adds an item to the TransformingReadOnlyCollection. - /// The object to add to the TransformingReadOnlyCollection - /// - /// The TransformingReadOnlyCollection is read-only. - /// - void ICollection.Add(TExposedItem item) { - throw new NotSupportedException("The collection is ready-only"); - } - - /// Removes all items from the TransformingReadOnlyCollection - /// - /// The TransformingReadOnlyCollection is read-only. - /// - void ICollection.Clear() { - throw new NotSupportedException("The collection is ready-only"); - } - - /// - /// Removes the first occurrence of a specific object from the - /// TransformingReadOnlyCollection. - /// - /// - /// The object to remove from the TransformingReadOnlyCollection - /// - /// - /// True if item was successfully removed from the TransformingReadOnlyCollection; - /// otherwise, false. This method also returns false if item is not found in the - /// original TransformingReadOnlyCollection. - /// - /// - /// The TransformingReadOnlyCollection is read-only. - /// - bool ICollection.Remove(TExposedItem item) { - throw new NotSupportedException("The collection is ready-only"); - } - - #endregion - - #region IEnumerable Members - - /// Returns an enumerator that iterates through a collection. - /// - /// A System.Collections.IEnumerator object that can be used to iterate through - /// the collection. - /// - IEnumerator IEnumerable.GetEnumerator() { - return GetEnumerator(); - } - - #endregion - - #region IList Members - - /// Adds an item to the TransformingReadOnlyCollection. - /// - /// The System.Object to add to the TransformingReadOnlyCollection. - /// - /// The position into which the new element was inserted. - /// - /// The System.Collections.IList is read-only or the TransformingReadOnlyCollection - /// has a fixed size. - /// - int IList.Add(object value) { - throw new NotSupportedException("The collection is ready-only"); - } - - /// Removes all items from the TransformingReadOnlyCollection. - /// - /// The TransformingReadOnlyCollection is read-only. - /// - void IList.Clear() { - throw new NotSupportedException("The collection is ready-only"); - } - - /// - /// Determines whether the TransformingReadOnlyCollection contains a specific value. - /// - /// - /// The System.Object to locate in the TransformingReadOnlyCollection. - /// - /// - /// True if the System.Object is found in the TransformingReadOnlyCollection; - /// otherwise, false. - /// - bool IList.Contains(object value) { - return Contains((TExposedItem)value); - } - - /// - /// Determines the index of a specific item in the TransformingReadOnlyCollection. - /// - /// - /// The System.Object to locate in the TransformingReadOnlyCollection. - /// - /// - /// The index of value if found in the list; otherwise, -1. - /// - int IList.IndexOf(object value) { - return IndexOf((TExposedItem)value); - } - - /// - /// Inserts an item to the TransformingReadOnlyCollection at the specified index. - /// - /// - /// The zero-based index at which value should be inserted. - /// - /// - /// The System.Object to insert into the TransformingReadOnlyCollection. - /// - /// - /// Index is not a valid index in the TransformingReadOnlyCollection. - /// - /// - /// The System.Collections.IList is read-only or the TransformingReadOnlyCollection - /// has a fixed size. - /// - /// - /// Value is null reference in the TransformingReadOnlyCollection. - /// - void IList.Insert(int index, object value) { - throw new NotSupportedException("The collection is ready-only"); - } - - /// - /// A value indicating whether the TransformingReadOnlyCollection has a fixed - /// size. - /// - bool IList.IsFixedSize { - get { return true; } - } - - /// - /// Removes the first occurrence of a specific object from the - /// TransformingReadOnlyCollection. - /// - /// - /// The System.Object to remove from the TransformingReadOnlyCollection. - /// - /// - /// The TransformingReadOnlyCollection is read-only or the - /// TransformingReadOnlyCollection has a fixed size. - /// - void IList.Remove(object value) { - throw new NotSupportedException("The collection is ready-only"); - } - - /// - /// Removes the TransformingReadOnlyCollection item at the specified index. - /// - /// The zero-based index of the item to remove. - /// - /// Index is not a valid index in the TransformingReadOnlyCollection. - /// - /// - /// The TransformingReadOnlyCollection is read-only or the - /// TransformingReadOnlyCollection has a fixed size. - /// - void IList.RemoveAt(int index) { - throw new NotSupportedException("The collection is ready-only"); - } - - /// Gets or sets the element at the specified index. - /// The zero-based index of the element to get or set. - /// The element at the specified index - /// - /// Index is not a valid index in the TransformingReadOnlyCollection - /// - /// - /// The property is set and the TransformingReadOnlyCollection is read-only. - /// - object IList.this[int index] { - get { - return this[index]; - } - set { - throw new NotSupportedException("The collection is ready-only"); - } - } - - #endregion - - #region ICollection Members - - /// - /// Copies the elements of the TransformingReadOnlyCollection to an System.Array, - /// starting at a particular System.Array index. - /// - /// - /// The one-dimensional System.Array that is the destination of the elements - /// copied from TransformingReadOnlyCollection. The System.Array must have zero-based - /// indexing. - /// - /// The zero-based index in array at which copying begins. - /// - /// Array is null. - /// - /// - /// Index is less than zero. - /// - /// - /// Array is multidimensional or index is equal to or greater than the length - /// of array or the number of elements in the source TransformingReadOnlyCollection - /// is greater than the available space from index to the end of the destination - /// array. - /// - /// - /// The type of the source TransformingReadOnlyCollection cannot be cast - /// automatically to the type of the destination array. - /// - void ICollection.CopyTo(Array array, int index) { - CopyTo((TExposedItem[])array, index); - } - - /// - /// The number of elements contained in the TransformingReadOnlyCollection. - /// - int ICollection.Count { - get { return Count; } - } - - /// - /// A value indicating whether access to the TransformingReadOnlyCollection - /// is synchronized (thread safe). - /// - bool ICollection.IsSynchronized { - get { return false; } - } - - /// - /// An object that can be used to synchronize access to the - /// TransformingReadOnlyCollection. - /// - object ICollection.SyncRoot { - get { - if(this.syncRoot == null) { - ICollection is2 = this.items as ICollection; - if(is2 != null) { - this.syncRoot = is2.SyncRoot; - } else { - Interlocked.CompareExchange(ref this.syncRoot, new object(), null); - } - } - - return this.syncRoot; - } - } - - #endregion - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Threading; +using System.Collections; +using System.Collections.Generic; + +namespace Nuclex.Support.Collections { + + // More than 400 lines of code just to implement the .NET collection interfaces. + // Shows the niceties of having to support languages without generics and using + // an inferior design to make collections "more convenient" for the user :/ + + partial class TransformingReadOnlyCollection { + + #region IList Members + + /// + /// Inserts an item to the TransformingReadOnlyCollection at the specified index. + /// + /// + /// The zero-based index at which item should be inserted. + /// + /// + /// The object to insert into the TransformingReadOnlyCollection + /// + /// + /// The TransformingReadOnlyCollection is read-only. + /// + /// + /// index is not a valid index in the TransformingReadOnlyCollection. + /// + void IList.Insert(int index, TExposedItem item) { + throw new NotSupportedException("The collection is ready-only"); + } + + /// + /// Removes the TransformingReadOnlyCollection item at the specified index. + /// + /// The zero-based index of the item to remove. + /// + /// The TransformingReadOnlyCollection is read-only. + /// + /// + /// Index is not a valid index in the TransformingReadOnlyCollection. + /// + void IList.RemoveAt(int index) { + throw new NotSupportedException("The collection is ready-only"); + } + + /// Gets or sets the element at the specified index. + /// The zero-based index of the element to get or set. + /// The element at the specified index. + /// + /// Index is not a valid index in the TransformingReadOnlyCollection. + /// + /// + /// The property is set and the TransformingReadOnlyCollection is read-only + /// + TExposedItem IList.this[int index] { + get { + return this[index]; + } + set { + throw new NotSupportedException("The collection is ready-only"); + } + } + + #endregion + + #region ICollection Members + + /// Adds an item to the TransformingReadOnlyCollection. + /// The object to add to the TransformingReadOnlyCollection + /// + /// The TransformingReadOnlyCollection is read-only. + /// + void ICollection.Add(TExposedItem item) { + throw new NotSupportedException("The collection is ready-only"); + } + + /// Removes all items from the TransformingReadOnlyCollection + /// + /// The TransformingReadOnlyCollection is read-only. + /// + void ICollection.Clear() { + throw new NotSupportedException("The collection is ready-only"); + } + + /// + /// Removes the first occurrence of a specific object from the + /// TransformingReadOnlyCollection. + /// + /// + /// The object to remove from the TransformingReadOnlyCollection + /// + /// + /// True if item was successfully removed from the TransformingReadOnlyCollection; + /// otherwise, false. This method also returns false if item is not found in the + /// original TransformingReadOnlyCollection. + /// + /// + /// The TransformingReadOnlyCollection is read-only. + /// + bool ICollection.Remove(TExposedItem item) { + throw new NotSupportedException("The collection is ready-only"); + } + + #endregion + + #region IEnumerable Members + + /// Returns an enumerator that iterates through a collection. + /// + /// A System.Collections.IEnumerator object that can be used to iterate through + /// the collection. + /// + IEnumerator IEnumerable.GetEnumerator() { + return GetEnumerator(); + } + + #endregion + + #region IList Members + + /// Adds an item to the TransformingReadOnlyCollection. + /// + /// The System.Object to add to the TransformingReadOnlyCollection. + /// + /// The position into which the new element was inserted. + /// + /// The System.Collections.IList is read-only or the TransformingReadOnlyCollection + /// has a fixed size. + /// + int IList.Add(object value) { + throw new NotSupportedException("The collection is ready-only"); + } + + /// Removes all items from the TransformingReadOnlyCollection. + /// + /// The TransformingReadOnlyCollection is read-only. + /// + void IList.Clear() { + throw new NotSupportedException("The collection is ready-only"); + } + + /// + /// Determines whether the TransformingReadOnlyCollection contains a specific value. + /// + /// + /// The System.Object to locate in the TransformingReadOnlyCollection. + /// + /// + /// True if the System.Object is found in the TransformingReadOnlyCollection; + /// otherwise, false. + /// + bool IList.Contains(object value) { + return Contains((TExposedItem)value); + } + + /// + /// Determines the index of a specific item in the TransformingReadOnlyCollection. + /// + /// + /// The System.Object to locate in the TransformingReadOnlyCollection. + /// + /// + /// The index of value if found in the list; otherwise, -1. + /// + int IList.IndexOf(object value) { + return IndexOf((TExposedItem)value); + } + + /// + /// Inserts an item to the TransformingReadOnlyCollection at the specified index. + /// + /// + /// The zero-based index at which value should be inserted. + /// + /// + /// The System.Object to insert into the TransformingReadOnlyCollection. + /// + /// + /// Index is not a valid index in the TransformingReadOnlyCollection. + /// + /// + /// The System.Collections.IList is read-only or the TransformingReadOnlyCollection + /// has a fixed size. + /// + /// + /// Value is null reference in the TransformingReadOnlyCollection. + /// + void IList.Insert(int index, object value) { + throw new NotSupportedException("The collection is ready-only"); + } + + /// + /// A value indicating whether the TransformingReadOnlyCollection has a fixed + /// size. + /// + bool IList.IsFixedSize { + get { return true; } + } + + /// + /// Removes the first occurrence of a specific object from the + /// TransformingReadOnlyCollection. + /// + /// + /// The System.Object to remove from the TransformingReadOnlyCollection. + /// + /// + /// The TransformingReadOnlyCollection is read-only or the + /// TransformingReadOnlyCollection has a fixed size. + /// + void IList.Remove(object value) { + throw new NotSupportedException("The collection is ready-only"); + } + + /// + /// Removes the TransformingReadOnlyCollection item at the specified index. + /// + /// The zero-based index of the item to remove. + /// + /// Index is not a valid index in the TransformingReadOnlyCollection. + /// + /// + /// The TransformingReadOnlyCollection is read-only or the + /// TransformingReadOnlyCollection has a fixed size. + /// + void IList.RemoveAt(int index) { + throw new NotSupportedException("The collection is ready-only"); + } + + /// Gets or sets the element at the specified index. + /// The zero-based index of the element to get or set. + /// The element at the specified index + /// + /// Index is not a valid index in the TransformingReadOnlyCollection + /// + /// + /// The property is set and the TransformingReadOnlyCollection is read-only. + /// + object IList.this[int index] { + get { + return this[index]; + } + set { + throw new NotSupportedException("The collection is ready-only"); + } + } + + #endregion + + #region ICollection Members + + /// + /// Copies the elements of the TransformingReadOnlyCollection to an System.Array, + /// starting at a particular System.Array index. + /// + /// + /// The one-dimensional System.Array that is the destination of the elements + /// copied from TransformingReadOnlyCollection. The System.Array must have zero-based + /// indexing. + /// + /// The zero-based index in array at which copying begins. + /// + /// Array is null. + /// + /// + /// Index is less than zero. + /// + /// + /// Array is multidimensional or index is equal to or greater than the length + /// of array or the number of elements in the source TransformingReadOnlyCollection + /// is greater than the available space from index to the end of the destination + /// array. + /// + /// + /// The type of the source TransformingReadOnlyCollection cannot be cast + /// automatically to the type of the destination array. + /// + void ICollection.CopyTo(Array array, int index) { + CopyTo((TExposedItem[])array, index); + } + + /// + /// The number of elements contained in the TransformingReadOnlyCollection. + /// + int ICollection.Count { + get { return Count; } + } + + /// + /// A value indicating whether access to the TransformingReadOnlyCollection + /// is synchronized (thread safe). + /// + bool ICollection.IsSynchronized { + get { return false; } + } + + /// + /// An object that can be used to synchronize access to the + /// TransformingReadOnlyCollection. + /// + object ICollection.SyncRoot { + get { + if(this.syncRoot == null) { + ICollection is2 = this.items as ICollection; + if(is2 != null) { + this.syncRoot = is2.SyncRoot; + } else { + Interlocked.CompareExchange(ref this.syncRoot, new object(), null); + } + } + + return this.syncRoot; + } + } + + #endregion + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/TransformingReadOnlyCollection.Test.cs b/Source/Collections/TransformingReadOnlyCollection.Test.cs index 4425e2c..978136f 100644 --- a/Source/Collections/TransformingReadOnlyCollection.Test.cs +++ b/Source/Collections/TransformingReadOnlyCollection.Test.cs @@ -1,501 +1,500 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if !NO_NMOCK - -using System; -using System.Collections; -using System.Collections.Generic; - -#if UNITTEST - -using NUnit.Framework; -using NMock; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the transforming read only collection wrapper - [TestFixture] - internal class TransformingReadOnlyCollectionTest { - - #region class StringTransformer - - /// Test implementation of a transforming collection - private class StringTransformer : TransformingReadOnlyCollection { - - /// Initializes a new int-to-string transforming collection - /// Items the transforming collection will contain - public StringTransformer(IList items) : base(items) { } - - /// Transforms an item into the exposed type - /// Item to be transformed - /// The transformed item - /// - /// This method is used to transform an item in the wrapped collection into - /// the exposed item type whenever the user accesses an item. Expect it to - /// be called frequently, because the TransformingReadOnlyCollection does - /// not cache or otherwise store the transformed items. - /// - protected override string Transform(int item) { - if(item == 42) { - return null; - } - - return item.ToString(); - } - - } - - #endregion // class StringTransformer - - /// - /// Verifies that the copy constructor of the transforming read only collection works - /// - [Test] - public void TestCopyConstructor() { - int[] integers = new int[] { 12, 34, 56, 78 }; - StringTransformer testCollection = new StringTransformer(integers); - - string[] strings = new string[] { "12", "34", "56", "78" }; - CollectionAssert.AreEqual(strings, testCollection); - } - - /// Verifies that the IsReadOnly property returns true - [Test] - public void TestIsReadOnly() { - StringTransformer testCollection = new StringTransformer(new int[0]); - - Assert.IsTrue(testCollection.IsReadOnly); - } - - /// - /// Verifies that the CopyTo() method of the transforming read only collection works - /// - [Test] - public void TestCopyToArray() { - int[] inputIntegers = new int[] { 12, 34, 56, 78 }; - StringTransformer testCollection = new StringTransformer(inputIntegers); - - string[] inputStrings = new string[] { "12", "34", "56", "78" }; - string[] outputStrings = new string[testCollection.Count]; - testCollection.CopyTo(outputStrings, 0); - - CollectionAssert.AreEqual(inputStrings, outputStrings); - } - - /// - /// Verifies that the CopyTo() method of the transforming read only collection throws - /// an exception if the target array is too small to hold the collection's contents - /// - [Test] - public void TestThrowOnCopyToTooSmallArray() { - int[] inputIntegers = new int[] { 12, 34, 56, 78 }; - StringTransformer testCollection = new StringTransformer(inputIntegers); - - string[] outputStrings = new string[testCollection.Count - 1]; - Assert.Throws( - delegate() { testCollection.CopyTo(outputStrings, 0); } - ); - } - - /// - /// Checks whether the Contains() method of the transforming read only collection - /// is able to determine if the collection contains an item - /// - [Test] - public void TestContains() { - int[] integers = new int[] { 1234, 6789 }; - StringTransformer testCollection = new StringTransformer(integers); - - Assert.IsTrue(testCollection.Contains("1234")); - Assert.IsFalse(testCollection.Contains("4321")); - } - - /// - /// Checks whether the IndexOf() method of the transforming read only collection - /// is able to determine if the index of an item in the collection - /// - [Test] - public void TestIndexOf() { - int[] integers = new int[] { 12, 34, 67, 89 }; - StringTransformer testCollection = new StringTransformer(integers); - - Assert.AreEqual(0, testCollection.IndexOf("12")); - Assert.AreEqual(1, testCollection.IndexOf("34")); - Assert.AreEqual(2, testCollection.IndexOf("67")); - Assert.AreEqual(3, testCollection.IndexOf("89")); - } - - /// - /// Checks whether the IndexOf() method of the transforming read only collection - /// can cope with queries for 'null' when no 'null' item is contained on it - /// - [Test] - public void TestIndexOfWithNullItemNotContained() { - int[] integers = new int[] { 12, 34, 67, 89 }; - StringTransformer testCollection = new StringTransformer(integers); - - Assert.AreEqual(-1, testCollection.IndexOf(null)); - } - - /// - /// Checks whether the IndexOf() method of the transforming read only collection - /// can cope with queries for 'null' when a 'null' item is contained on it - /// - [Test] - public void TestIndexOfWithNullItemContained() { - int[] integers = new int[] { 12, 34, 67, 89, 42 }; - StringTransformer testCollection = new StringTransformer(integers); - - Assert.AreEqual(4, testCollection.IndexOf(null)); - } - - /// - /// Verifies that the Enumerator of the transforming read only collection correctly - /// implements the Reset() method - /// - [Test] - public void TestEnumeratorReset() { - int[] integers = new int[] { 1234, 6789 }; - StringTransformer testCollection = new StringTransformer(integers); - - IEnumerator stringEnumerator = testCollection.GetEnumerator(); - Assert.IsTrue(stringEnumerator.MoveNext()); - Assert.IsTrue(stringEnumerator.MoveNext()); - Assert.IsFalse(stringEnumerator.MoveNext()); - - stringEnumerator.Reset(); - - Assert.IsTrue(stringEnumerator.MoveNext()); - Assert.IsTrue(stringEnumerator.MoveNext()); - Assert.IsFalse(stringEnumerator.MoveNext()); - } - - /// - /// Checks whether the indexer method of the transforming read only collection - /// is able to retrieve items from the collection - /// - [Test] - public void TestRetrieveByIndexer() { - int[] integers = new int[] { 12, 34, 67, 89 }; - StringTransformer testCollection = new StringTransformer(integers); - - Assert.AreEqual("12", testCollection[0]); - Assert.AreEqual("34", testCollection[1]); - Assert.AreEqual("67", testCollection[2]); - Assert.AreEqual("89", testCollection[3]); - } - - /// - /// Checks whether the transforming read only collection will throw an exception - /// if its Insert() method is called via the generic IList<> interface - /// - [Test] - public void TestThrowOnInsertViaGenericIList() { - StringTransformer testCollection = new StringTransformer(new int[0]); - Assert.Throws( - delegate() { (testCollection as IList).Insert(0, "12345"); } - ); - } - - /// - /// Checks whether the transforming read only collection will throw an exception - /// if its RemoveAt() method is called via the generic IList<> interface - /// - [Test] - public void TestThrowOnRemoveViaGenericIList() { - StringTransformer testCollection = new StringTransformer(new int[1]); - Assert.Throws( - delegate() { (testCollection as IList).RemoveAt(0); } - ); - } - - /// - /// Checks whether the indexer method of the transforming read only collection will - /// throw an exception if it is attempted to be used for replacing an item - /// - [Test] - public void TestRetrieveByIndexerViaGenericIList() { - int[] integers = new int[] { 12, 34, 67, 89 }; - StringTransformer testCollection = new StringTransformer(integers); - - Assert.AreEqual("12", (testCollection as IList)[0]); - Assert.AreEqual("34", (testCollection as IList)[1]); - Assert.AreEqual("67", (testCollection as IList)[2]); - Assert.AreEqual("89", (testCollection as IList)[3]); - } - - /// - /// Checks whether the indexer method of the transforming read only collection - /// will throw an exception if it is attempted to be used for replacing an item - /// - [Test] - public void TestThrowOnReplaceByIndexerViaGenericIList() { - StringTransformer testCollection = new StringTransformer(new int[1]); - - Assert.Throws( - delegate() { (testCollection as IList)[0] = "12345"; } - ); - } - - /// - /// Checks whether the transforming read only collection will throw an exception - /// if its Add() method is called via the generic ICollection<> interface - /// - [Test] - public void TestThrowOnAddViaGenericICollection() { - StringTransformer testCollection = new StringTransformer(new int[0]); - Assert.Throws( - delegate() { (testCollection as ICollection).Add("12345"); } - ); - } - - /// - /// Checks whether the transforming read only collection will throw an exception - /// if its Clear() method is called via the generic ICollection<> interface - /// - [Test] - public void TestThrowOnClearViaGenericICollection() { - StringTransformer testCollection = new StringTransformer(new int[1]); - Assert.Throws( - delegate() { (testCollection as ICollection).Clear(); } - ); - } - - /// - /// Checks whether the transforming read only collection will throw an exception - /// if its Remove() method is called via the generic ICollection<> interface - /// - [Test] - public void TestThrowOnRemoveViaGenericICollection() { - int[] integers = new int[] { 12, 34, 67, 89 }; - StringTransformer testCollection = new StringTransformer(integers); - - Assert.Throws( - delegate() { (testCollection as ICollection).Remove("89"); } - ); - } - - /// - /// Tests whether the typesafe enumerator of the read only collection is working - /// - [Test] - public void TestTypesafeEnumerator() { - int[] inputIntegers = new int[] { 12, 34, 56, 78 }; - StringTransformer testCollection = new StringTransformer(inputIntegers); - - List outputStrings = new List(); - foreach(string value in testCollection) { - outputStrings.Add(value); - } - - string[] inputStrings = new string[] { "12", "34", "56", "78" }; - CollectionAssert.AreEqual(inputStrings, outputStrings); - } - - /// - /// Checks whether the transforming read only collection will throw an exception - /// if its Clear() method is called via the IList interface - /// - [Test] - public void TestThrowOnClearViaIList() { - StringTransformer testCollection = new StringTransformer(new int[1]); - Assert.Throws( - delegate() { (testCollection as IList).Clear(); } - ); - } - - /// - /// Checks whether the transforming read only collection will throw an exception - /// if its Add() method is called via the IList interface - /// - [Test] - public void TestThrowOnAddViaIList() { - StringTransformer testCollection = new StringTransformer(new int[0]); - Assert.Throws( - delegate() { (testCollection as IList).Add("12345"); } - ); - } - - /// - /// Checks whether the Contains() method of the transforming read only collection - /// is able to determine if the collection contains an item - /// - [Test] - public void TestContainsViaIList() { - int[] integers = new int[] { 1234, 6789 }; - StringTransformer testCollection = new StringTransformer(integers); - - Assert.IsTrue((testCollection as IList).Contains("1234")); - Assert.IsFalse((testCollection as IList).Contains("4321")); - } - - /// - /// Checks whether the IndexOf() method of the transforming read only collection - /// is able to determine if the index of an item in the collection - /// - [Test] - public void TestIndexOfViaIList() { - int[] integers = new int[] { 12, 34, 67, 89 }; - StringTransformer testCollection = new StringTransformer(integers); - - Assert.AreEqual(0, (testCollection as IList).IndexOf("12")); - Assert.AreEqual(1, (testCollection as IList).IndexOf("34")); - Assert.AreEqual(2, (testCollection as IList).IndexOf("67")); - Assert.AreEqual(3, (testCollection as IList).IndexOf("89")); - } - - /// - /// Checks whether the transforming read only collection will throw an exception - /// if its Insert() method is called via the IList interface - /// - [Test] - public void TestThrowOnInsertViaIList() { - StringTransformer testCollection = new StringTransformer(new int[0]); - Assert.Throws( - delegate() { (testCollection as IList).Insert(0, "12345"); } - ); - } - - /// - /// Checks whether the IsFixedSize property of the transforming read only collection - /// returns the expected result for a transforming read only collection based on - /// a fixed array - /// - [Test] - public void TestIsFixedSizeViaIList() { - int[] integers = new int[] { 12, 34, 67, 89 }; - StringTransformer testCollection = new StringTransformer(integers); - - Assert.IsTrue((testCollection as IList).IsFixedSize); - } - - /// - /// Checks whether the transforming read only collection will throw an exception - /// if its Remove() method is called via the IList interface - /// - [Test] - public void TestThrowOnRemoveViaIList() { - int[] integers = new int[] { 1234, 6789 }; - StringTransformer testCollection = new StringTransformer(integers); - - Assert.Throws( - delegate() { (testCollection as IList).Remove("6789"); } - ); - } - - /// - /// Checks whether the transforming read only collection will throw an exception - /// if its Remove() method is called via the IList interface - /// - [Test] - public void TestThrowOnRemoveAtViaIList() { - StringTransformer testCollection = new StringTransformer(new int[1]); - - Assert.Throws( - delegate() { (testCollection as IList).RemoveAt(0); } - ); - } - - /// - /// Checks whether the indexer method of the transforming read only collection - /// will throw an exception if it is attempted to be used for replacing an item - /// - [Test] - public void TestRetrieveByIndexerViaIList() { - int[] integers = new int[] { 12, 34, 67, 89 }; - StringTransformer testCollection = new StringTransformer(integers); - - Assert.AreEqual("12", (testCollection as IList)[0]); - Assert.AreEqual("34", (testCollection as IList)[1]); - Assert.AreEqual("67", (testCollection as IList)[2]); - Assert.AreEqual("89", (testCollection as IList)[3]); - } - - /// - /// Checks whether the indexer method of the transforming read only collection - /// will throw an exception if it is attempted to be used for replacing an item - /// - [Test] - public void TestThrowOnReplaceByIndexerViaIList() { - StringTransformer testCollection = new StringTransformer(new int[1]); - - Assert.Throws( - delegate() { (testCollection as IList)[0] = "12345"; } - ); - } - - /// - /// Verifies that the CopyTo() method of the transforming read only collection - /// works if invoked via the ICollection interface - /// - [Test] - public void TestCopyToArrayViaICollection() { - int[] inputIntegers = new int[] { 12, 34, 56, 78 }; - StringTransformer testCollection = new StringTransformer(inputIntegers); - - string[] outputStrings = new string[testCollection.Count]; - (testCollection as ICollection).CopyTo(outputStrings, 0); - - string[] inputStrings = new string[] { "12", "34", "56", "78" }; - CollectionAssert.AreEqual(inputStrings, outputStrings); - } - - /// - /// Verifies that the IsSynchronized property and the SyncRoot property are working - /// - [Test] - public void TestSynchronization() { - StringTransformer testCollection = new StringTransformer(new int[0]); - - if(!(testCollection as ICollection).IsSynchronized) { - lock((testCollection as ICollection).SyncRoot) { - Assert.AreEqual(0, testCollection.Count); - } - } - } - - /// - /// Verifies that the IsSynchronized property and the SyncRoot property are working - /// on transforming read only collections based on IList<>s that do not - /// implement the ICollection interface - /// - [Test] - public void TestSynchronizationOfIListWithoutICollection() { - MockFactory mockery = new MockFactory(); - Mock> mockedIList = mockery.CreateMock>(); - StringTransformer testCollection = new StringTransformer(mockedIList.MockObject); - - if(!(testCollection as ICollection).IsSynchronized) { - lock((testCollection as ICollection).SyncRoot) { - mockedIList.Expects.One.GetProperty(p => p.Count).WillReturn(12345); - int count = testCollection.Count; - Assert.AreEqual(12345, count); // ;-) - } - } - } - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST - -#endif // !NO_NMOCK +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if !NO_NMOCK + +using System; +using System.Collections; +using System.Collections.Generic; + +#if UNITTEST + +using NUnit.Framework; +using NMock; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the transforming read only collection wrapper + [TestFixture] + internal class TransformingReadOnlyCollectionTest { + + #region class StringTransformer + + /// Test implementation of a transforming collection + private class StringTransformer : TransformingReadOnlyCollection { + + /// Initializes a new int-to-string transforming collection + /// Items the transforming collection will contain + public StringTransformer(IList items) : base(items) { } + + /// Transforms an item into the exposed type + /// Item to be transformed + /// The transformed item + /// + /// This method is used to transform an item in the wrapped collection into + /// the exposed item type whenever the user accesses an item. Expect it to + /// be called frequently, because the TransformingReadOnlyCollection does + /// not cache or otherwise store the transformed items. + /// + protected override string Transform(int item) { + if(item == 42) { + return null; + } + + return item.ToString(); + } + + } + + #endregion // class StringTransformer + + /// + /// Verifies that the copy constructor of the transforming read only collection works + /// + [Test] + public void TestCopyConstructor() { + int[] integers = new int[] { 12, 34, 56, 78 }; + StringTransformer testCollection = new StringTransformer(integers); + + string[] strings = new string[] { "12", "34", "56", "78" }; + CollectionAssert.AreEqual(strings, testCollection); + } + + /// Verifies that the IsReadOnly property returns true + [Test] + public void TestIsReadOnly() { + StringTransformer testCollection = new StringTransformer(new int[0]); + + Assert.IsTrue(testCollection.IsReadOnly); + } + + /// + /// Verifies that the CopyTo() method of the transforming read only collection works + /// + [Test] + public void TestCopyToArray() { + int[] inputIntegers = new int[] { 12, 34, 56, 78 }; + StringTransformer testCollection = new StringTransformer(inputIntegers); + + string[] inputStrings = new string[] { "12", "34", "56", "78" }; + string[] outputStrings = new string[testCollection.Count]; + testCollection.CopyTo(outputStrings, 0); + + CollectionAssert.AreEqual(inputStrings, outputStrings); + } + + /// + /// Verifies that the CopyTo() method of the transforming read only collection throws + /// an exception if the target array is too small to hold the collection's contents + /// + [Test] + public void TestThrowOnCopyToTooSmallArray() { + int[] inputIntegers = new int[] { 12, 34, 56, 78 }; + StringTransformer testCollection = new StringTransformer(inputIntegers); + + string[] outputStrings = new string[testCollection.Count - 1]; + Assert.Throws( + delegate() { testCollection.CopyTo(outputStrings, 0); } + ); + } + + /// + /// Checks whether the Contains() method of the transforming read only collection + /// is able to determine if the collection contains an item + /// + [Test] + public void TestContains() { + int[] integers = new int[] { 1234, 6789 }; + StringTransformer testCollection = new StringTransformer(integers); + + Assert.IsTrue(testCollection.Contains("1234")); + Assert.IsFalse(testCollection.Contains("4321")); + } + + /// + /// Checks whether the IndexOf() method of the transforming read only collection + /// is able to determine if the index of an item in the collection + /// + [Test] + public void TestIndexOf() { + int[] integers = new int[] { 12, 34, 67, 89 }; + StringTransformer testCollection = new StringTransformer(integers); + + Assert.AreEqual(0, testCollection.IndexOf("12")); + Assert.AreEqual(1, testCollection.IndexOf("34")); + Assert.AreEqual(2, testCollection.IndexOf("67")); + Assert.AreEqual(3, testCollection.IndexOf("89")); + } + + /// + /// Checks whether the IndexOf() method of the transforming read only collection + /// can cope with queries for 'null' when no 'null' item is contained on it + /// + [Test] + public void TestIndexOfWithNullItemNotContained() { + int[] integers = new int[] { 12, 34, 67, 89 }; + StringTransformer testCollection = new StringTransformer(integers); + + Assert.AreEqual(-1, testCollection.IndexOf(null)); + } + + /// + /// Checks whether the IndexOf() method of the transforming read only collection + /// can cope with queries for 'null' when a 'null' item is contained on it + /// + [Test] + public void TestIndexOfWithNullItemContained() { + int[] integers = new int[] { 12, 34, 67, 89, 42 }; + StringTransformer testCollection = new StringTransformer(integers); + + Assert.AreEqual(4, testCollection.IndexOf(null)); + } + + /// + /// Verifies that the Enumerator of the transforming read only collection correctly + /// implements the Reset() method + /// + [Test] + public void TestEnumeratorReset() { + int[] integers = new int[] { 1234, 6789 }; + StringTransformer testCollection = new StringTransformer(integers); + + IEnumerator stringEnumerator = testCollection.GetEnumerator(); + Assert.IsTrue(stringEnumerator.MoveNext()); + Assert.IsTrue(stringEnumerator.MoveNext()); + Assert.IsFalse(stringEnumerator.MoveNext()); + + stringEnumerator.Reset(); + + Assert.IsTrue(stringEnumerator.MoveNext()); + Assert.IsTrue(stringEnumerator.MoveNext()); + Assert.IsFalse(stringEnumerator.MoveNext()); + } + + /// + /// Checks whether the indexer method of the transforming read only collection + /// is able to retrieve items from the collection + /// + [Test] + public void TestRetrieveByIndexer() { + int[] integers = new int[] { 12, 34, 67, 89 }; + StringTransformer testCollection = new StringTransformer(integers); + + Assert.AreEqual("12", testCollection[0]); + Assert.AreEqual("34", testCollection[1]); + Assert.AreEqual("67", testCollection[2]); + Assert.AreEqual("89", testCollection[3]); + } + + /// + /// Checks whether the transforming read only collection will throw an exception + /// if its Insert() method is called via the generic IList<> interface + /// + [Test] + public void TestThrowOnInsertViaGenericIList() { + StringTransformer testCollection = new StringTransformer(new int[0]); + Assert.Throws( + delegate() { (testCollection as IList).Insert(0, "12345"); } + ); + } + + /// + /// Checks whether the transforming read only collection will throw an exception + /// if its RemoveAt() method is called via the generic IList<> interface + /// + [Test] + public void TestThrowOnRemoveViaGenericIList() { + StringTransformer testCollection = new StringTransformer(new int[1]); + Assert.Throws( + delegate() { (testCollection as IList).RemoveAt(0); } + ); + } + + /// + /// Checks whether the indexer method of the transforming read only collection will + /// throw an exception if it is attempted to be used for replacing an item + /// + [Test] + public void TestRetrieveByIndexerViaGenericIList() { + int[] integers = new int[] { 12, 34, 67, 89 }; + StringTransformer testCollection = new StringTransformer(integers); + + Assert.AreEqual("12", (testCollection as IList)[0]); + Assert.AreEqual("34", (testCollection as IList)[1]); + Assert.AreEqual("67", (testCollection as IList)[2]); + Assert.AreEqual("89", (testCollection as IList)[3]); + } + + /// + /// Checks whether the indexer method of the transforming read only collection + /// will throw an exception if it is attempted to be used for replacing an item + /// + [Test] + public void TestThrowOnReplaceByIndexerViaGenericIList() { + StringTransformer testCollection = new StringTransformer(new int[1]); + + Assert.Throws( + delegate() { (testCollection as IList)[0] = "12345"; } + ); + } + + /// + /// Checks whether the transforming read only collection will throw an exception + /// if its Add() method is called via the generic ICollection<> interface + /// + [Test] + public void TestThrowOnAddViaGenericICollection() { + StringTransformer testCollection = new StringTransformer(new int[0]); + Assert.Throws( + delegate() { (testCollection as ICollection).Add("12345"); } + ); + } + + /// + /// Checks whether the transforming read only collection will throw an exception + /// if its Clear() method is called via the generic ICollection<> interface + /// + [Test] + public void TestThrowOnClearViaGenericICollection() { + StringTransformer testCollection = new StringTransformer(new int[1]); + Assert.Throws( + delegate() { (testCollection as ICollection).Clear(); } + ); + } + + /// + /// Checks whether the transforming read only collection will throw an exception + /// if its Remove() method is called via the generic ICollection<> interface + /// + [Test] + public void TestThrowOnRemoveViaGenericICollection() { + int[] integers = new int[] { 12, 34, 67, 89 }; + StringTransformer testCollection = new StringTransformer(integers); + + Assert.Throws( + delegate() { (testCollection as ICollection).Remove("89"); } + ); + } + + /// + /// Tests whether the typesafe enumerator of the read only collection is working + /// + [Test] + public void TestTypesafeEnumerator() { + int[] inputIntegers = new int[] { 12, 34, 56, 78 }; + StringTransformer testCollection = new StringTransformer(inputIntegers); + + List outputStrings = new List(); + foreach(string value in testCollection) { + outputStrings.Add(value); + } + + string[] inputStrings = new string[] { "12", "34", "56", "78" }; + CollectionAssert.AreEqual(inputStrings, outputStrings); + } + + /// + /// Checks whether the transforming read only collection will throw an exception + /// if its Clear() method is called via the IList interface + /// + [Test] + public void TestThrowOnClearViaIList() { + StringTransformer testCollection = new StringTransformer(new int[1]); + Assert.Throws( + delegate() { (testCollection as IList).Clear(); } + ); + } + + /// + /// Checks whether the transforming read only collection will throw an exception + /// if its Add() method is called via the IList interface + /// + [Test] + public void TestThrowOnAddViaIList() { + StringTransformer testCollection = new StringTransformer(new int[0]); + Assert.Throws( + delegate() { (testCollection as IList).Add("12345"); } + ); + } + + /// + /// Checks whether the Contains() method of the transforming read only collection + /// is able to determine if the collection contains an item + /// + [Test] + public void TestContainsViaIList() { + int[] integers = new int[] { 1234, 6789 }; + StringTransformer testCollection = new StringTransformer(integers); + + Assert.IsTrue((testCollection as IList).Contains("1234")); + Assert.IsFalse((testCollection as IList).Contains("4321")); + } + + /// + /// Checks whether the IndexOf() method of the transforming read only collection + /// is able to determine if the index of an item in the collection + /// + [Test] + public void TestIndexOfViaIList() { + int[] integers = new int[] { 12, 34, 67, 89 }; + StringTransformer testCollection = new StringTransformer(integers); + + Assert.AreEqual(0, (testCollection as IList).IndexOf("12")); + Assert.AreEqual(1, (testCollection as IList).IndexOf("34")); + Assert.AreEqual(2, (testCollection as IList).IndexOf("67")); + Assert.AreEqual(3, (testCollection as IList).IndexOf("89")); + } + + /// + /// Checks whether the transforming read only collection will throw an exception + /// if its Insert() method is called via the IList interface + /// + [Test] + public void TestThrowOnInsertViaIList() { + StringTransformer testCollection = new StringTransformer(new int[0]); + Assert.Throws( + delegate() { (testCollection as IList).Insert(0, "12345"); } + ); + } + + /// + /// Checks whether the IsFixedSize property of the transforming read only collection + /// returns the expected result for a transforming read only collection based on + /// a fixed array + /// + [Test] + public void TestIsFixedSizeViaIList() { + int[] integers = new int[] { 12, 34, 67, 89 }; + StringTransformer testCollection = new StringTransformer(integers); + + Assert.IsTrue((testCollection as IList).IsFixedSize); + } + + /// + /// Checks whether the transforming read only collection will throw an exception + /// if its Remove() method is called via the IList interface + /// + [Test] + public void TestThrowOnRemoveViaIList() { + int[] integers = new int[] { 1234, 6789 }; + StringTransformer testCollection = new StringTransformer(integers); + + Assert.Throws( + delegate() { (testCollection as IList).Remove("6789"); } + ); + } + + /// + /// Checks whether the transforming read only collection will throw an exception + /// if its Remove() method is called via the IList interface + /// + [Test] + public void TestThrowOnRemoveAtViaIList() { + StringTransformer testCollection = new StringTransformer(new int[1]); + + Assert.Throws( + delegate() { (testCollection as IList).RemoveAt(0); } + ); + } + + /// + /// Checks whether the indexer method of the transforming read only collection + /// will throw an exception if it is attempted to be used for replacing an item + /// + [Test] + public void TestRetrieveByIndexerViaIList() { + int[] integers = new int[] { 12, 34, 67, 89 }; + StringTransformer testCollection = new StringTransformer(integers); + + Assert.AreEqual("12", (testCollection as IList)[0]); + Assert.AreEqual("34", (testCollection as IList)[1]); + Assert.AreEqual("67", (testCollection as IList)[2]); + Assert.AreEqual("89", (testCollection as IList)[3]); + } + + /// + /// Checks whether the indexer method of the transforming read only collection + /// will throw an exception if it is attempted to be used for replacing an item + /// + [Test] + public void TestThrowOnReplaceByIndexerViaIList() { + StringTransformer testCollection = new StringTransformer(new int[1]); + + Assert.Throws( + delegate() { (testCollection as IList)[0] = "12345"; } + ); + } + + /// + /// Verifies that the CopyTo() method of the transforming read only collection + /// works if invoked via the ICollection interface + /// + [Test] + public void TestCopyToArrayViaICollection() { + int[] inputIntegers = new int[] { 12, 34, 56, 78 }; + StringTransformer testCollection = new StringTransformer(inputIntegers); + + string[] outputStrings = new string[testCollection.Count]; + (testCollection as ICollection).CopyTo(outputStrings, 0); + + string[] inputStrings = new string[] { "12", "34", "56", "78" }; + CollectionAssert.AreEqual(inputStrings, outputStrings); + } + + /// + /// Verifies that the IsSynchronized property and the SyncRoot property are working + /// + [Test] + public void TestSynchronization() { + StringTransformer testCollection = new StringTransformer(new int[0]); + + if(!(testCollection as ICollection).IsSynchronized) { + lock((testCollection as ICollection).SyncRoot) { + Assert.AreEqual(0, testCollection.Count); + } + } + } + + /// + /// Verifies that the IsSynchronized property and the SyncRoot property are working + /// on transforming read only collections based on IList<>s that do not + /// implement the ICollection interface + /// + [Test] + public void TestSynchronizationOfIListWithoutICollection() { + MockFactory mockery = new MockFactory(); + Mock> mockedIList = mockery.CreateMock>(); + StringTransformer testCollection = new StringTransformer(mockedIList.MockObject); + + if(!(testCollection as ICollection).IsSynchronized) { + lock((testCollection as ICollection).SyncRoot) { + mockedIList.Expects.One.GetProperty(p => p.Count).WillReturn(12345); + int count = testCollection.Count; + Assert.AreEqual(12345, count); // ;-) + } + } + } + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST + +#endif // !NO_NMOCK diff --git a/Source/Collections/TransformingReadOnlyCollection.cs b/Source/Collections/TransformingReadOnlyCollection.cs index 536d6a6..4117115 100644 --- a/Source/Collections/TransformingReadOnlyCollection.cs +++ b/Source/Collections/TransformingReadOnlyCollection.cs @@ -1,289 +1,288 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Nuclex.Support.Collections { - - /// Collection that transforms the contents of another collection. - /// - /// Type of the items contained in the wrapped collection. - /// - /// - /// Type this collection exposes its items as. - /// - /// - /// - /// This collection is useful if you want to expose the objects of an arbitrary - /// collection under a different type. It can be used, for example, to construct - /// wrappers for the items in a collection on-the-fly, eliminating the need to - /// manage the wrappers in parallel to the real items and improving performance - /// by only constructing a wrapper when an item is actually requested. - /// - /// - /// Another common use would be if you have a private collection of a non-public - /// type that's derived from some publicly visible type. By using this collection, - /// you can return the items under the publicly visible type while still having - /// your private collection under the non-public type, eliminating the need to - /// downcast each time you need to access elements of the non-public type. - /// - /// - public abstract partial class TransformingReadOnlyCollection< - TContainedItem, TExposedItem - > : IList, IList { - - #region class TransformingEnumerator - - /// - /// An enumerator that transforms the items returned by an enumerator of the - /// wrapped collection into the exposed type on-the-fly. - /// - private class TransformingEnumerator : IEnumerator { - - /// Initializes a new transforming enumerator - /// Owner; used to invoke the Transform() method - /// Enumerator of the wrapped collection - public TransformingEnumerator( - TransformingReadOnlyCollection transformer, - IEnumerator containedTypeEnumerator - ) { - this.transformer = transformer; - this.containedTypeEnumerator = containedTypeEnumerator; - } - - /// Immediately releases all resources used by the instance - public void Dispose() { - this.containedTypeEnumerator.Dispose(); - } - - /// - /// The element in the collection at the current position of the enumerator. - /// - public TExposedItem Current { - get { - return this.transformer.Transform(this.containedTypeEnumerator.Current); - } - } - - /// Gets the current element in the collection. - /// The current element in the collection. - /// - /// The enumerator is positioned before the first element of the collection - /// or after the last element. - /// - public bool MoveNext() { - return this.containedTypeEnumerator.MoveNext(); - } - - /// - /// Sets the enumerator to its initial position, which is before the first element - /// in the collection. - /// - /// - /// The collection was modified after the enumerator was created. - /// - public void Reset() { - this.containedTypeEnumerator.Reset(); - } - - /// The current element in the collection. - /// - /// The enumerator is positioned before the first element of the collection - /// or after the last element. - /// - object IEnumerator.Current { - get { return Current; } - } - - /// - /// Collection that owns this enumerator; required to invoke the item - /// transformation method. - /// - private TransformingReadOnlyCollection transformer; - /// An enumerator from the wrapped collection - private IEnumerator containedTypeEnumerator; - - } - - #endregion // class TransformingEnumerator - - /// Initializes a new transforming collection wrapper - /// - /// Internal list of items that are transformed into the exposed type when - /// accessed through the TransformingReadOnlyCollection. - /// - public TransformingReadOnlyCollection(IList items) { - this.items = items; - } - - /// - /// Determines whether an element is in the TransformingReadOnlyCollection - /// - /// - /// The object to locate in the TransformingReadOnlyCollection. - /// The value can be null for reference types. - /// - /// - /// True if value is found in the TransformingReadOnlyCollection; otherwise, false. - /// - /// - /// The default implementation of this method is very unoptimized and will - /// enumerate all the items in the collection, transforming one after another - /// to check whether the transformed item matches the item the user was - /// looking for. It is recommended to provide a custom implementation of - /// this method, if possible. - /// - public virtual bool Contains(TExposedItem item) { - return (IndexOf(item) != -1); - } - - /// - /// Copies the entire TransformingReadOnlyCollection to a compatible one-dimensional - /// System.Array, starting at the specified index of the target array. - /// - /// - /// The one-dimensional System.Array that is the destination of the elements copied - /// from the TransformingReadOnlyCollection. The System.Array must have - /// zero-based indexing. - /// - /// - /// The zero-based index in array at which copying begins. - /// - /// - /// Index is equal to or greater than the length of array or the number of elements - /// in the source TransformingReadOnlyCollection is greater than the available space - /// from index to the end of the destination array. - /// - /// - /// Index is less than zero. - /// - /// - /// Array is null. - /// - public void CopyTo(TExposedItem[] array, int index) { - if(this.items.Count > (array.Length - index)) { - throw new ArgumentException( - "Array too small to fit the collection items starting at the specified index" - ); - } - - for(int itemIndex = 0; itemIndex < this.items.Count; ++itemIndex) { - array[itemIndex + index] = Transform(this.items[itemIndex]); - } - } - - /// - /// Returns an enumerator that iterates through the TransformingReadOnlyCollection. - /// - /// - /// An enumerator or the TransformingReadOnlyCollection. - /// - public IEnumerator GetEnumerator() { - return new TransformingEnumerator(this, this.items.GetEnumerator()); - } - - /// - /// Searches for the specified object and returns the zero-based index of the - /// first occurrence within the entire TransformingReadOnlyCollection. - /// - /// - /// The object to locate in the TransformingReadOnlyCollection. The value can - /// be null for reference types. - /// - /// - /// The zero-based index of the first occurrence of item within the entire - /// TransformingReadOnlyCollection, if found; otherwise, -1. - /// - /// - /// The default implementation of this method is very unoptimized and will - /// enumerate all the items in the collection, transforming one after another - /// to check whether the transformed item matches the item the user was - /// looking for. It is recommended to provide a custom implementation of - /// this method, if possible. - /// - public int IndexOf(TExposedItem item) { - - if(item == null) { - - for(int index = 0; index < this.items.Count; ++index) { - if(Transform(this.items[index]) == null) { - return index; - } - } - - } else { - - var comparer = EqualityComparer.Default; - for(int index = 0; index < this.items.Count; ++index) { - if(comparer.Equals(Transform(this.items[index]), item)) { - return index; - } - } - - } - - return -1; - - } - - /// - /// The number of elements contained in the TransformingReadOnlyCollection instance - /// - public int Count { - get { return this.items.Count; } - } - - /// Gets the element at the specified index. - /// The zero-based index of the element to get. - /// The element at the specified index. - /// - /// Index is less than zero or index is equal to or greater than - /// TransformingReadOnlyCollection.Count. - /// - public TExposedItem this[int index] { - get { return Transform(this.items[index]); } - } - - /// Whether the List is write-protected - public bool IsReadOnly { - get { return true; } - } - - /// Transforms an item into the exposed type - /// Item to be transformed - /// The transformed item - /// - /// This method is used to transform an item in the wrapped collection into - /// the exposed item type whenever the user accesses an item. Expect it to - /// be called frequently, because the TransformingReadOnlyCollection does - /// not cache or otherwise store the transformed items. - /// - protected abstract TExposedItem Transform(TContainedItem item); - - /// Items being transformed upon exposure by this collection - private IList items; - /// Synchronization root for threaded accesses to this collection - private object syncRoot; - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Nuclex.Support.Collections { + + /// Collection that transforms the contents of another collection. + /// + /// Type of the items contained in the wrapped collection. + /// + /// + /// Type this collection exposes its items as. + /// + /// + /// + /// This collection is useful if you want to expose the objects of an arbitrary + /// collection under a different type. It can be used, for example, to construct + /// wrappers for the items in a collection on-the-fly, eliminating the need to + /// manage the wrappers in parallel to the real items and improving performance + /// by only constructing a wrapper when an item is actually requested. + /// + /// + /// Another common use would be if you have a private collection of a non-public + /// type that's derived from some publicly visible type. By using this collection, + /// you can return the items under the publicly visible type while still having + /// your private collection under the non-public type, eliminating the need to + /// downcast each time you need to access elements of the non-public type. + /// + /// + public abstract partial class TransformingReadOnlyCollection< + TContainedItem, TExposedItem + > : IList, IList { + + #region class TransformingEnumerator + + /// + /// An enumerator that transforms the items returned by an enumerator of the + /// wrapped collection into the exposed type on-the-fly. + /// + private class TransformingEnumerator : IEnumerator { + + /// Initializes a new transforming enumerator + /// Owner; used to invoke the Transform() method + /// Enumerator of the wrapped collection + public TransformingEnumerator( + TransformingReadOnlyCollection transformer, + IEnumerator containedTypeEnumerator + ) { + this.transformer = transformer; + this.containedTypeEnumerator = containedTypeEnumerator; + } + + /// Immediately releases all resources used by the instance + public void Dispose() { + this.containedTypeEnumerator.Dispose(); + } + + /// + /// The element in the collection at the current position of the enumerator. + /// + public TExposedItem Current { + get { + return this.transformer.Transform(this.containedTypeEnumerator.Current); + } + } + + /// Gets the current element in the collection. + /// The current element in the collection. + /// + /// The enumerator is positioned before the first element of the collection + /// or after the last element. + /// + public bool MoveNext() { + return this.containedTypeEnumerator.MoveNext(); + } + + /// + /// Sets the enumerator to its initial position, which is before the first element + /// in the collection. + /// + /// + /// The collection was modified after the enumerator was created. + /// + public void Reset() { + this.containedTypeEnumerator.Reset(); + } + + /// The current element in the collection. + /// + /// The enumerator is positioned before the first element of the collection + /// or after the last element. + /// + object IEnumerator.Current { + get { return Current; } + } + + /// + /// Collection that owns this enumerator; required to invoke the item + /// transformation method. + /// + private TransformingReadOnlyCollection transformer; + /// An enumerator from the wrapped collection + private IEnumerator containedTypeEnumerator; + + } + + #endregion // class TransformingEnumerator + + /// Initializes a new transforming collection wrapper + /// + /// Internal list of items that are transformed into the exposed type when + /// accessed through the TransformingReadOnlyCollection. + /// + public TransformingReadOnlyCollection(IList items) { + this.items = items; + } + + /// + /// Determines whether an element is in the TransformingReadOnlyCollection + /// + /// + /// The object to locate in the TransformingReadOnlyCollection. + /// The value can be null for reference types. + /// + /// + /// True if value is found in the TransformingReadOnlyCollection; otherwise, false. + /// + /// + /// The default implementation of this method is very unoptimized and will + /// enumerate all the items in the collection, transforming one after another + /// to check whether the transformed item matches the item the user was + /// looking for. It is recommended to provide a custom implementation of + /// this method, if possible. + /// + public virtual bool Contains(TExposedItem item) { + return (IndexOf(item) != -1); + } + + /// + /// Copies the entire TransformingReadOnlyCollection to a compatible one-dimensional + /// System.Array, starting at the specified index of the target array. + /// + /// + /// The one-dimensional System.Array that is the destination of the elements copied + /// from the TransformingReadOnlyCollection. The System.Array must have + /// zero-based indexing. + /// + /// + /// The zero-based index in array at which copying begins. + /// + /// + /// Index is equal to or greater than the length of array or the number of elements + /// in the source TransformingReadOnlyCollection is greater than the available space + /// from index to the end of the destination array. + /// + /// + /// Index is less than zero. + /// + /// + /// Array is null. + /// + public void CopyTo(TExposedItem[] array, int index) { + if(this.items.Count > (array.Length - index)) { + throw new ArgumentException( + "Array too small to fit the collection items starting at the specified index" + ); + } + + for(int itemIndex = 0; itemIndex < this.items.Count; ++itemIndex) { + array[itemIndex + index] = Transform(this.items[itemIndex]); + } + } + + /// + /// Returns an enumerator that iterates through the TransformingReadOnlyCollection. + /// + /// + /// An enumerator or the TransformingReadOnlyCollection. + /// + public IEnumerator GetEnumerator() { + return new TransformingEnumerator(this, this.items.GetEnumerator()); + } + + /// + /// Searches for the specified object and returns the zero-based index of the + /// first occurrence within the entire TransformingReadOnlyCollection. + /// + /// + /// The object to locate in the TransformingReadOnlyCollection. The value can + /// be null for reference types. + /// + /// + /// The zero-based index of the first occurrence of item within the entire + /// TransformingReadOnlyCollection, if found; otherwise, -1. + /// + /// + /// The default implementation of this method is very unoptimized and will + /// enumerate all the items in the collection, transforming one after another + /// to check whether the transformed item matches the item the user was + /// looking for. It is recommended to provide a custom implementation of + /// this method, if possible. + /// + public int IndexOf(TExposedItem item) { + + if(item == null) { + + for(int index = 0; index < this.items.Count; ++index) { + if(Transform(this.items[index]) == null) { + return index; + } + } + + } else { + + var comparer = EqualityComparer.Default; + for(int index = 0; index < this.items.Count; ++index) { + if(comparer.Equals(Transform(this.items[index]), item)) { + return index; + } + } + + } + + return -1; + + } + + /// + /// The number of elements contained in the TransformingReadOnlyCollection instance + /// + public int Count { + get { return this.items.Count; } + } + + /// Gets the element at the specified index. + /// The zero-based index of the element to get. + /// The element at the specified index. + /// + /// Index is less than zero or index is equal to or greater than + /// TransformingReadOnlyCollection.Count. + /// + public TExposedItem this[int index] { + get { return Transform(this.items[index]); } + } + + /// Whether the List is write-protected + public bool IsReadOnly { + get { return true; } + } + + /// Transforms an item into the exposed type + /// Item to be transformed + /// The transformed item + /// + /// This method is used to transform an item in the wrapped collection into + /// the exposed item type whenever the user accesses an item. Expect it to + /// be called frequently, because the TransformingReadOnlyCollection does + /// not cache or otherwise store the transformed items. + /// + protected abstract TExposedItem Transform(TContainedItem item); + + /// Items being transformed upon exposure by this collection + private IList items; + /// Synchronization root for threaded accesses to this collection + private object syncRoot; + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/Variegator.Test.cs b/Source/Collections/Variegator.Test.cs index a7f44c3..f14e94d 100644 --- a/Source/Collections/Variegator.Test.cs +++ b/Source/Collections/Variegator.Test.cs @@ -1,88 +1,87 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if !NO_SETS - -using System; -using System.Collections.Generic; - -#if UNITTEST - -using NUnit.Framework; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the Variegator multi dictionary - [TestFixture] - internal class VariegatorTest { - - /// - /// Tests whether the default constructor of the reverse comparer works - /// - [Test] - public void InstancesCanBeCreated() { - new Variegator(); - } - - /// - /// Verifies that querying for a missing value leads to an exception being thrown - /// - [Test] - public void QueryingMissingValueThrowsException() { - var variegator = new Variegator(); - Assert.Throws( - () => { - variegator.Get(123); - } - ); - } - - /// - /// Verifies that the variegator resolves ambiguous matches according to its design - /// - [Test] - public void AmbiguityResolvesToLeastRecentValue() { - var variegator = new Variegator(); - variegator.Add(1, "one"); - variegator.Add(1, "eins"); - - string first = variegator.Get(1); - string second = variegator.Get(1); - - // The variegator should have selected the first value by random and then - // returned the other value on the second query - Assert.AreNotEqual(first, second); - - // Now the variegator should return the first value again because it is - // the least recently used value - Assert.AreEqual(first, variegator.Get(1)); - - // Repeating the query, the second should be returned again because now - // it has become the least recently used value - Assert.AreEqual(second, variegator.Get(1)); - } - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST - -#endif // !NO_SETS +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if !NO_SETS + +using System; +using System.Collections.Generic; + +#if UNITTEST + +using NUnit.Framework; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the Variegator multi dictionary + [TestFixture] + internal class VariegatorTest { + + /// + /// Tests whether the default constructor of the reverse comparer works + /// + [Test] + public void InstancesCanBeCreated() { + new Variegator(); + } + + /// + /// Verifies that querying for a missing value leads to an exception being thrown + /// + [Test] + public void QueryingMissingValueThrowsException() { + var variegator = new Variegator(); + Assert.Throws( + () => { + variegator.Get(123); + } + ); + } + + /// + /// Verifies that the variegator resolves ambiguous matches according to its design + /// + [Test] + public void AmbiguityResolvesToLeastRecentValue() { + var variegator = new Variegator(); + variegator.Add(1, "one"); + variegator.Add(1, "eins"); + + string first = variegator.Get(1); + string second = variegator.Get(1); + + // The variegator should have selected the first value by random and then + // returned the other value on the second query + Assert.AreNotEqual(first, second); + + // Now the variegator should return the first value again because it is + // the least recently used value + Assert.AreEqual(first, variegator.Get(1)); + + // Repeating the query, the second should be returned again because now + // it has become the least recently used value + Assert.AreEqual(second, variegator.Get(1)); + } + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST + +#endif // !NO_SETS diff --git a/Source/Collections/Variegator.cs b/Source/Collections/Variegator.cs index 064b042..0e1bb91 100644 --- a/Source/Collections/Variegator.cs +++ b/Source/Collections/Variegator.cs @@ -1,287 +1,286 @@ -#region CPL License -/* -Nuclex Native Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion // CPL License - -using System; -using System.Collections.Generic; - -#if !NO_SETS - -namespace Nuclex.Support.Collections { - - /// Randomly selects between different options, trying to avoid repetition - /// Type of keys through which values can be looked up - /// Type of values provided by the variegator - /// - /// - /// This class is useful wherever randomness is involved in a game: picking random - /// actions for an NPC to execute, selecting different songs to play, displaying - /// different dialogue and more. - /// - /// - /// In principle, it works like a multimap, associating keys with a number of values - /// and allowing you to look up values by their keys. Unlike a multimap, it will try - /// to avoid handing out a previously provided value again as long as possible. - /// - /// - /// A typical usage would be to set up a mapping between situations and dialogue lines. - /// Upon calling with the situation 'detected-player-stealing', - /// the variegator would return a random (but not recently used) value which in this case - /// might contain a commentary an NPC might make upon encountering that situation. - /// Other NPCs requesting dialogue lines for the same situation would receive different - /// random commentary for as long as long as available data allows. - /// - /// - public class Variegator { - - /// Initializes a new variegator using the default history length - public Variegator() : this(64) {} - - /// Initializes a new variegator - /// - /// How far into the past the variegator will look to avoid repetition - /// - public Variegator(int historyLength) { - this.historyLength = historyLength; - this.history = new TValue[historyLength]; - this.values = new MultiDictionary(); - - this.randomNumberGenerator = new Random(); - } - - /// Removes all entries from the variegator - /// - /// This is mainly useful if you are storing smart pointers to values of substantial - /// size (eg. audio clips instead of just resource proxies or paths) and need to - /// reclaim memory. - /// - public void Clear() { - freeHistory(); - this.historyFull = false; - this.historyTailIndex = 0; - } - - /// Checks whether the variegator is empty - /// True if there are no entries in the variegator - public bool IsEmpty { - get { return (Count == 0); } - } - - /// Returns the number of values in the variegator - /// The number of values stored in the variegator - /// - /// If the same value is added with different keys (a situation that doesn't make - /// sense because such reuse should be covered by specifying multiple keys in - /// a query), it will be counted multiple times. - /// - public int Count { - get { return ((System.Collections.ICollection)this.values).Count; } - } - - /// - /// Insert a new value that can be returned when requesting the specified key - /// - /// Key of the value that will be inserted - /// Value that will be inserted under the provided key - public void Add(TKey key, TValue value) { - this.values.Add(key, value); - } - - /// Retrieves a random value associated with the specified key - /// For for which a value will be looked up - /// A random value associated with the specified key - public TValue Get(TKey key) { - ISet candidates = new HashSet(); - { - ICollection valueRange = this.values[key]; - - // If possible access the values by index because it's faster and produces less - // garbage, otherwise fall back to using an enumerator - var indexableValueRange = valueRange as IList; - if(indexableValueRange == null) { - foreach(TValue value in valueRange) { - candidates.Add(value); - } - } else { - for(int valueIndex = 0; valueIndex < indexableValueRange.Count; ++valueIndex) { - candidates.Add(indexableValueRange[valueIndex]); - } - } - } - - TValue result = destructivePickCandidateValue(candidates); - addRecentlyUsedValue(result); - return result; - } - - /// Retrieves a random value associated with one of the specified keys - /// Keys that will be considered - /// - /// In many cases, you have generic situations (such as 'detected-player-stealing', - /// 'observed-hostile-action') and specified situations (such as - /// 'detected-player-stealing-from-beggar', 'observed-hostile-action-on-cop') - /// where a values from both pools should be considered. This method allows you - /// to specify any number of keys, creating a greater set of values the variegator - /// can pick between. - /// - public TValue Get(params TKey[] keys) { - ISet candidates = new HashSet(); - - for(int index = 0; index < keys.Length; ++index) { - ICollection valueRange = this.values[keys[index]]; - - // If possible access the values by index because it's faster and produces less - // garbage, otherwise fall back to using an enumerator - var indexableValueRange = valueRange as IList; - if(indexableValueRange == null) { - foreach(TValue value in valueRange) { - candidates.Add(value); - } - } else { - for(int valueIndex = 0; valueIndex < indexableValueRange.Count; ++valueIndex) { - candidates.Add(indexableValueRange[valueIndex]); - } - } - } - - TValue result = destructivePickCandidateValue(candidates); - addRecentlyUsedValue(result); - return result; - } - - /// Picks amongst the values in a set - /// - /// Set containing the candidats values to consider. Will be destroyed. - /// - /// The least recently used candidate value or a random one - private TValue destructivePickCandidateValue(ISet candidates) { - removeRecentlyUsedValues(candidates); - - switch(candidates.Count) { - case 0: { - throw new InvalidOperationException("No values mapped to this key"); - } - case 1: { - using(IEnumerator enumerator = candidates.GetEnumerator()) { - enumerator.MoveNext(); // We can be sure this one returns true - return enumerator.Current; - } - } - default: { - int index = this.randomNumberGenerator.Next(candidates.Count); - using(IEnumerator enumerator = candidates.GetEnumerator()) { - do { - --index; - enumerator.MoveNext(); // We can be sure this one returns true - } while(index >= 0); - - return enumerator.Current; - } - - throw new InvalidOperationException( - "ISet.Count was off or random number generator malfunctioned" - ); - } - } - } - - /// Adds a recently used value to the history - /// Value that will be added to the history - private void addRecentlyUsedValue(TValue value) { - if(this.historyTailIndex == this.historyLength) { - this.historyFull = true; - this.history[0] = value; - this.historyTailIndex = 1; - } else { - this.history[this.historyTailIndex] = value; - ++this.historyTailIndex; - } - } - - /// Removes all values that are in the recent use list from a set - /// Set from which recently used values are removed - /// - /// Stops removing values when there's only 1 value left in the set - /// - private void removeRecentlyUsedValues(ISet candidates) { - if(candidates.Count <= 1) { - return; - } - - if(this.historyFull) { // History buffer has wrapped around - int index = this.historyTailIndex; - while(index > 0) { - --index; - if(candidates.Remove(this.history[index])) { - if(candidates.Count <= 1) { - return; - } - } - } - index = this.historyLength; - while(index > this.historyTailIndex) { - --index; - if(candidates.Remove(this.history[index])) { - if(candidates.Count <= 1) { - return; - } - } - } - } else { // History buffer was not full yet - int index = this.historyTailIndex; - while(index > 0) { - --index; - if(candidates.Remove(this.history[index])) { - if(candidates.Count <= 1) { - return; - } - } - } - } - } - - /// Frees all memory used by the individual history entries - /// - /// The history array itself is kept alive and the tail index + full flag will - /// not be reset. - /// - private void freeHistory() { - Array.Clear(this.history, 0, this.historyLength); - } - - /// Stores the entries the variegator can select from by their keys - private IMultiDictionary values; - - /// Random number generator that will be used to pick random values - private Random randomNumberGenerator; - /// Number of entries in the recently used list - private int historyLength; - - /// Array containing the most recently provided values - private TValue[] history; - /// Index of the tail in the recently used value array - private int historyTailIndex; - /// Whether the recently used value history is at capacity - private bool historyFull; - - } - -} // namespace Nuclex.Support.Collections - -#endif // !NO_SETS +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; + +#if !NO_SETS + +namespace Nuclex.Support.Collections { + + /// Randomly selects between different options, trying to avoid repetition + /// Type of keys through which values can be looked up + /// Type of values provided by the variegator + /// + /// + /// This class is useful wherever randomness is involved in a game: picking random + /// actions for an NPC to execute, selecting different songs to play, displaying + /// different dialogue and more. + /// + /// + /// In principle, it works like a multimap, associating keys with a number of values + /// and allowing you to look up values by their keys. Unlike a multimap, it will try + /// to avoid handing out a previously provided value again as long as possible. + /// + /// + /// A typical usage would be to set up a mapping between situations and dialogue lines. + /// Upon calling with the situation 'detected-player-stealing', + /// the variegator would return a random (but not recently used) value which in this case + /// might contain a commentary an NPC might make upon encountering that situation. + /// Other NPCs requesting dialogue lines for the same situation would receive different + /// random commentary for as long as long as available data allows. + /// + /// + public class Variegator { + + /// Initializes a new variegator using the default history length + public Variegator() : this(64) {} + + /// Initializes a new variegator + /// + /// How far into the past the variegator will look to avoid repetition + /// + public Variegator(int historyLength) { + this.historyLength = historyLength; + this.history = new TValue[historyLength]; + this.values = new MultiDictionary(); + + this.randomNumberGenerator = new Random(); + } + + /// Removes all entries from the variegator + /// + /// This is mainly useful if you are storing smart pointers to values of substantial + /// size (eg. audio clips instead of just resource proxies or paths) and need to + /// reclaim memory. + /// + public void Clear() { + freeHistory(); + this.historyFull = false; + this.historyTailIndex = 0; + } + + /// Checks whether the variegator is empty + /// True if there are no entries in the variegator + public bool IsEmpty { + get { return (Count == 0); } + } + + /// Returns the number of values in the variegator + /// The number of values stored in the variegator + /// + /// If the same value is added with different keys (a situation that doesn't make + /// sense because such reuse should be covered by specifying multiple keys in + /// a query), it will be counted multiple times. + /// + public int Count { + get { return ((System.Collections.ICollection)this.values).Count; } + } + + /// + /// Insert a new value that can be returned when requesting the specified key + /// + /// Key of the value that will be inserted + /// Value that will be inserted under the provided key + public void Add(TKey key, TValue value) { + this.values.Add(key, value); + } + + /// Retrieves a random value associated with the specified key + /// For for which a value will be looked up + /// A random value associated with the specified key + public TValue Get(TKey key) { + ISet candidates = new HashSet(); + { + ICollection valueRange = this.values[key]; + + // If possible access the values by index because it's faster and produces less + // garbage, otherwise fall back to using an enumerator + var indexableValueRange = valueRange as IList; + if(indexableValueRange == null) { + foreach(TValue value in valueRange) { + candidates.Add(value); + } + } else { + for(int valueIndex = 0; valueIndex < indexableValueRange.Count; ++valueIndex) { + candidates.Add(indexableValueRange[valueIndex]); + } + } + } + + TValue result = destructivePickCandidateValue(candidates); + addRecentlyUsedValue(result); + return result; + } + + /// Retrieves a random value associated with one of the specified keys + /// Keys that will be considered + /// + /// In many cases, you have generic situations (such as 'detected-player-stealing', + /// 'observed-hostile-action') and specified situations (such as + /// 'detected-player-stealing-from-beggar', 'observed-hostile-action-on-cop') + /// where a values from both pools should be considered. This method allows you + /// to specify any number of keys, creating a greater set of values the variegator + /// can pick between. + /// + public TValue Get(params TKey[] keys) { + ISet candidates = new HashSet(); + + for(int index = 0; index < keys.Length; ++index) { + ICollection valueRange = this.values[keys[index]]; + + // If possible access the values by index because it's faster and produces less + // garbage, otherwise fall back to using an enumerator + var indexableValueRange = valueRange as IList; + if(indexableValueRange == null) { + foreach(TValue value in valueRange) { + candidates.Add(value); + } + } else { + for(int valueIndex = 0; valueIndex < indexableValueRange.Count; ++valueIndex) { + candidates.Add(indexableValueRange[valueIndex]); + } + } + } + + TValue result = destructivePickCandidateValue(candidates); + addRecentlyUsedValue(result); + return result; + } + + /// Picks amongst the values in a set + /// + /// Set containing the candidats values to consider. Will be destroyed. + /// + /// The least recently used candidate value or a random one + private TValue destructivePickCandidateValue(ISet candidates) { + removeRecentlyUsedValues(candidates); + + switch(candidates.Count) { + case 0: { + throw new InvalidOperationException("No values mapped to this key"); + } + case 1: { + using(IEnumerator enumerator = candidates.GetEnumerator()) { + enumerator.MoveNext(); // We can be sure this one returns true + return enumerator.Current; + } + } + default: { + int index = this.randomNumberGenerator.Next(candidates.Count); + using(IEnumerator enumerator = candidates.GetEnumerator()) { + do { + --index; + enumerator.MoveNext(); // We can be sure this one returns true + } while(index >= 0); + + return enumerator.Current; + } + + throw new InvalidOperationException( + "ISet.Count was off or random number generator malfunctioned" + ); + } + } + } + + /// Adds a recently used value to the history + /// Value that will be added to the history + private void addRecentlyUsedValue(TValue value) { + if(this.historyTailIndex == this.historyLength) { + this.historyFull = true; + this.history[0] = value; + this.historyTailIndex = 1; + } else { + this.history[this.historyTailIndex] = value; + ++this.historyTailIndex; + } + } + + /// Removes all values that are in the recent use list from a set + /// Set from which recently used values are removed + /// + /// Stops removing values when there's only 1 value left in the set + /// + private void removeRecentlyUsedValues(ISet candidates) { + if(candidates.Count <= 1) { + return; + } + + if(this.historyFull) { // History buffer has wrapped around + int index = this.historyTailIndex; + while(index > 0) { + --index; + if(candidates.Remove(this.history[index])) { + if(candidates.Count <= 1) { + return; + } + } + } + index = this.historyLength; + while(index > this.historyTailIndex) { + --index; + if(candidates.Remove(this.history[index])) { + if(candidates.Count <= 1) { + return; + } + } + } + } else { // History buffer was not full yet + int index = this.historyTailIndex; + while(index > 0) { + --index; + if(candidates.Remove(this.history[index])) { + if(candidates.Count <= 1) { + return; + } + } + } + } + } + + /// Frees all memory used by the individual history entries + /// + /// The history array itself is kept alive and the tail index + full flag will + /// not be reset. + /// + private void freeHistory() { + Array.Clear(this.history, 0, this.historyLength); + } + + /// Stores the entries the variegator can select from by their keys + private IMultiDictionary values; + + /// Random number generator that will be used to pick random values + private Random randomNumberGenerator; + /// Number of entries in the recently used list + private int historyLength; + + /// Array containing the most recently provided values + private TValue[] history; + /// Index of the tail in the recently used value array + private int historyTailIndex; + /// Whether the recently used value history is at capacity + private bool historyFull; + + } + +} // namespace Nuclex.Support.Collections + +#endif // !NO_SETS diff --git a/Source/Collections/WeakCollection.Interfaces.cs b/Source/Collections/WeakCollection.Interfaces.cs index d09e3cd..99a8d32 100644 --- a/Source/Collections/WeakCollection.Interfaces.cs +++ b/Source/Collections/WeakCollection.Interfaces.cs @@ -1,211 +1,210 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Threading; -using System.Collections; -using System.Collections.Generic; - -namespace Nuclex.Support.Collections { - - partial class WeakCollection { - - #region IEnumerable Members - - /// Returns an enumerator that iterates through a collection. - /// - /// A System.Collections.IEnumerator object that can be used to iterate through - /// the collection. - /// - IEnumerator IEnumerable.GetEnumerator() { - return GetEnumerator(); - } - - #endregion - - #region IList Members - - /// Adds an item to the WeakCollection. - /// The System.Object to add to the WeakCollection. - /// The position into which the new element was inserted. - /// - /// The System.Collections.IList is read-only or the WeakCollection has a fixed size. - /// - int IList.Add(object value) { - TItem valueAsItemType = downcastToItemType(value); - return (this.items as IList).Add(new WeakReference(valueAsItemType)); - } - - /// - /// Determines whether the WeakCollection contains a specific value. - /// - /// The System.Object to locate in the WeakCollection. - /// - /// True if the System.Object is found in the WeakCollection; otherwise, false. - /// - bool IList.Contains(object value) { - TItem valueAsItemType = downcastToItemType(value); - return Contains(valueAsItemType); - } - - /// Determines the index of a specific item in the WeakCollection. - /// The System.Object to locate in the WeakCollection. - /// - /// The index of value if found in the list; otherwise, -1. - /// - int IList.IndexOf(object value) { - TItem valueAsItemType = downcastToItemType(value); - return IndexOf(valueAsItemType); - } - - /// - /// Inserts an item to the WeakCollection at the specified index. - /// - /// - /// The zero-based index at which value should be inserted. - /// - /// The System.Object to insert into the WeakCollection. - /// - /// Index is not a valid index in the TransformingReadOnlyCollection. - /// - /// - /// The System.Collections.IList is read-only or the WeakCollection has a fixed size. - /// - /// - /// Value is null reference in the WeakCollection. - /// - void IList.Insert(int index, object value) { - TItem valueAsItemType = downcastToItemType(value); - Insert(index, valueAsItemType); - } - - /// - /// A value indicating whether the WeakCollection has a fixed size. - /// - bool IList.IsFixedSize { - get { return (this.items as IList).IsFixedSize; } - } - - /// - /// Removes the first occurrence of a specific object from the WeakCollection. - /// - /// The System.Object to remove from the WeakCollection. - /// - /// The WeakCollection is read-only or the WeakCollection has a fixed size. - /// - void IList.Remove(object value) { - TItem valueAsItemType = downcastToItemType(value); - Remove(valueAsItemType); - } - - /// Gets or sets the element at the specified index. - /// The zero-based index of the element to get or set. - /// The element at the specified index - /// - /// Index is not a valid index in the WeakCollection - /// - object IList.this[int index] { - get { return this[index]; } - set { - TItem valueAsItemType = downcastToItemType(value); - this[index] = valueAsItemType; - } - } - - #endregion - - #region ICollection Members - - /// - /// Copies the elements of the WeakCollection to an System.Array, starting at - /// a particular System.Array index. - /// - /// - /// The one-dimensional System.Array that is the destination of the elements - /// copied from WeakCollection. The System.Array must have zero-based indexing. - /// - /// The zero-based index in array at which copying begins. - /// - /// Array is null. - /// - /// - /// Index is less than zero. - /// - /// - /// Array is multidimensional or index is equal to or greater than the length - /// of array or the number of elements in the source WeakCollection is greater than - /// the available space from index to the end of the destination array. - /// - /// - /// The type of the source WeakCollection cannot be cast automatically to the type of - /// the destination array. - /// - void ICollection.CopyTo(Array array, int index) { - CopyTo((TItem[])array, index); - } - - /// - /// A value indicating whether access to the WeakCollection is - /// synchronized (thread safe). - /// - bool ICollection.IsSynchronized { - get { return false; } - } - - /// - /// An object that can be used to synchronize access to the WeakCollection. - /// - object ICollection.SyncRoot { - get { - if(this.syncRoot == null) { - ICollection is2 = this.items as ICollection; - if(is2 != null) { - this.syncRoot = is2.SyncRoot; - } else { - Interlocked.CompareExchange(ref this.syncRoot, new object(), null); - } - } - - return this.syncRoot; - } - } - - #endregion - - /// - /// Downcasts an object reference to a reference to the collection's item type - /// - /// Object reference that will be downcast - /// - /// The specified object referecne as a reference to the collection's item type - /// - private static TItem downcastToItemType(object value) { - TItem valueAsItemType = value as TItem; - if(!ReferenceEquals(value, null)) { - if(valueAsItemType == null) { - throw new ArgumentException("Object is not of a compatible type", "value"); - } - } - return valueAsItemType; - } - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Threading; +using System.Collections; +using System.Collections.Generic; + +namespace Nuclex.Support.Collections { + + partial class WeakCollection { + + #region IEnumerable Members + + /// Returns an enumerator that iterates through a collection. + /// + /// A System.Collections.IEnumerator object that can be used to iterate through + /// the collection. + /// + IEnumerator IEnumerable.GetEnumerator() { + return GetEnumerator(); + } + + #endregion + + #region IList Members + + /// Adds an item to the WeakCollection. + /// The System.Object to add to the WeakCollection. + /// The position into which the new element was inserted. + /// + /// The System.Collections.IList is read-only or the WeakCollection has a fixed size. + /// + int IList.Add(object value) { + TItem valueAsItemType = downcastToItemType(value); + return (this.items as IList).Add(new WeakReference(valueAsItemType)); + } + + /// + /// Determines whether the WeakCollection contains a specific value. + /// + /// The System.Object to locate in the WeakCollection. + /// + /// True if the System.Object is found in the WeakCollection; otherwise, false. + /// + bool IList.Contains(object value) { + TItem valueAsItemType = downcastToItemType(value); + return Contains(valueAsItemType); + } + + /// Determines the index of a specific item in the WeakCollection. + /// The System.Object to locate in the WeakCollection. + /// + /// The index of value if found in the list; otherwise, -1. + /// + int IList.IndexOf(object value) { + TItem valueAsItemType = downcastToItemType(value); + return IndexOf(valueAsItemType); + } + + /// + /// Inserts an item to the WeakCollection at the specified index. + /// + /// + /// The zero-based index at which value should be inserted. + /// + /// The System.Object to insert into the WeakCollection. + /// + /// Index is not a valid index in the TransformingReadOnlyCollection. + /// + /// + /// The System.Collections.IList is read-only or the WeakCollection has a fixed size. + /// + /// + /// Value is null reference in the WeakCollection. + /// + void IList.Insert(int index, object value) { + TItem valueAsItemType = downcastToItemType(value); + Insert(index, valueAsItemType); + } + + /// + /// A value indicating whether the WeakCollection has a fixed size. + /// + bool IList.IsFixedSize { + get { return (this.items as IList).IsFixedSize; } + } + + /// + /// Removes the first occurrence of a specific object from the WeakCollection. + /// + /// The System.Object to remove from the WeakCollection. + /// + /// The WeakCollection is read-only or the WeakCollection has a fixed size. + /// + void IList.Remove(object value) { + TItem valueAsItemType = downcastToItemType(value); + Remove(valueAsItemType); + } + + /// Gets or sets the element at the specified index. + /// The zero-based index of the element to get or set. + /// The element at the specified index + /// + /// Index is not a valid index in the WeakCollection + /// + object IList.this[int index] { + get { return this[index]; } + set { + TItem valueAsItemType = downcastToItemType(value); + this[index] = valueAsItemType; + } + } + + #endregion + + #region ICollection Members + + /// + /// Copies the elements of the WeakCollection to an System.Array, starting at + /// a particular System.Array index. + /// + /// + /// The one-dimensional System.Array that is the destination of the elements + /// copied from WeakCollection. The System.Array must have zero-based indexing. + /// + /// The zero-based index in array at which copying begins. + /// + /// Array is null. + /// + /// + /// Index is less than zero. + /// + /// + /// Array is multidimensional or index is equal to or greater than the length + /// of array or the number of elements in the source WeakCollection is greater than + /// the available space from index to the end of the destination array. + /// + /// + /// The type of the source WeakCollection cannot be cast automatically to the type of + /// the destination array. + /// + void ICollection.CopyTo(Array array, int index) { + CopyTo((TItem[])array, index); + } + + /// + /// A value indicating whether access to the WeakCollection is + /// synchronized (thread safe). + /// + bool ICollection.IsSynchronized { + get { return false; } + } + + /// + /// An object that can be used to synchronize access to the WeakCollection. + /// + object ICollection.SyncRoot { + get { + if(this.syncRoot == null) { + ICollection is2 = this.items as ICollection; + if(is2 != null) { + this.syncRoot = is2.SyncRoot; + } else { + Interlocked.CompareExchange(ref this.syncRoot, new object(), null); + } + } + + return this.syncRoot; + } + } + + #endregion + + /// + /// Downcasts an object reference to a reference to the collection's item type + /// + /// Object reference that will be downcast + /// + /// The specified object referecne as a reference to the collection's item type + /// + private static TItem downcastToItemType(object value) { + TItem valueAsItemType = value as TItem; + if(!ReferenceEquals(value, null)) { + if(valueAsItemType == null) { + throw new ArgumentException("Object is not of a compatible type", "value"); + } + } + return valueAsItemType; + } + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/WeakCollection.Test.cs b/Source/Collections/WeakCollection.Test.cs index 1acb2bf..24f02e7 100644 --- a/Source/Collections/WeakCollection.Test.cs +++ b/Source/Collections/WeakCollection.Test.cs @@ -1,669 +1,668 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections; -using System.Collections.Generic; - -#if UNITTEST - -using NUnit.Framework; - -namespace Nuclex.Support.Collections { - - /// Unit Test for the weak collection wrapper - [TestFixture] - internal class WeakCollectionTest { - - #region class Dummy - - /// Dummy class used to test the weakly referencing collection - private class Dummy { - - /// Initializes a new dummy - /// Value that will be stored by the dummy - public Dummy(int value) { - this.Value = value; - } - - /// - /// Determines whether the specified System.Object is equal to - /// the current Dummy object. - /// - /// - /// The System.Object to compare with the current Dummy object - /// - /// - /// True if the specified System.Object is equal to the current Dummy object; - /// otherwise, false. - /// - public override bool Equals(object otherAsObject) { - Dummy other = otherAsObject as Dummy; - if(other == null) { - return false; - } - return this.Value.Equals(other.Value); - } - - /// Serves as a hash function for a particular type. - /// A hash code for the current System.Object. - public override int GetHashCode() { - return this.Value.GetHashCode(); - } - - /// Some value that can be used for testing - public int Value; - - } - - #endregion // class Dummy - - #region class ListWithoutICollection - - private class ListWithoutICollection : IList> { - public int IndexOf(WeakReference item) { throw new NotImplementedException(); } - public void Insert(int index, WeakReference item) { - throw new NotImplementedException(); - } - public void RemoveAt(int index) { throw new NotImplementedException(); } - public WeakReference this[int index] { - get { throw new NotImplementedException(); } - set { throw new NotImplementedException(); } - } - public void Add(WeakReference item) { throw new NotImplementedException(); } - public void Clear() { throw new NotImplementedException(); } - public bool Contains(WeakReference item) { throw new NotImplementedException(); } - public void CopyTo(WeakReference[] array, int arrayIndex) { - throw new NotImplementedException(); - } - public int Count { get { return 12345; } } - public bool IsReadOnly { get { throw new NotImplementedException(); } } - public bool Remove(WeakReference item) { throw new NotImplementedException(); } - public IEnumerator> GetEnumerator() { - throw new NotImplementedException(); - } - IEnumerator IEnumerable.GetEnumerator() { throw new NotImplementedException(); } - } - - #endregion // class ListWithoutICollection - - /// Verifies that the constructor of the weak collection is working - [Test] - public void TestConstructor() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - Assert.IsNotNull(dummies); - } - - /// - /// Test whether the non-typesafe Add() method of the weak collection works - /// - [Test] - public void TestAddAsObject() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - - Dummy oneTwoThreeDummy = new Dummy(12345); - (dummies as IList).Add((object)oneTwoThreeDummy); - - CollectionAssert.Contains(dummies, oneTwoThreeDummy); - } - - /// - /// Test whether the non-typesafe Add() method throws an exception if an object is - /// added that is not compatible to the collection's item type - /// - [Test] - public void TestThrowOnAddIncompatibleObject() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - - Assert.Throws( - delegate() { (dummies as IList).Add(new object()); } - ); - } - - /// - /// Test whether the generic Add() method of the weak collection works - /// - [Test] - public void TestAdd() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - - Dummy oneTwoThreeDummy = new Dummy(12345); - dummies.Add(oneTwoThreeDummy); - - CollectionAssert.Contains(dummies, oneTwoThreeDummy); - } - - /// Tests whether the Clear() method works - [Test] - public void TestClear() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - - Dummy oneTwoThreeDummy = new Dummy(12345); - dummies.Add(oneTwoThreeDummy); - Dummy threeTwoOneDummy = new Dummy(54321); - dummies.Add(threeTwoOneDummy); - - Assert.AreEqual(2, dummies.Count); - - dummies.Clear(); - - Assert.AreEqual(0, dummies.Count); - } - - /// Tests whether the Contains() method works - [Test] - public void TestContains() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - - Dummy oneTwoThreeDummy = new Dummy(12345); - dummies.Add(oneTwoThreeDummy); - Dummy threeTwoOneDummy = new Dummy(54321); - - Assert.IsTrue(dummies.Contains(oneTwoThreeDummy)); - Assert.IsFalse(dummies.Contains(threeTwoOneDummy)); - } - - /// Tests whether the non-typesafe Contains() method works - [Test] - public void TestContainsWithObject() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - - Dummy oneTwoThreeDummy = new Dummy(12345); - dummies.Add(oneTwoThreeDummy); - Dummy threeTwoOneDummy = new Dummy(54321); - - Assert.IsTrue((dummies as IList).Contains((object)oneTwoThreeDummy)); - Assert.IsFalse((dummies as IList).Contains((object)threeTwoOneDummy)); - } - - /// - /// Verifies that the Enumerator of the dummy collection correctly - /// implements the Reset() method - /// - [Test] - public void TestEnumeratorReset() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - Dummy oneTwoThreeDummy = new Dummy(123); - dummies.Add(oneTwoThreeDummy); - Dummy fourFiveSixDummy = new Dummy(456); - dummies.Add(fourFiveSixDummy); - - IEnumerator dummyEnumerator = dummies.GetEnumerator(); - Assert.IsTrue(dummyEnumerator.MoveNext()); - Assert.IsTrue(dummyEnumerator.MoveNext()); - Assert.IsFalse(dummyEnumerator.MoveNext()); - - dummyEnumerator.Reset(); - - Assert.IsTrue(dummyEnumerator.MoveNext()); - Assert.IsTrue(dummyEnumerator.MoveNext()); - Assert.IsFalse(dummyEnumerator.MoveNext()); - } - - /// Verifies that the IndexOf() method is working as intended - [Test] - public void TestIndexOf() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - Dummy oneTwoThreeDummy = new Dummy(123); - dummies.Add(oneTwoThreeDummy); - Dummy fourFiveSixDummy = new Dummy(456); - dummies.Add(fourFiveSixDummy); - Dummy sevenEightNineDummy = new Dummy(789); - - Assert.AreEqual(0, dummies.IndexOf(oneTwoThreeDummy)); - Assert.AreEqual(1, dummies.IndexOf(fourFiveSixDummy)); - Assert.AreEqual(-1, dummies.IndexOf(sevenEightNineDummy)); - } - - /// - /// Verifies that the non-typesafe IndexOf() method is working as intended - /// - [Test] - public void TestIndexOfWithObject() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - Dummy oneTwoThreeDummy = new Dummy(123); - dummies.Add(oneTwoThreeDummy); - Dummy fourFiveSixDummy = new Dummy(456); - dummies.Add(fourFiveSixDummy); - Dummy sevenEightNineDummy = new Dummy(789); - - Assert.AreEqual(0, (dummies as IList).IndexOf((object)oneTwoThreeDummy)); - Assert.AreEqual(1, (dummies as IList).IndexOf((object)fourFiveSixDummy)); - Assert.AreEqual(-1, (dummies as IList).IndexOf((object)sevenEightNineDummy)); - } - - /// - /// Verifies that an exception is thrown if an incompatible object is passed to - /// the non-typesafe variant of the IndexOf() method - /// - [Test] - public void TestThrowOnIndexOfWithIncompatibleObject() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - - Assert.Throws( - delegate() { Assert.IsNull((dummies as IList).IndexOf(new object())); } - ); - } - - /// Test whether the IndexOf() method can cope with null references - [Test] - public void TestIndexOfNull() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - - Assert.AreEqual(-1, dummies.IndexOf(null)); - dummies.Add(null); - Assert.AreEqual(0, dummies.IndexOf(null)); - } - - /// - /// Verifies that the CopyTo() method of the weak collection works - /// - [Test] - public void TestCopyToArray() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - Dummy oneTwoThreeDummy = new Dummy(123); - dummies.Add(oneTwoThreeDummy); - Dummy fourFiveSixDummy = new Dummy(456); - dummies.Add(fourFiveSixDummy); - - Dummy[] inputDummies = new Dummy[] { oneTwoThreeDummy, fourFiveSixDummy }; - Dummy[] outputDummies = new Dummy[dummies.Count]; - - dummies.CopyTo(outputDummies, 0); - - CollectionAssert.AreEqual(inputDummies, outputDummies); - } - - /// - /// Verifies that the CopyTo() method of the weak collection throws an exception - /// if the target array is too small to hold the collection's contents - /// - [Test] - public void TestThrowOnCopyToTooSmallArray() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - Dummy oneTwoThreeDummy = new Dummy(123); - dummies.Add(oneTwoThreeDummy); - Dummy fourFiveSixDummy = new Dummy(456); - dummies.Add(fourFiveSixDummy); - - Dummy[] outputStrings = new Dummy[dummies.Count - 1]; - Assert.Throws( - delegate() { dummies.CopyTo(outputStrings, 0); } - ); - } - - /// - /// Verifies that the CopyTo() method of the transforming read only collection - /// works if invoked via the ICollection interface - /// - [Test] - public void TestCopyToArrayViaICollection() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - Dummy oneTwoThreeDummy = new Dummy(123); - dummies.Add(oneTwoThreeDummy); - Dummy fourFiveSixDummy = new Dummy(456); - dummies.Add(fourFiveSixDummy); - - Dummy[] inputDummies = new Dummy[] { oneTwoThreeDummy, fourFiveSixDummy }; - Dummy[] outputDummies = new Dummy[dummies.Count]; - - (dummies as ICollection).CopyTo(outputDummies, 0); - - CollectionAssert.AreEqual(inputDummies, outputDummies); - } - - /// - /// Verifies that the Insert() method correctly shifts items in the collection - /// - [Test] - public void TestInsert() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - Dummy oneTwoThreeDummy = new Dummy(123); - dummies.Add(oneTwoThreeDummy); - - Dummy fourFiveSixDummy = new Dummy(456); - dummies.Insert(0, fourFiveSixDummy); - - Assert.AreEqual(2, dummies.Count); - Assert.AreSame(fourFiveSixDummy, dummies[0]); - Assert.AreSame(oneTwoThreeDummy, dummies[1]); - } - - /// - /// Verifies that the non-typesafe Insert() method correctly shifts items in - /// the collection - /// - [Test] - public void TestInsertObject() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - Dummy oneTwoThreeDummy = new Dummy(123); - dummies.Add(oneTwoThreeDummy); - - Dummy fourFiveSixDummy = new Dummy(456); - (dummies as IList).Insert(0, (object)fourFiveSixDummy); - - Assert.AreEqual(2, dummies.Count); - Assert.AreSame(fourFiveSixDummy, dummies[0]); - Assert.AreSame(oneTwoThreeDummy, dummies[1]); - } - - /// - /// Verifies that the non-typesafe Insert() method correctly shifts items in - /// the collection - /// - [Test] - public void TestThrowOnInsertIncompatibleObject() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - Dummy oneTwoThreeDummy = new Dummy(123); - dummies.Add(oneTwoThreeDummy); - - Assert.Throws( - delegate() { (dummies as IList).Insert(0, new object()); } - ); - } - - /// - /// Checks whether the IsFixedSize property of the weak collection returns - /// the expected result for a weak collection based on a fixed array - /// - [Test] - public void TestIsFixedSizeViaIList() { - Dummy oneTwoThreeDummy = new Dummy(123); - Dummy fourFiveSixDummy = new Dummy(456); - - WeakReference[] dummyReferences = new WeakReference[] { - new WeakReference(oneTwoThreeDummy), - new WeakReference(fourFiveSixDummy) - }; - WeakCollection dummies = new WeakCollection(dummyReferences); - - Assert.IsTrue((dummies as IList).IsFixedSize); - } - - /// - /// Tests whether the IsReadOnly property of the weak collection works - /// - [Test] - public void TestIsReadOnly() { - Dummy oneTwoThreeDummy = new Dummy(123); - Dummy fourFiveSixDummy = new Dummy(456); - - List> dummyReferences = new List>(); - dummyReferences.Add(new WeakReference(oneTwoThreeDummy)); - dummyReferences.Add(new WeakReference(fourFiveSixDummy)); - - ReadOnlyList> readOnlyDummyReferences = - new ReadOnlyList>(dummyReferences); - - WeakCollection dummies = new WeakCollection(dummyReferences); - WeakCollection readOnlydummies = new WeakCollection( - readOnlyDummyReferences - ); - - Assert.IsFalse(dummies.IsReadOnly); - Assert.IsTrue(readOnlydummies.IsReadOnly); - } - - /// - /// Tests whether the IsSynchronized property of the weak collection works - /// - [Test] - public void TestIsSynchronized() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - - Assert.IsFalse((dummies as IList).IsSynchronized); - } - - /// Tests the indexer of the weak collection - [Test] - public void TestIndexer() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - Dummy oneTwoThreeDummy = new Dummy(123); - dummies.Add(oneTwoThreeDummy); - Dummy fourFiveSixDummy = new Dummy(456); - dummies.Add(fourFiveSixDummy); - - Assert.AreSame(oneTwoThreeDummy, dummies[0]); - Assert.AreSame(fourFiveSixDummy, dummies[1]); - - dummies[0] = fourFiveSixDummy; - - Assert.AreSame(fourFiveSixDummy, dummies[0]); - } - - /// Tests the non-typesafe indexer of the weak collection - [Test] - public void TestIndexerWithObject() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - Dummy oneTwoThreeDummy = new Dummy(123); - dummies.Add(oneTwoThreeDummy); - Dummy fourFiveSixDummy = new Dummy(456); - dummies.Add(fourFiveSixDummy); - - Assert.AreSame((object)oneTwoThreeDummy, (dummies as IList)[0]); - Assert.AreSame((object)fourFiveSixDummy, (dummies as IList)[1]); - - (dummies as IList)[0] = (object)fourFiveSixDummy; - - Assert.AreSame((object)fourFiveSixDummy, (dummies as IList)[0]); - } - - /// - /// Tests whether the non-typesafe indexer of the weak collection throws - /// the correct exception if an incompatible object is assigned - /// - [Test] - public void TestThrowOnIndexerWithIncompatibleObject() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - Dummy oneTwoThreeDummy = new Dummy(123); - dummies.Add(oneTwoThreeDummy); - - Assert.Throws( - delegate() { (dummies as IList)[0] = new object(); } - ); - } - - /// Tests the Remove() method of the weak collection - [Test] - public void TestRemove() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - Dummy oneTwoThreeDummy = new Dummy(123); - dummies.Add(oneTwoThreeDummy); - Dummy fourFiveSixDummy = new Dummy(456); - dummies.Add(fourFiveSixDummy); - - Assert.AreEqual(2, dummies.Count); - Assert.IsTrue(dummies.Remove(oneTwoThreeDummy)); - Assert.AreEqual(1, dummies.Count); - Assert.IsFalse(dummies.Remove(oneTwoThreeDummy)); - } - - /// Tests the non-typesafe Remove() method of the weak collection - [Test] - public void TestRemoveObject() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - Dummy oneTwoThreeDummy = new Dummy(123); - dummies.Add(oneTwoThreeDummy); - Dummy fourFiveSixDummy = new Dummy(456); - dummies.Add(fourFiveSixDummy); - - Assert.AreEqual(2, dummies.Count); - (dummies as IList).Remove((object)oneTwoThreeDummy); - Assert.AreEqual(1, dummies.Count); - } - - /// - /// Tests whether a null object can be managed by and removed from the weak collection - /// - [Test] - public void TestRemoveNull() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - dummies.Add(null); - - Assert.AreEqual(1, dummies.Count); - Assert.IsTrue(dummies.Remove(null)); - Assert.AreEqual(0, dummies.Count); - } - - /// - /// Tests whether the non-typesafe Remove() method of the weak collection throws - /// an exception if an object is tried to be removed that is incompatible with - /// the collection's item type - /// - [Test] - public void TestThrowOnRemoveIncompatibleObject() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - Assert.Throws( - delegate() { (dummies as IList).Remove(new object()); } - ); - } - - /// Tests the RemoveAt() method of the weak collection - [Test] - public void TestRemoveAt() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - Dummy oneTwoThreeDummy = new Dummy(123); - dummies.Add(oneTwoThreeDummy); - Dummy fourFiveSixDummy = new Dummy(456); - dummies.Add(fourFiveSixDummy); - - Assert.AreSame(oneTwoThreeDummy, dummies[0]); - dummies.RemoveAt(0); - Assert.AreSame(fourFiveSixDummy, dummies[0]); - } - - /// - /// Verifies that the IsSynchronized property and the SyncRoot property are working - /// - [Test] - public void TestSynchronization() { - WeakCollection dummies = new WeakCollection( - new List>() - ); - - if(!(dummies as ICollection).IsSynchronized) { - lock((dummies as ICollection).SyncRoot) { - Assert.AreEqual(0, dummies.Count); - } - } - } - - /// - /// Verifies that the IsSynchronized property and the SyncRoot property are working - /// on transforming read only collections based on IList<>s that do not - /// implement the ICollection interface - /// - [Test] - public void TestSynchronizationOfIListWithoutICollection() { - WeakCollection dummies = new WeakCollection( - new ListWithoutICollection() - ); - - if(!(dummies as ICollection).IsSynchronized) { - lock((dummies as ICollection).SyncRoot) { - int count = dummies.Count; - Assert.AreEqual(12345, count); // ;-) - } - } - } - - /// Tests the RemoveDeadItems() method - [Test] - public void TestRemoveDeadItems() { - List> dummyReferences = new List>(); - - Dummy oneTwoThreeDummy = new Dummy(123); - dummyReferences.Add(new WeakReference(oneTwoThreeDummy)); - - dummyReferences.Add(new WeakReference(null)); - - Dummy fourFiveSixDummy = new Dummy(456); - dummyReferences.Add(new WeakReference(fourFiveSixDummy)); - - WeakCollection dummies = new WeakCollection(dummyReferences); - - Assert.AreEqual(3, dummies.Count); - - dummies.RemoveDeadItems(); - - Assert.AreEqual(2, dummies.Count); - Assert.AreSame(oneTwoThreeDummy, dummies[0]); - Assert.AreSame(fourFiveSixDummy, dummies[1]); - } - - } - -} // namespace Nuclex.Support.Collections - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections; +using System.Collections.Generic; + +#if UNITTEST + +using NUnit.Framework; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the weak collection wrapper + [TestFixture] + internal class WeakCollectionTest { + + #region class Dummy + + /// Dummy class used to test the weakly referencing collection + private class Dummy { + + /// Initializes a new dummy + /// Value that will be stored by the dummy + public Dummy(int value) { + this.Value = value; + } + + /// + /// Determines whether the specified System.Object is equal to + /// the current Dummy object. + /// + /// + /// The System.Object to compare with the current Dummy object + /// + /// + /// True if the specified System.Object is equal to the current Dummy object; + /// otherwise, false. + /// + public override bool Equals(object otherAsObject) { + Dummy other = otherAsObject as Dummy; + if(other == null) { + return false; + } + return this.Value.Equals(other.Value); + } + + /// Serves as a hash function for a particular type. + /// A hash code for the current System.Object. + public override int GetHashCode() { + return this.Value.GetHashCode(); + } + + /// Some value that can be used for testing + public int Value; + + } + + #endregion // class Dummy + + #region class ListWithoutICollection + + private class ListWithoutICollection : IList> { + public int IndexOf(WeakReference item) { throw new NotImplementedException(); } + public void Insert(int index, WeakReference item) { + throw new NotImplementedException(); + } + public void RemoveAt(int index) { throw new NotImplementedException(); } + public WeakReference this[int index] { + get { throw new NotImplementedException(); } + set { throw new NotImplementedException(); } + } + public void Add(WeakReference item) { throw new NotImplementedException(); } + public void Clear() { throw new NotImplementedException(); } + public bool Contains(WeakReference item) { throw new NotImplementedException(); } + public void CopyTo(WeakReference[] array, int arrayIndex) { + throw new NotImplementedException(); + } + public int Count { get { return 12345; } } + public bool IsReadOnly { get { throw new NotImplementedException(); } } + public bool Remove(WeakReference item) { throw new NotImplementedException(); } + public IEnumerator> GetEnumerator() { + throw new NotImplementedException(); + } + IEnumerator IEnumerable.GetEnumerator() { throw new NotImplementedException(); } + } + + #endregion // class ListWithoutICollection + + /// Verifies that the constructor of the weak collection is working + [Test] + public void TestConstructor() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + Assert.IsNotNull(dummies); + } + + /// + /// Test whether the non-typesafe Add() method of the weak collection works + /// + [Test] + public void TestAddAsObject() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + + Dummy oneTwoThreeDummy = new Dummy(12345); + (dummies as IList).Add((object)oneTwoThreeDummy); + + CollectionAssert.Contains(dummies, oneTwoThreeDummy); + } + + /// + /// Test whether the non-typesafe Add() method throws an exception if an object is + /// added that is not compatible to the collection's item type + /// + [Test] + public void TestThrowOnAddIncompatibleObject() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + + Assert.Throws( + delegate() { (dummies as IList).Add(new object()); } + ); + } + + /// + /// Test whether the generic Add() method of the weak collection works + /// + [Test] + public void TestAdd() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + + Dummy oneTwoThreeDummy = new Dummy(12345); + dummies.Add(oneTwoThreeDummy); + + CollectionAssert.Contains(dummies, oneTwoThreeDummy); + } + + /// Tests whether the Clear() method works + [Test] + public void TestClear() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + + Dummy oneTwoThreeDummy = new Dummy(12345); + dummies.Add(oneTwoThreeDummy); + Dummy threeTwoOneDummy = new Dummy(54321); + dummies.Add(threeTwoOneDummy); + + Assert.AreEqual(2, dummies.Count); + + dummies.Clear(); + + Assert.AreEqual(0, dummies.Count); + } + + /// Tests whether the Contains() method works + [Test] + public void TestContains() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + + Dummy oneTwoThreeDummy = new Dummy(12345); + dummies.Add(oneTwoThreeDummy); + Dummy threeTwoOneDummy = new Dummy(54321); + + Assert.IsTrue(dummies.Contains(oneTwoThreeDummy)); + Assert.IsFalse(dummies.Contains(threeTwoOneDummy)); + } + + /// Tests whether the non-typesafe Contains() method works + [Test] + public void TestContainsWithObject() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + + Dummy oneTwoThreeDummy = new Dummy(12345); + dummies.Add(oneTwoThreeDummy); + Dummy threeTwoOneDummy = new Dummy(54321); + + Assert.IsTrue((dummies as IList).Contains((object)oneTwoThreeDummy)); + Assert.IsFalse((dummies as IList).Contains((object)threeTwoOneDummy)); + } + + /// + /// Verifies that the Enumerator of the dummy collection correctly + /// implements the Reset() method + /// + [Test] + public void TestEnumeratorReset() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + Dummy oneTwoThreeDummy = new Dummy(123); + dummies.Add(oneTwoThreeDummy); + Dummy fourFiveSixDummy = new Dummy(456); + dummies.Add(fourFiveSixDummy); + + IEnumerator dummyEnumerator = dummies.GetEnumerator(); + Assert.IsTrue(dummyEnumerator.MoveNext()); + Assert.IsTrue(dummyEnumerator.MoveNext()); + Assert.IsFalse(dummyEnumerator.MoveNext()); + + dummyEnumerator.Reset(); + + Assert.IsTrue(dummyEnumerator.MoveNext()); + Assert.IsTrue(dummyEnumerator.MoveNext()); + Assert.IsFalse(dummyEnumerator.MoveNext()); + } + + /// Verifies that the IndexOf() method is working as intended + [Test] + public void TestIndexOf() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + Dummy oneTwoThreeDummy = new Dummy(123); + dummies.Add(oneTwoThreeDummy); + Dummy fourFiveSixDummy = new Dummy(456); + dummies.Add(fourFiveSixDummy); + Dummy sevenEightNineDummy = new Dummy(789); + + Assert.AreEqual(0, dummies.IndexOf(oneTwoThreeDummy)); + Assert.AreEqual(1, dummies.IndexOf(fourFiveSixDummy)); + Assert.AreEqual(-1, dummies.IndexOf(sevenEightNineDummy)); + } + + /// + /// Verifies that the non-typesafe IndexOf() method is working as intended + /// + [Test] + public void TestIndexOfWithObject() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + Dummy oneTwoThreeDummy = new Dummy(123); + dummies.Add(oneTwoThreeDummy); + Dummy fourFiveSixDummy = new Dummy(456); + dummies.Add(fourFiveSixDummy); + Dummy sevenEightNineDummy = new Dummy(789); + + Assert.AreEqual(0, (dummies as IList).IndexOf((object)oneTwoThreeDummy)); + Assert.AreEqual(1, (dummies as IList).IndexOf((object)fourFiveSixDummy)); + Assert.AreEqual(-1, (dummies as IList).IndexOf((object)sevenEightNineDummy)); + } + + /// + /// Verifies that an exception is thrown if an incompatible object is passed to + /// the non-typesafe variant of the IndexOf() method + /// + [Test] + public void TestThrowOnIndexOfWithIncompatibleObject() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + + Assert.Throws( + delegate() { Assert.IsNull((dummies as IList).IndexOf(new object())); } + ); + } + + /// Test whether the IndexOf() method can cope with null references + [Test] + public void TestIndexOfNull() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + + Assert.AreEqual(-1, dummies.IndexOf(null)); + dummies.Add(null); + Assert.AreEqual(0, dummies.IndexOf(null)); + } + + /// + /// Verifies that the CopyTo() method of the weak collection works + /// + [Test] + public void TestCopyToArray() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + Dummy oneTwoThreeDummy = new Dummy(123); + dummies.Add(oneTwoThreeDummy); + Dummy fourFiveSixDummy = new Dummy(456); + dummies.Add(fourFiveSixDummy); + + Dummy[] inputDummies = new Dummy[] { oneTwoThreeDummy, fourFiveSixDummy }; + Dummy[] outputDummies = new Dummy[dummies.Count]; + + dummies.CopyTo(outputDummies, 0); + + CollectionAssert.AreEqual(inputDummies, outputDummies); + } + + /// + /// Verifies that the CopyTo() method of the weak collection throws an exception + /// if the target array is too small to hold the collection's contents + /// + [Test] + public void TestThrowOnCopyToTooSmallArray() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + Dummy oneTwoThreeDummy = new Dummy(123); + dummies.Add(oneTwoThreeDummy); + Dummy fourFiveSixDummy = new Dummy(456); + dummies.Add(fourFiveSixDummy); + + Dummy[] outputStrings = new Dummy[dummies.Count - 1]; + Assert.Throws( + delegate() { dummies.CopyTo(outputStrings, 0); } + ); + } + + /// + /// Verifies that the CopyTo() method of the transforming read only collection + /// works if invoked via the ICollection interface + /// + [Test] + public void TestCopyToArrayViaICollection() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + Dummy oneTwoThreeDummy = new Dummy(123); + dummies.Add(oneTwoThreeDummy); + Dummy fourFiveSixDummy = new Dummy(456); + dummies.Add(fourFiveSixDummy); + + Dummy[] inputDummies = new Dummy[] { oneTwoThreeDummy, fourFiveSixDummy }; + Dummy[] outputDummies = new Dummy[dummies.Count]; + + (dummies as ICollection).CopyTo(outputDummies, 0); + + CollectionAssert.AreEqual(inputDummies, outputDummies); + } + + /// + /// Verifies that the Insert() method correctly shifts items in the collection + /// + [Test] + public void TestInsert() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + Dummy oneTwoThreeDummy = new Dummy(123); + dummies.Add(oneTwoThreeDummy); + + Dummy fourFiveSixDummy = new Dummy(456); + dummies.Insert(0, fourFiveSixDummy); + + Assert.AreEqual(2, dummies.Count); + Assert.AreSame(fourFiveSixDummy, dummies[0]); + Assert.AreSame(oneTwoThreeDummy, dummies[1]); + } + + /// + /// Verifies that the non-typesafe Insert() method correctly shifts items in + /// the collection + /// + [Test] + public void TestInsertObject() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + Dummy oneTwoThreeDummy = new Dummy(123); + dummies.Add(oneTwoThreeDummy); + + Dummy fourFiveSixDummy = new Dummy(456); + (dummies as IList).Insert(0, (object)fourFiveSixDummy); + + Assert.AreEqual(2, dummies.Count); + Assert.AreSame(fourFiveSixDummy, dummies[0]); + Assert.AreSame(oneTwoThreeDummy, dummies[1]); + } + + /// + /// Verifies that the non-typesafe Insert() method correctly shifts items in + /// the collection + /// + [Test] + public void TestThrowOnInsertIncompatibleObject() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + Dummy oneTwoThreeDummy = new Dummy(123); + dummies.Add(oneTwoThreeDummy); + + Assert.Throws( + delegate() { (dummies as IList).Insert(0, new object()); } + ); + } + + /// + /// Checks whether the IsFixedSize property of the weak collection returns + /// the expected result for a weak collection based on a fixed array + /// + [Test] + public void TestIsFixedSizeViaIList() { + Dummy oneTwoThreeDummy = new Dummy(123); + Dummy fourFiveSixDummy = new Dummy(456); + + WeakReference[] dummyReferences = new WeakReference[] { + new WeakReference(oneTwoThreeDummy), + new WeakReference(fourFiveSixDummy) + }; + WeakCollection dummies = new WeakCollection(dummyReferences); + + Assert.IsTrue((dummies as IList).IsFixedSize); + } + + /// + /// Tests whether the IsReadOnly property of the weak collection works + /// + [Test] + public void TestIsReadOnly() { + Dummy oneTwoThreeDummy = new Dummy(123); + Dummy fourFiveSixDummy = new Dummy(456); + + List> dummyReferences = new List>(); + dummyReferences.Add(new WeakReference(oneTwoThreeDummy)); + dummyReferences.Add(new WeakReference(fourFiveSixDummy)); + + ReadOnlyList> readOnlyDummyReferences = + new ReadOnlyList>(dummyReferences); + + WeakCollection dummies = new WeakCollection(dummyReferences); + WeakCollection readOnlydummies = new WeakCollection( + readOnlyDummyReferences + ); + + Assert.IsFalse(dummies.IsReadOnly); + Assert.IsTrue(readOnlydummies.IsReadOnly); + } + + /// + /// Tests whether the IsSynchronized property of the weak collection works + /// + [Test] + public void TestIsSynchronized() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + + Assert.IsFalse((dummies as IList).IsSynchronized); + } + + /// Tests the indexer of the weak collection + [Test] + public void TestIndexer() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + Dummy oneTwoThreeDummy = new Dummy(123); + dummies.Add(oneTwoThreeDummy); + Dummy fourFiveSixDummy = new Dummy(456); + dummies.Add(fourFiveSixDummy); + + Assert.AreSame(oneTwoThreeDummy, dummies[0]); + Assert.AreSame(fourFiveSixDummy, dummies[1]); + + dummies[0] = fourFiveSixDummy; + + Assert.AreSame(fourFiveSixDummy, dummies[0]); + } + + /// Tests the non-typesafe indexer of the weak collection + [Test] + public void TestIndexerWithObject() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + Dummy oneTwoThreeDummy = new Dummy(123); + dummies.Add(oneTwoThreeDummy); + Dummy fourFiveSixDummy = new Dummy(456); + dummies.Add(fourFiveSixDummy); + + Assert.AreSame((object)oneTwoThreeDummy, (dummies as IList)[0]); + Assert.AreSame((object)fourFiveSixDummy, (dummies as IList)[1]); + + (dummies as IList)[0] = (object)fourFiveSixDummy; + + Assert.AreSame((object)fourFiveSixDummy, (dummies as IList)[0]); + } + + /// + /// Tests whether the non-typesafe indexer of the weak collection throws + /// the correct exception if an incompatible object is assigned + /// + [Test] + public void TestThrowOnIndexerWithIncompatibleObject() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + Dummy oneTwoThreeDummy = new Dummy(123); + dummies.Add(oneTwoThreeDummy); + + Assert.Throws( + delegate() { (dummies as IList)[0] = new object(); } + ); + } + + /// Tests the Remove() method of the weak collection + [Test] + public void TestRemove() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + Dummy oneTwoThreeDummy = new Dummy(123); + dummies.Add(oneTwoThreeDummy); + Dummy fourFiveSixDummy = new Dummy(456); + dummies.Add(fourFiveSixDummy); + + Assert.AreEqual(2, dummies.Count); + Assert.IsTrue(dummies.Remove(oneTwoThreeDummy)); + Assert.AreEqual(1, dummies.Count); + Assert.IsFalse(dummies.Remove(oneTwoThreeDummy)); + } + + /// Tests the non-typesafe Remove() method of the weak collection + [Test] + public void TestRemoveObject() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + Dummy oneTwoThreeDummy = new Dummy(123); + dummies.Add(oneTwoThreeDummy); + Dummy fourFiveSixDummy = new Dummy(456); + dummies.Add(fourFiveSixDummy); + + Assert.AreEqual(2, dummies.Count); + (dummies as IList).Remove((object)oneTwoThreeDummy); + Assert.AreEqual(1, dummies.Count); + } + + /// + /// Tests whether a null object can be managed by and removed from the weak collection + /// + [Test] + public void TestRemoveNull() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + dummies.Add(null); + + Assert.AreEqual(1, dummies.Count); + Assert.IsTrue(dummies.Remove(null)); + Assert.AreEqual(0, dummies.Count); + } + + /// + /// Tests whether the non-typesafe Remove() method of the weak collection throws + /// an exception if an object is tried to be removed that is incompatible with + /// the collection's item type + /// + [Test] + public void TestThrowOnRemoveIncompatibleObject() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + Assert.Throws( + delegate() { (dummies as IList).Remove(new object()); } + ); + } + + /// Tests the RemoveAt() method of the weak collection + [Test] + public void TestRemoveAt() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + Dummy oneTwoThreeDummy = new Dummy(123); + dummies.Add(oneTwoThreeDummy); + Dummy fourFiveSixDummy = new Dummy(456); + dummies.Add(fourFiveSixDummy); + + Assert.AreSame(oneTwoThreeDummy, dummies[0]); + dummies.RemoveAt(0); + Assert.AreSame(fourFiveSixDummy, dummies[0]); + } + + /// + /// Verifies that the IsSynchronized property and the SyncRoot property are working + /// + [Test] + public void TestSynchronization() { + WeakCollection dummies = new WeakCollection( + new List>() + ); + + if(!(dummies as ICollection).IsSynchronized) { + lock((dummies as ICollection).SyncRoot) { + Assert.AreEqual(0, dummies.Count); + } + } + } + + /// + /// Verifies that the IsSynchronized property and the SyncRoot property are working + /// on transforming read only collections based on IList<>s that do not + /// implement the ICollection interface + /// + [Test] + public void TestSynchronizationOfIListWithoutICollection() { + WeakCollection dummies = new WeakCollection( + new ListWithoutICollection() + ); + + if(!(dummies as ICollection).IsSynchronized) { + lock((dummies as ICollection).SyncRoot) { + int count = dummies.Count; + Assert.AreEqual(12345, count); // ;-) + } + } + } + + /// Tests the RemoveDeadItems() method + [Test] + public void TestRemoveDeadItems() { + List> dummyReferences = new List>(); + + Dummy oneTwoThreeDummy = new Dummy(123); + dummyReferences.Add(new WeakReference(oneTwoThreeDummy)); + + dummyReferences.Add(new WeakReference(null)); + + Dummy fourFiveSixDummy = new Dummy(456); + dummyReferences.Add(new WeakReference(fourFiveSixDummy)); + + WeakCollection dummies = new WeakCollection(dummyReferences); + + Assert.AreEqual(3, dummies.Count); + + dummies.RemoveDeadItems(); + + Assert.AreEqual(2, dummies.Count); + Assert.AreSame(oneTwoThreeDummy, dummies[0]); + Assert.AreSame(fourFiveSixDummy, dummies[1]); + } + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST diff --git a/Source/Collections/WeakCollection.cs b/Source/Collections/WeakCollection.cs index b2565fe..ed19269 100644 --- a/Source/Collections/WeakCollection.cs +++ b/Source/Collections/WeakCollection.cs @@ -1,339 +1,338 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Nuclex.Support.Collections { - - /// Collection of weakly referenced objects - /// - /// This collection tries to expose the interface of a normal collection, but stores - /// objects as weak references. When an object is accessed, it can return null. - /// when the collection detects that one of its items was garbage collected, it - /// will silently remove that item. - /// - public partial class WeakCollection : IList, IList - where TItem : class { - - #region class UnpackingEnumerator - - /// - /// An enumerator that unpacks the items returned by an enumerator of the - /// weak reference collection into the actual item type on-the-fly. - /// - private class UnpackingEnumerator : IEnumerator { - - /// Initializes a new unpacking enumerator - /// - /// Enumerator of the weak reference collection - /// - public UnpackingEnumerator( - IEnumerator> containedTypeEnumerator - ) { - this.containedTypeEnumerator = containedTypeEnumerator; - } - - /// Immediately releases all resources used by the instance - public void Dispose() { - this.containedTypeEnumerator.Dispose(); - } - - /// - /// The element in the collection at the current position of the enumerator. - /// - public TItem Current { - get { return this.containedTypeEnumerator.Current.Target; } - } - - /// Gets the current element in the collection. - /// The current element in the collection. - /// - /// The enumerator is positioned before the first element of the collection - /// or after the last element. - /// - public bool MoveNext() { - return this.containedTypeEnumerator.MoveNext(); - } - - /// - /// Sets the enumerator to its initial position, which is before the first element - /// in the collection. - /// - /// - /// The collection was modified after the enumerator was created. - /// - public void Reset() { - this.containedTypeEnumerator.Reset(); - } - - /// The current element in the collection. - /// - /// The enumerator is positioned before the first element of the collection - /// or after the last element. - /// - object IEnumerator.Current { - get { return Current; } - } - - /// An enumerator from the wrapped collection - private IEnumerator> containedTypeEnumerator; - - } - - #endregion // class UnpackingEnumerator - - /// Initializes a new weak reference collection - /// - /// Internal list of weak references that are unpacking when accessed through - /// the WeakCollection's interface. - /// - public WeakCollection(IList> items) : - this(items, EqualityComparer.Default) { } - - /// Initializes a new weak reference collection - /// - /// Internal list of weak references that are unpacking when accessed through - /// the WeakCollection's interface. - /// - /// - /// Comparer used to identify and compare items to each other - /// - public WeakCollection( - IList> items, IEqualityComparer comparer - ) { - this.items = items; - this.comparer = comparer; - } - - /// - /// Determines whether an element is in the WeakCollection - /// - /// - /// The object to locate in the WeakCollection. The value can be null. - /// - /// - /// True if value is found in the WeakCollection; otherwise, false. - /// - /// - /// The default implementation of this method is very unoptimized and will - /// enumerate all the items in the collection, transforming one after another - /// to check whether the transformed item matches the item the user was - /// looking for. It is recommended to provide a custom implementation of - /// this method, if possible. - /// - public virtual bool Contains(TItem item) { - return (IndexOf(item) != -1); - } - - /// - /// Copies the entire WeakCollection to a compatible one-dimensional - /// System.Array, starting at the specified index of the target array. - /// - /// - /// The one-dimensional System.Array that is the destination of the elements copied - /// from the WeakCollection. The System.Array must have zero-based indexing. - /// - /// - /// The zero-based index in array at which copying begins. - /// - /// - /// Index is equal to or greater than the length of array or the number of elements - /// in the source WeakCollection is greater than the available space from index to - /// the end of the destination array. - /// - /// - /// Index is less than zero. - /// - /// - /// Array is null. - /// - public void CopyTo(TItem[] array, int index) { - if(this.items.Count > (array.Length - index)) { - throw new ArgumentException( - "Array too small to fit the collection items starting at the specified index" - ); - } - - for(int itemIndex = 0; itemIndex < this.items.Count; ++itemIndex) { - array[itemIndex + index] = this.items[itemIndex].Target; - } - } - - /// Removes all items from the WeakCollection - public void Clear() { - this.items.Clear(); - } - - /// - /// Returns an enumerator that iterates through the WeakCollection. - /// - /// An enumerator or the WeakCollection. - public IEnumerator GetEnumerator() { - return new UnpackingEnumerator(this.items.GetEnumerator()); - } - - /// - /// Searches for the specified object and returns the zero-based index of the - /// first occurrence within the entire WeakCollection. - /// - /// - /// The object to locate in the WeakCollection. The value can - /// be null for reference types. - /// - /// - /// The zero-based index of the first occurrence of item within the entire - /// WeakCollection, if found; otherwise, -1. - /// - /// - /// The default implementation of this method is very unoptimized and will - /// enumerate all the items in the collection, transforming one after another - /// to check whether the transformed item matches the item the user was - /// looking for. It is recommended to provide a custom implementation of - /// this method, if possible. - /// - public int IndexOf(TItem item) { - for(int index = 0; index < this.items.Count; ++index) { - TItem itemAtIndex = this.items[index].Target; - if((itemAtIndex == null) || (item == null)) { - if(ReferenceEquals(item, itemAtIndex)) { - return index; - } - } else { - if(this.comparer.Equals(itemAtIndex, item)) { - return index; - } - } - } - - return -1; - } - - /// - /// The number of elements contained in the WeakCollection instance - /// - public int Count { - get { return this.items.Count; } - } - - /// Gets the element at the specified index. - /// The zero-based index of the element to get. - /// The element at the specified index. - /// - /// Index is less than zero or index is equal to or greater than - /// WeakCollection.Count. - /// - public TItem this[int index] { - get { return this.items[index].Target; } - set { this.items[index] = new WeakReference(value); } - } - - /// - /// Removes the first occurrence of a specific object from the WeakCollection. - /// - /// The object to remove from the WeakCollection - /// - /// True if item was successfully removed from the WeakCollection; otherwise, false. - /// - public bool Remove(TItem item) { - for(int index = 0; index < this.items.Count; ++index) { - TItem itemAtIndex = this.items[index].Target; - if((itemAtIndex == null) || (item == null)) { - if(ReferenceEquals(item, itemAtIndex)) { - this.items.RemoveAt(index); - return true; - } - } else { - if(this.comparer.Equals(item, itemAtIndex)) { - this.items.RemoveAt(index); - return true; - } - } - } - - return false; - } - - /// Adds an item to the WeakCollection. - /// The object to add to the WeakCollection - public void Add(TItem item) { - this.items.Add(new WeakReference(item)); - } - - /// Inserts an item to the WeakCollection at the specified index. - /// - /// The zero-based index at which item should be inserted. - /// - /// The object to insert into the WeakCollection - /// - /// index is not a valid index in the WeakCollection. - /// - public void Insert(int index, TItem item) { - this.items.Insert(index, new WeakReference(item)); - } - - /// - /// Removes the WeakCollection item at the specified index. - /// - /// The zero-based index of the item to remove. - /// - /// Index is not a valid index in the WeakCollection. - /// - public void RemoveAt(int index) { - this.items.RemoveAt(index); - } - - /// Whether the List is write-protected - public bool IsReadOnly { - get { return this.items.IsReadOnly; } - } - - /// - /// Removes the items that have been garbage collected from the collection - /// - public void RemoveDeadItems() { - int newCount = 0; - - // Eliminate all items that have been garbage collected by shifting - for(int index = 0; index < this.items.Count; ++index) { - if(this.items[index].IsAlive) { - this.items[newCount] = this.items[index]; - ++newCount; - } - } - - // If any garbage collected items were found, resize the collection so - // the space that became empty in the previous shifting process will be freed - while(this.items.Count > newCount) { - this.items.RemoveAt(this.items.Count - 1); - } - } - - /// Weak references to the items contained in the collection - private IList> items; - /// Used to identify and compare items in the collection - private IEqualityComparer comparer; - /// Synchronization root for threaded accesses to this collection - private object syncRoot; - - } - -} // namespace Nuclex.Support.Collections +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Nuclex.Support.Collections { + + /// Collection of weakly referenced objects + /// + /// This collection tries to expose the interface of a normal collection, but stores + /// objects as weak references. When an object is accessed, it can return null. + /// when the collection detects that one of its items was garbage collected, it + /// will silently remove that item. + /// + public partial class WeakCollection : IList, IList + where TItem : class { + + #region class UnpackingEnumerator + + /// + /// An enumerator that unpacks the items returned by an enumerator of the + /// weak reference collection into the actual item type on-the-fly. + /// + private class UnpackingEnumerator : IEnumerator { + + /// Initializes a new unpacking enumerator + /// + /// Enumerator of the weak reference collection + /// + public UnpackingEnumerator( + IEnumerator> containedTypeEnumerator + ) { + this.containedTypeEnumerator = containedTypeEnumerator; + } + + /// Immediately releases all resources used by the instance + public void Dispose() { + this.containedTypeEnumerator.Dispose(); + } + + /// + /// The element in the collection at the current position of the enumerator. + /// + public TItem Current { + get { return this.containedTypeEnumerator.Current.Target; } + } + + /// Gets the current element in the collection. + /// The current element in the collection. + /// + /// The enumerator is positioned before the first element of the collection + /// or after the last element. + /// + public bool MoveNext() { + return this.containedTypeEnumerator.MoveNext(); + } + + /// + /// Sets the enumerator to its initial position, which is before the first element + /// in the collection. + /// + /// + /// The collection was modified after the enumerator was created. + /// + public void Reset() { + this.containedTypeEnumerator.Reset(); + } + + /// The current element in the collection. + /// + /// The enumerator is positioned before the first element of the collection + /// or after the last element. + /// + object IEnumerator.Current { + get { return Current; } + } + + /// An enumerator from the wrapped collection + private IEnumerator> containedTypeEnumerator; + + } + + #endregion // class UnpackingEnumerator + + /// Initializes a new weak reference collection + /// + /// Internal list of weak references that are unpacking when accessed through + /// the WeakCollection's interface. + /// + public WeakCollection(IList> items) : + this(items, EqualityComparer.Default) { } + + /// Initializes a new weak reference collection + /// + /// Internal list of weak references that are unpacking when accessed through + /// the WeakCollection's interface. + /// + /// + /// Comparer used to identify and compare items to each other + /// + public WeakCollection( + IList> items, IEqualityComparer comparer + ) { + this.items = items; + this.comparer = comparer; + } + + /// + /// Determines whether an element is in the WeakCollection + /// + /// + /// The object to locate in the WeakCollection. The value can be null. + /// + /// + /// True if value is found in the WeakCollection; otherwise, false. + /// + /// + /// The default implementation of this method is very unoptimized and will + /// enumerate all the items in the collection, transforming one after another + /// to check whether the transformed item matches the item the user was + /// looking for. It is recommended to provide a custom implementation of + /// this method, if possible. + /// + public virtual bool Contains(TItem item) { + return (IndexOf(item) != -1); + } + + /// + /// Copies the entire WeakCollection to a compatible one-dimensional + /// System.Array, starting at the specified index of the target array. + /// + /// + /// The one-dimensional System.Array that is the destination of the elements copied + /// from the WeakCollection. The System.Array must have zero-based indexing. + /// + /// + /// The zero-based index in array at which copying begins. + /// + /// + /// Index is equal to or greater than the length of array or the number of elements + /// in the source WeakCollection is greater than the available space from index to + /// the end of the destination array. + /// + /// + /// Index is less than zero. + /// + /// + /// Array is null. + /// + public void CopyTo(TItem[] array, int index) { + if(this.items.Count > (array.Length - index)) { + throw new ArgumentException( + "Array too small to fit the collection items starting at the specified index" + ); + } + + for(int itemIndex = 0; itemIndex < this.items.Count; ++itemIndex) { + array[itemIndex + index] = this.items[itemIndex].Target; + } + } + + /// Removes all items from the WeakCollection + public void Clear() { + this.items.Clear(); + } + + /// + /// Returns an enumerator that iterates through the WeakCollection. + /// + /// An enumerator or the WeakCollection. + public IEnumerator GetEnumerator() { + return new UnpackingEnumerator(this.items.GetEnumerator()); + } + + /// + /// Searches for the specified object and returns the zero-based index of the + /// first occurrence within the entire WeakCollection. + /// + /// + /// The object to locate in the WeakCollection. The value can + /// be null for reference types. + /// + /// + /// The zero-based index of the first occurrence of item within the entire + /// WeakCollection, if found; otherwise, -1. + /// + /// + /// The default implementation of this method is very unoptimized and will + /// enumerate all the items in the collection, transforming one after another + /// to check whether the transformed item matches the item the user was + /// looking for. It is recommended to provide a custom implementation of + /// this method, if possible. + /// + public int IndexOf(TItem item) { + for(int index = 0; index < this.items.Count; ++index) { + TItem itemAtIndex = this.items[index].Target; + if((itemAtIndex == null) || (item == null)) { + if(ReferenceEquals(item, itemAtIndex)) { + return index; + } + } else { + if(this.comparer.Equals(itemAtIndex, item)) { + return index; + } + } + } + + return -1; + } + + /// + /// The number of elements contained in the WeakCollection instance + /// + public int Count { + get { return this.items.Count; } + } + + /// Gets the element at the specified index. + /// The zero-based index of the element to get. + /// The element at the specified index. + /// + /// Index is less than zero or index is equal to or greater than + /// WeakCollection.Count. + /// + public TItem this[int index] { + get { return this.items[index].Target; } + set { this.items[index] = new WeakReference(value); } + } + + /// + /// Removes the first occurrence of a specific object from the WeakCollection. + /// + /// The object to remove from the WeakCollection + /// + /// True if item was successfully removed from the WeakCollection; otherwise, false. + /// + public bool Remove(TItem item) { + for(int index = 0; index < this.items.Count; ++index) { + TItem itemAtIndex = this.items[index].Target; + if((itemAtIndex == null) || (item == null)) { + if(ReferenceEquals(item, itemAtIndex)) { + this.items.RemoveAt(index); + return true; + } + } else { + if(this.comparer.Equals(item, itemAtIndex)) { + this.items.RemoveAt(index); + return true; + } + } + } + + return false; + } + + /// Adds an item to the WeakCollection. + /// The object to add to the WeakCollection + public void Add(TItem item) { + this.items.Add(new WeakReference(item)); + } + + /// Inserts an item to the WeakCollection at the specified index. + /// + /// The zero-based index at which item should be inserted. + /// + /// The object to insert into the WeakCollection + /// + /// index is not a valid index in the WeakCollection. + /// + public void Insert(int index, TItem item) { + this.items.Insert(index, new WeakReference(item)); + } + + /// + /// Removes the WeakCollection item at the specified index. + /// + /// The zero-based index of the item to remove. + /// + /// Index is not a valid index in the WeakCollection. + /// + public void RemoveAt(int index) { + this.items.RemoveAt(index); + } + + /// Whether the List is write-protected + public bool IsReadOnly { + get { return this.items.IsReadOnly; } + } + + /// + /// Removes the items that have been garbage collected from the collection + /// + public void RemoveDeadItems() { + int newCount = 0; + + // Eliminate all items that have been garbage collected by shifting + for(int index = 0; index < this.items.Count; ++index) { + if(this.items[index].IsAlive) { + this.items[newCount] = this.items[index]; + ++newCount; + } + } + + // If any garbage collected items were found, resize the collection so + // the space that became empty in the previous shifting process will be freed + while(this.items.Count > newCount) { + this.items.RemoveAt(this.items.Count - 1); + } + } + + /// Weak references to the items contained in the collection + private IList> items; + /// Used to identify and compare items in the collection + private IEqualityComparer comparer; + /// Synchronization root for threaded accesses to this collection + private object syncRoot; + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/EnumHelper.Test.cs b/Source/EnumHelper.Test.cs index 34b104f..ae9e890 100644 --- a/Source/EnumHelper.Test.cs +++ b/Source/EnumHelper.Test.cs @@ -1,124 +1,123 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.IO; - -using NUnit.Framework; - -namespace Nuclex.Support { - - /// Unit Test for the enumeration helper class - [TestFixture] - internal class EnumHelperTest { - - #region enum TestEnumeration - - /// An enumeration used for unit testing - internal enum TestEnumeration { - /// First arbitrary enumeration value - One = -2, - /// Third arbitrary enumeration value - Three = 33, - /// Second arbitrary enumeration value - Two = 23 - } - - #endregion // enum TestEnumeration - - #region enum EmptyEnumeration - - internal enum EmptyEnumeration { } - - #endregion // enum EmptyEnumeration - - /// - /// Verifies that the enum helper can list the members of an enumeration - /// - [Test] - public void TestGetValues() { - CollectionAssert.AreEquivalent( - new TestEnumeration[] { - TestEnumeration.One, TestEnumeration.Two, TestEnumeration.Three - }, - EnumHelper.GetValues() - ); - } - - /// - /// Verifies that the enum helper can locate the highest value in an enumeration - /// - [Test] - public void TestGetHighestValue() { - Assert.AreEqual( - TestEnumeration.Three, EnumHelper.GetHighestValue() - ); - } - - /// - /// Verifies that the enum helper can locate the lowest value in an enumeration - /// - [Test] - public void TestGetLowestValue() { - Assert.AreEqual( - TestEnumeration.One, EnumHelper.GetLowestValue() - ); - } - - /// - /// Tests whether an exception is thrown if the GetValues() method is used on - /// a non-enumeration type - /// - [Test] - public void TestThrowOnNonEnumType() { - Assert.Throws( - delegate() { EnumHelper.GetValues(); } - ); - } - - /// - /// Verifies that the default value for an enumeration is returned if - /// the GetLowestValue() method is used on an empty enumeration - /// - [Test] - public void TestLowestValueInEmptyEnumeration() { - Assert.AreEqual( - default(EmptyEnumeration), EnumHelper.GetLowestValue() - ); - } - - /// - /// Verifies that the default value for an enumeration is returned if - /// the GetHighestValue() method is used on an empty enumeration - /// - [Test] - public void TestHighestValueInEmptyEnumeration() { - Assert.AreEqual( - default(EmptyEnumeration), EnumHelper.GetHighestValue() - ); - } - - } - -} // namespace Nuclex.Support - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.IO; + +using NUnit.Framework; + +namespace Nuclex.Support { + + /// Unit Test for the enumeration helper class + [TestFixture] + internal class EnumHelperTest { + + #region enum TestEnumeration + + /// An enumeration used for unit testing + internal enum TestEnumeration { + /// First arbitrary enumeration value + One = -2, + /// Third arbitrary enumeration value + Three = 33, + /// Second arbitrary enumeration value + Two = 23 + } + + #endregion // enum TestEnumeration + + #region enum EmptyEnumeration + + internal enum EmptyEnumeration { } + + #endregion // enum EmptyEnumeration + + /// + /// Verifies that the enum helper can list the members of an enumeration + /// + [Test] + public void TestGetValues() { + CollectionAssert.AreEquivalent( + new TestEnumeration[] { + TestEnumeration.One, TestEnumeration.Two, TestEnumeration.Three + }, + EnumHelper.GetValues() + ); + } + + /// + /// Verifies that the enum helper can locate the highest value in an enumeration + /// + [Test] + public void TestGetHighestValue() { + Assert.AreEqual( + TestEnumeration.Three, EnumHelper.GetHighestValue() + ); + } + + /// + /// Verifies that the enum helper can locate the lowest value in an enumeration + /// + [Test] + public void TestGetLowestValue() { + Assert.AreEqual( + TestEnumeration.One, EnumHelper.GetLowestValue() + ); + } + + /// + /// Tests whether an exception is thrown if the GetValues() method is used on + /// a non-enumeration type + /// + [Test] + public void TestThrowOnNonEnumType() { + Assert.Throws( + delegate() { EnumHelper.GetValues(); } + ); + } + + /// + /// Verifies that the default value for an enumeration is returned if + /// the GetLowestValue() method is used on an empty enumeration + /// + [Test] + public void TestLowestValueInEmptyEnumeration() { + Assert.AreEqual( + default(EmptyEnumeration), EnumHelper.GetLowestValue() + ); + } + + /// + /// Verifies that the default value for an enumeration is returned if + /// the GetHighestValue() method is used on an empty enumeration + /// + [Test] + public void TestHighestValueInEmptyEnumeration() { + Assert.AreEqual( + default(EmptyEnumeration), EnumHelper.GetHighestValue() + ); + } + + } + +} // namespace Nuclex.Support + +#endif // UNITTEST diff --git a/Source/EnumHelper.cs b/Source/EnumHelper.cs index 9a7628b..24edfd0 100644 --- a/Source/EnumHelper.cs +++ b/Source/EnumHelper.cs @@ -1,97 +1,96 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; - -namespace Nuclex.Support { - - /// Helper methods for enumerations - public static class EnumHelper { - - /// Returns the highest value encountered in an enumeration - /// - /// Enumeration of which the highest value will be returned - /// - /// The highest value in the enumeration - public static TEnumeration GetHighestValue() - where TEnumeration : IComparable { - TEnumeration[] values = GetValues(); - - // If the enumeration is empty, return nothing - if(values.Length == 0) { - return default(TEnumeration); - } - - // Look for the highest value in the enumeration. We initialize the highest value - // to the first enumeration value so we don't have to use some arbitrary starting - // value which might actually appear in the enumeration. - TEnumeration highestValue = values[0]; - for(int index = 1; index < values.Length; ++index) { - if(values[index].CompareTo(highestValue) > 0) { - highestValue = values[index]; - } - } - - return highestValue; - } - - /// Returns the lowest value encountered in an enumeration - /// - /// Enumeration of which the lowest value will be returned - /// - /// The lowest value in the enumeration - public static TEnumeration GetLowestValue() - where TEnumeration : IComparable { - TEnumeration[] values = GetValues(); - - // If the enumeration is empty, return nothing - if(values.Length == 0) { - return default(TEnumeration); - } - - // Look for the lowest value in the enumeration. We initialize the lowest value - // to the first enumeration value so we don't have to use some arbitrary starting - // value which might actually appear in the enumeration. - TEnumeration lowestValue = values[0]; - for(int index = 1; index < values.Length; ++index) { - if(values[index].CompareTo(lowestValue) < 0) { - lowestValue = values[index]; - } - } - - return lowestValue; - } - - /// Retrieves a list of all values contained in an enumeration - /// - /// Type of the enumeration whose values will be returned - /// - /// All values contained in the specified enumeration - /// - /// This method produces collectable garbage so it's best to only call it once - /// and cache the result. - /// - public static TEnum[] GetValues() { - return (TEnum[])Enum.GetValues(typeof(TEnum)); - } - - } - +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +namespace Nuclex.Support { + + /// Helper methods for enumerations + public static class EnumHelper { + + /// Returns the highest value encountered in an enumeration + /// + /// Enumeration of which the highest value will be returned + /// + /// The highest value in the enumeration + public static TEnumeration GetHighestValue() + where TEnumeration : IComparable { + TEnumeration[] values = GetValues(); + + // If the enumeration is empty, return nothing + if(values.Length == 0) { + return default(TEnumeration); + } + + // Look for the highest value in the enumeration. We initialize the highest value + // to the first enumeration value so we don't have to use some arbitrary starting + // value which might actually appear in the enumeration. + TEnumeration highestValue = values[0]; + for(int index = 1; index < values.Length; ++index) { + if(values[index].CompareTo(highestValue) > 0) { + highestValue = values[index]; + } + } + + return highestValue; + } + + /// Returns the lowest value encountered in an enumeration + /// + /// Enumeration of which the lowest value will be returned + /// + /// The lowest value in the enumeration + public static TEnumeration GetLowestValue() + where TEnumeration : IComparable { + TEnumeration[] values = GetValues(); + + // If the enumeration is empty, return nothing + if(values.Length == 0) { + return default(TEnumeration); + } + + // Look for the lowest value in the enumeration. We initialize the lowest value + // to the first enumeration value so we don't have to use some arbitrary starting + // value which might actually appear in the enumeration. + TEnumeration lowestValue = values[0]; + for(int index = 1; index < values.Length; ++index) { + if(values[index].CompareTo(lowestValue) < 0) { + lowestValue = values[index]; + } + } + + return lowestValue; + } + + /// Retrieves a list of all values contained in an enumeration + /// + /// Type of the enumeration whose values will be returned + /// + /// All values contained in the specified enumeration + /// + /// This method produces collectable garbage so it's best to only call it once + /// and cache the result. + /// + public static TEnum[] GetValues() { + return (TEnum[])Enum.GetValues(typeof(TEnum)); + } + + } + } // namespace Nuclex.Support \ No newline at end of file diff --git a/Source/FloatHelper.Test.cs b/Source/FloatHelper.Test.cs index 6c6a18b..2a94b8f 100644 --- a/Source/FloatHelper.Test.cs +++ b/Source/FloatHelper.Test.cs @@ -1,266 +1,265 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.Collections.Generic; - -using NUnit.Framework; - -namespace Nuclex.Support { - - /// Unit Test for the FloatHelper class - [TestFixture] - internal class FloatHelperTest { - - /// Tests the floating point value comparison helper - [Test] - public void UlpDistancesOnFloatsCompareAsEqual() { - Assert.IsTrue( - FloatHelper.AreAlmostEqual(0.00000001f, 0.0000000100000008f, 1), - "Minimal difference between very small floating point numbers is considered equal" - ); - Assert.IsFalse( - FloatHelper.AreAlmostEqual(0.00000001f, 0.0000000100000017f, 1), - "Larger difference between very small floating point numbers is not considered equal" - ); - - Assert.IsTrue( - FloatHelper.AreAlmostEqual(1000000.00f, 1000000.06f, 1), - "Minimal difference between very large floating point numbers is considered equal" - ); - Assert.IsFalse( - FloatHelper.AreAlmostEqual(1000000.00f, 1000000.13f, 1), - "Larger difference between very large floating point numbers is not considered equal" - ); - } - - /// Tests the double precision floating point value comparison helper - [Test] - public void UlpDistancesOnDoublesCompareAsEqual() { - Assert.IsTrue( - FloatHelper.AreAlmostEqual(0.00000001, 0.000000010000000000000002, 1), - "Minimal difference between very small double precision floating point " + - "numbers is considered equal" - ); - Assert.IsFalse( - FloatHelper.AreAlmostEqual(0.00000001, 0.000000010000000000000004, 1), - "Larger difference between very small double precision floating point " + - "numbers is not considered equal" - ); - - Assert.IsTrue( - FloatHelper.AreAlmostEqual(1000000.00, 1000000.0000000001, 1), - "Minimal difference between very large double precision floating point " + - "numbers is considered equal" - ); - Assert.IsFalse( - FloatHelper.AreAlmostEqual(1000000.00, 1000000.0000000002, 1), - "Larger difference between very large double precision floating point " + - "numbers is not considered equal" - ); - } - - /// Tests the integer reinterpretation functions - [Test] - public void IntegersCanBeReinterpretedAsFloats() { - Assert.AreEqual( - 12345.0f, - FloatHelper.ReinterpretAsFloat(FloatHelper.ReinterpretAsInt(12345.0f)), - "Number hasn't changed after mirrored reinterpretation" - ); - } - - /// Tests the long reinterpretation functions - [Test] - public void LongsCanBeReinterpretedAsDoubles() { - Assert.AreEqual( - 12345.67890, - FloatHelper.ReinterpretAsDouble(FloatHelper.ReinterpretAsLong(12345.67890)), - "Number hasn't changed after mirrored reinterpretation" - ); - } - - /// Tests the floating point reinterpretation functions - [Test] - public void FloatsCanBeReinterpretedAsIntegers() { - Assert.AreEqual( - 12345, - FloatHelper.ReinterpretAsInt(FloatHelper.ReinterpretAsFloat(12345)), - "Number hasn't changed after mirrored reinterpretation" - ); - } - - /// - /// Verifies that the IsZero() method can distinguish zero from very small values - /// - [Test] - public void CanDetermineIfFloatIsZero() { - Assert.IsTrue(FloatHelper.IsZero(FloatHelper.PositiveZeroFloat)); - Assert.IsTrue(FloatHelper.IsZero(FloatHelper.NegativeZeroFloat)); - Assert.IsFalse(FloatHelper.IsZero(1.401298E-45f)); - Assert.IsFalse(FloatHelper.IsZero(-1.401298E-45f)); - } - - /// - /// Verifies that the IsZero() method can distinguish zero from very small values - /// - [Test] - public void CanDetermineIfDoubleIsZero() { - Assert.IsTrue(FloatHelper.IsZero(FloatHelper.PositiveZeroDouble)); - Assert.IsTrue(FloatHelper.IsZero(FloatHelper.NegativeZeroDouble)); - Assert.IsFalse(FloatHelper.IsZero(4.94065645841247E-324)); - Assert.IsFalse(FloatHelper.IsZero(-4.94065645841247E-324)); - } - - /// - /// Tests the double prevision floating point reinterpretation functions - /// - [Test] - public void DoublesCanBeReinterpretedAsLongs() { - Assert.AreEqual( - 1234567890, - FloatHelper.ReinterpretAsLong(FloatHelper.ReinterpretAsDouble(1234567890)), - "Number hasn't changed after mirrored reinterpretation" - ); - } - - /// - /// Verifies that two denormalized floats can be compared in ulps - /// - [Test] - public void DenormalizedFloatsCanBeCompared() { - float zero = 0.0f; - float zeroPlusOneUlp = FloatHelper.ReinterpretAsFloat( - FloatHelper.ReinterpretAsInt(zero) + 1 - ); - float zeroMinusOneUlp = -zeroPlusOneUlp; - - // Across zero - Assert.IsFalse(FloatHelper.AreAlmostEqual(zeroMinusOneUlp, zeroPlusOneUlp, 1)); - Assert.IsTrue(FloatHelper.AreAlmostEqual(zeroPlusOneUlp, zeroMinusOneUlp, 2)); - - // Against zero - Assert.IsFalse(FloatHelper.AreAlmostEqual(zero, zeroPlusOneUlp, 0)); - Assert.IsTrue(FloatHelper.AreAlmostEqual(zero, zeroPlusOneUlp, 1)); - Assert.IsFalse(FloatHelper.AreAlmostEqual(zero, zeroMinusOneUlp, 0)); - Assert.IsTrue(FloatHelper.AreAlmostEqual(zero, zeroMinusOneUlp, 1)); - } - - /// - /// Verifies that the negative floating point zero is within one ulp of the positive - /// floating point zero and vice versa - /// - [Test] - public void NegativeZeroFloatEqualsPositiveZero() { - Assert.IsTrue( - FloatHelper.AreAlmostEqual( - FloatHelper.NegativeZeroFloat, FloatHelper.PositiveZeroFloat, 0 - ) - ); - Assert.IsTrue( - FloatHelper.AreAlmostEqual( - FloatHelper.PositiveZeroFloat, FloatHelper.NegativeZeroFloat, 0 - ) - ); - } - - /// Verifies that floats can be compared across the zero boundary - [Test] - public void FloatsCanBeComparedAcrossZeroInUlps() { - float tenUlps = float.Epsilon * 10.0f; - - Assert.IsTrue(FloatHelper.AreAlmostEqual(-tenUlps, tenUlps, 20)); - Assert.IsTrue(FloatHelper.AreAlmostEqual(tenUlps, -tenUlps, 20)); - Assert.IsFalse(FloatHelper.AreAlmostEqual(-tenUlps, tenUlps, 19)); - - Assert.IsTrue(FloatHelper.AreAlmostEqual(-tenUlps, 0, 10)); - Assert.IsTrue(FloatHelper.AreAlmostEqual(0, -tenUlps, 10)); - Assert.IsFalse(FloatHelper.AreAlmostEqual(-tenUlps, 0, 9)); - - Assert.IsTrue(FloatHelper.AreAlmostEqual(0, tenUlps, 10)); - Assert.IsTrue(FloatHelper.AreAlmostEqual(tenUlps, 0, 10)); - Assert.IsFalse(FloatHelper.AreAlmostEqual(0, tenUlps, 9)); - } - - /// - /// Verifies that two denormalized doubles can be compared in ulps - /// - [Test] - public void DenormalizedDoublesCanBeCompared() { - double zero = 0.0; - double zeroPlusOneUlp = FloatHelper.ReinterpretAsDouble( - FloatHelper.ReinterpretAsLong(zero) + 1 - ); - double zeroMinusOneUlp = -zeroPlusOneUlp; - - // Across zero - Assert.IsFalse(FloatHelper.AreAlmostEqual(zeroMinusOneUlp, zeroPlusOneUlp, 1)); - Assert.IsTrue(FloatHelper.AreAlmostEqual(zeroPlusOneUlp, zeroMinusOneUlp, 2)); - - // Against zero - Assert.IsFalse(FloatHelper.AreAlmostEqual(zero, zeroPlusOneUlp, 0)); - Assert.IsTrue(FloatHelper.AreAlmostEqual(zero, zeroPlusOneUlp, 1)); - Assert.IsFalse(FloatHelper.AreAlmostEqual(zero, zeroMinusOneUlp, 0)); - Assert.IsTrue(FloatHelper.AreAlmostEqual(zero, zeroMinusOneUlp, 1)); - } - - /// - /// Verifies that the negative double precision floating point zero is within one ulp - /// of the positive double precision floating point zero and vice versa - /// - [Test] - public void NegativeZeroDoubleEqualsPositiveZero() { - Assert.IsTrue( - FloatHelper.AreAlmostEqual( - FloatHelper.NegativeZeroDouble, FloatHelper.NegativeZeroDouble, 0 - ) - ); - Assert.IsTrue( - FloatHelper.AreAlmostEqual( - FloatHelper.NegativeZeroDouble, FloatHelper.NegativeZeroDouble, 0 - ) - ); - } - - /// Verifies that doubles can be compared across the zero boundary - [Test] - public void DoublesCanBeComparedAcrossZeroInUlps() { - double tenUlps = double.Epsilon * 10.0; - - Assert.IsTrue(FloatHelper.AreAlmostEqual(-tenUlps, tenUlps, 20)); - Assert.IsTrue(FloatHelper.AreAlmostEqual(tenUlps, -tenUlps, 20)); - Assert.IsFalse(FloatHelper.AreAlmostEqual(-tenUlps, tenUlps, 19)); - - Assert.IsTrue(FloatHelper.AreAlmostEqual(-tenUlps, 0, 10)); - Assert.IsTrue(FloatHelper.AreAlmostEqual(0, -tenUlps, 10)); - Assert.IsFalse(FloatHelper.AreAlmostEqual(-tenUlps, 0, 9)); - - Assert.IsTrue(FloatHelper.AreAlmostEqual(0, tenUlps, 10)); - Assert.IsTrue(FloatHelper.AreAlmostEqual(tenUlps, 0, 10)); - Assert.IsFalse(FloatHelper.AreAlmostEqual(0, tenUlps, 9)); - } - - } - -} // namespace Nuclex.Support - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.Collections.Generic; + +using NUnit.Framework; + +namespace Nuclex.Support { + + /// Unit Test for the FloatHelper class + [TestFixture] + internal class FloatHelperTest { + + /// Tests the floating point value comparison helper + [Test] + public void UlpDistancesOnFloatsCompareAsEqual() { + Assert.IsTrue( + FloatHelper.AreAlmostEqual(0.00000001f, 0.0000000100000008f, 1), + "Minimal difference between very small floating point numbers is considered equal" + ); + Assert.IsFalse( + FloatHelper.AreAlmostEqual(0.00000001f, 0.0000000100000017f, 1), + "Larger difference between very small floating point numbers is not considered equal" + ); + + Assert.IsTrue( + FloatHelper.AreAlmostEqual(1000000.00f, 1000000.06f, 1), + "Minimal difference between very large floating point numbers is considered equal" + ); + Assert.IsFalse( + FloatHelper.AreAlmostEqual(1000000.00f, 1000000.13f, 1), + "Larger difference between very large floating point numbers is not considered equal" + ); + } + + /// Tests the double precision floating point value comparison helper + [Test] + public void UlpDistancesOnDoublesCompareAsEqual() { + Assert.IsTrue( + FloatHelper.AreAlmostEqual(0.00000001, 0.000000010000000000000002, 1), + "Minimal difference between very small double precision floating point " + + "numbers is considered equal" + ); + Assert.IsFalse( + FloatHelper.AreAlmostEqual(0.00000001, 0.000000010000000000000004, 1), + "Larger difference between very small double precision floating point " + + "numbers is not considered equal" + ); + + Assert.IsTrue( + FloatHelper.AreAlmostEqual(1000000.00, 1000000.0000000001, 1), + "Minimal difference between very large double precision floating point " + + "numbers is considered equal" + ); + Assert.IsFalse( + FloatHelper.AreAlmostEqual(1000000.00, 1000000.0000000002, 1), + "Larger difference between very large double precision floating point " + + "numbers is not considered equal" + ); + } + + /// Tests the integer reinterpretation functions + [Test] + public void IntegersCanBeReinterpretedAsFloats() { + Assert.AreEqual( + 12345.0f, + FloatHelper.ReinterpretAsFloat(FloatHelper.ReinterpretAsInt(12345.0f)), + "Number hasn't changed after mirrored reinterpretation" + ); + } + + /// Tests the long reinterpretation functions + [Test] + public void LongsCanBeReinterpretedAsDoubles() { + Assert.AreEqual( + 12345.67890, + FloatHelper.ReinterpretAsDouble(FloatHelper.ReinterpretAsLong(12345.67890)), + "Number hasn't changed after mirrored reinterpretation" + ); + } + + /// Tests the floating point reinterpretation functions + [Test] + public void FloatsCanBeReinterpretedAsIntegers() { + Assert.AreEqual( + 12345, + FloatHelper.ReinterpretAsInt(FloatHelper.ReinterpretAsFloat(12345)), + "Number hasn't changed after mirrored reinterpretation" + ); + } + + /// + /// Verifies that the IsZero() method can distinguish zero from very small values + /// + [Test] + public void CanDetermineIfFloatIsZero() { + Assert.IsTrue(FloatHelper.IsZero(FloatHelper.PositiveZeroFloat)); + Assert.IsTrue(FloatHelper.IsZero(FloatHelper.NegativeZeroFloat)); + Assert.IsFalse(FloatHelper.IsZero(1.401298E-45f)); + Assert.IsFalse(FloatHelper.IsZero(-1.401298E-45f)); + } + + /// + /// Verifies that the IsZero() method can distinguish zero from very small values + /// + [Test] + public void CanDetermineIfDoubleIsZero() { + Assert.IsTrue(FloatHelper.IsZero(FloatHelper.PositiveZeroDouble)); + Assert.IsTrue(FloatHelper.IsZero(FloatHelper.NegativeZeroDouble)); + Assert.IsFalse(FloatHelper.IsZero(4.94065645841247E-324)); + Assert.IsFalse(FloatHelper.IsZero(-4.94065645841247E-324)); + } + + /// + /// Tests the double prevision floating point reinterpretation functions + /// + [Test] + public void DoublesCanBeReinterpretedAsLongs() { + Assert.AreEqual( + 1234567890, + FloatHelper.ReinterpretAsLong(FloatHelper.ReinterpretAsDouble(1234567890)), + "Number hasn't changed after mirrored reinterpretation" + ); + } + + /// + /// Verifies that two denormalized floats can be compared in ulps + /// + [Test] + public void DenormalizedFloatsCanBeCompared() { + float zero = 0.0f; + float zeroPlusOneUlp = FloatHelper.ReinterpretAsFloat( + FloatHelper.ReinterpretAsInt(zero) + 1 + ); + float zeroMinusOneUlp = -zeroPlusOneUlp; + + // Across zero + Assert.IsFalse(FloatHelper.AreAlmostEqual(zeroMinusOneUlp, zeroPlusOneUlp, 1)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(zeroPlusOneUlp, zeroMinusOneUlp, 2)); + + // Against zero + Assert.IsFalse(FloatHelper.AreAlmostEqual(zero, zeroPlusOneUlp, 0)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(zero, zeroPlusOneUlp, 1)); + Assert.IsFalse(FloatHelper.AreAlmostEqual(zero, zeroMinusOneUlp, 0)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(zero, zeroMinusOneUlp, 1)); + } + + /// + /// Verifies that the negative floating point zero is within one ulp of the positive + /// floating point zero and vice versa + /// + [Test] + public void NegativeZeroFloatEqualsPositiveZero() { + Assert.IsTrue( + FloatHelper.AreAlmostEqual( + FloatHelper.NegativeZeroFloat, FloatHelper.PositiveZeroFloat, 0 + ) + ); + Assert.IsTrue( + FloatHelper.AreAlmostEqual( + FloatHelper.PositiveZeroFloat, FloatHelper.NegativeZeroFloat, 0 + ) + ); + } + + /// Verifies that floats can be compared across the zero boundary + [Test] + public void FloatsCanBeComparedAcrossZeroInUlps() { + float tenUlps = float.Epsilon * 10.0f; + + Assert.IsTrue(FloatHelper.AreAlmostEqual(-tenUlps, tenUlps, 20)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(tenUlps, -tenUlps, 20)); + Assert.IsFalse(FloatHelper.AreAlmostEqual(-tenUlps, tenUlps, 19)); + + Assert.IsTrue(FloatHelper.AreAlmostEqual(-tenUlps, 0, 10)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(0, -tenUlps, 10)); + Assert.IsFalse(FloatHelper.AreAlmostEqual(-tenUlps, 0, 9)); + + Assert.IsTrue(FloatHelper.AreAlmostEqual(0, tenUlps, 10)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(tenUlps, 0, 10)); + Assert.IsFalse(FloatHelper.AreAlmostEqual(0, tenUlps, 9)); + } + + /// + /// Verifies that two denormalized doubles can be compared in ulps + /// + [Test] + public void DenormalizedDoublesCanBeCompared() { + double zero = 0.0; + double zeroPlusOneUlp = FloatHelper.ReinterpretAsDouble( + FloatHelper.ReinterpretAsLong(zero) + 1 + ); + double zeroMinusOneUlp = -zeroPlusOneUlp; + + // Across zero + Assert.IsFalse(FloatHelper.AreAlmostEqual(zeroMinusOneUlp, zeroPlusOneUlp, 1)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(zeroPlusOneUlp, zeroMinusOneUlp, 2)); + + // Against zero + Assert.IsFalse(FloatHelper.AreAlmostEqual(zero, zeroPlusOneUlp, 0)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(zero, zeroPlusOneUlp, 1)); + Assert.IsFalse(FloatHelper.AreAlmostEqual(zero, zeroMinusOneUlp, 0)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(zero, zeroMinusOneUlp, 1)); + } + + /// + /// Verifies that the negative double precision floating point zero is within one ulp + /// of the positive double precision floating point zero and vice versa + /// + [Test] + public void NegativeZeroDoubleEqualsPositiveZero() { + Assert.IsTrue( + FloatHelper.AreAlmostEqual( + FloatHelper.NegativeZeroDouble, FloatHelper.NegativeZeroDouble, 0 + ) + ); + Assert.IsTrue( + FloatHelper.AreAlmostEqual( + FloatHelper.NegativeZeroDouble, FloatHelper.NegativeZeroDouble, 0 + ) + ); + } + + /// Verifies that doubles can be compared across the zero boundary + [Test] + public void DoublesCanBeComparedAcrossZeroInUlps() { + double tenUlps = double.Epsilon * 10.0; + + Assert.IsTrue(FloatHelper.AreAlmostEqual(-tenUlps, tenUlps, 20)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(tenUlps, -tenUlps, 20)); + Assert.IsFalse(FloatHelper.AreAlmostEqual(-tenUlps, tenUlps, 19)); + + Assert.IsTrue(FloatHelper.AreAlmostEqual(-tenUlps, 0, 10)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(0, -tenUlps, 10)); + Assert.IsFalse(FloatHelper.AreAlmostEqual(-tenUlps, 0, 9)); + + Assert.IsTrue(FloatHelper.AreAlmostEqual(0, tenUlps, 10)); + Assert.IsTrue(FloatHelper.AreAlmostEqual(tenUlps, 0, 10)); + Assert.IsFalse(FloatHelper.AreAlmostEqual(0, tenUlps, 9)); + } + + } + +} // namespace Nuclex.Support + +#endif // UNITTEST diff --git a/Source/FloatHelper.cs b/Source/FloatHelper.cs index be69e3a..79f6565 100644 --- a/Source/FloatHelper.cs +++ b/Source/FloatHelper.cs @@ -1,314 +1,313 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Runtime.InteropServices; - -namespace Nuclex.Support { - - /// Helper routines for working with floating point numbers - /// - /// - /// The floating point comparison code is based on this excellent article: - /// http://www.cygnus-software.com/papers/comparingfloats/comparingfloats.htm - /// - /// - /// "ULP" means Unit in the Last Place and in the context of this library refers to - /// the distance between two adjacent floating point numbers. IEEE floating point - /// numbers can only represent a finite subset of natural numbers, with greater - /// accuracy for smaller numbers and lower accuracy for very large numbers. - /// - /// - /// If a comparison is allowed "2 ulps" of deviation, that means the values are - /// allowed to deviate by up to 2 adjacent floating point values, which might be - /// as low as 0.0000001 for small numbers or as high as 10.0 for large numbers. - /// - /// - public static class FloatHelper { - - #region struct FloatIntUnion - - /// Union of a floating point variable and an integer - [StructLayout(LayoutKind.Explicit)] - private struct FloatIntUnion { - - /// The union's value as a floating point variable - [FieldOffset(0)] - public float Float; - - /// The union's value as an integer - [FieldOffset(0)] - public int Int; - - /// The union's value as an unsigned integer - [FieldOffset(0)] - public uint UInt; - - } - - #endregion // struct FloatIntUnion - - #region struct DoubleLongUnion - - /// Union of a double precision floating point variable and a long - [StructLayout(LayoutKind.Explicit)] - private struct DoubleLongUnion { - - /// The union's value as a double precision floating point variable - [FieldOffset(0)] - public double Double; - - /// The union's value as a long - [FieldOffset(0)] - public long Long; - - /// The union's value as an unsigned long - [FieldOffset(0)] - public ulong ULong; - - } - - #endregion // struct DoubleLongUnion - - /// A floating point value that holds a positive zero - public const float PositiveZeroFloat = +0.0f; - - /// A floating point value that holds a negative zero - /// - /// Negative zeros have a special representation in IEEE 752 floating point math - /// - public const float NegativeZeroFloat = -0.0f; - - /// A double precision floating point value that holds a positive zero - public const double PositiveZeroDouble = +0.0; - - /// A doublep precision floating point value that holds a negative zero - /// - /// Negative zeros have a special representation in IEEE 752 floating point math - /// - public const double NegativeZeroDouble = -0.0; - - /// Checks whether the floating point value is exactly zero - /// Value that will be checked for being zero - /// True if the value is zero, false otherwise - public static bool IsZero(float value) { - return (value == PositiveZeroFloat) || (value == NegativeZeroFloat); - } - - /// - /// Checks whether the double precision floating point value is exactly zero - /// - /// Value that will be checked for being zero - /// True if the value is zero, false otherwise - public static bool IsZero(double value) { - return (value == PositiveZeroDouble) || (value == NegativeZeroDouble); - } - - /// Compares two floating point values for equality - /// First floating point value to be compared - /// Second floating point value t be compared - /// - /// Maximum number of representable floating point values that are allowed to - /// be between the left and the right floating point values - /// - /// True if both numbers are equal or close to being equal - /// - /// - /// Floating point values can only represent a finite subset of natural numbers. - /// For example, the values 2.00000000 and 2.00000024 can be stored in a float, - /// but nothing inbetween them. - /// - /// - /// This comparison will count how many possible floating point values are between - /// the left and the right number. If the number of possible values between both - /// numbers is less than or equal to maxUlps, then the numbers are considered as - /// being equal. - /// - /// - /// Implementation partially follows the code outlined here (link now defunct): - /// http://www.anttirt.net/2007/08/19/proper-floating-point-comparisons/ - /// And here: - /// http://www.altdevblogaday.com/2012/02/22/comparing-floating-point-numbers-2012-edition/ - /// - /// - public static bool AreAlmostEqual(float left, float right, int maxUlps) { - var leftUnion = new FloatIntUnion(); - var rightUnion = new FloatIntUnion(); - - leftUnion.Float = left; - rightUnion.Float = right; - - if(leftUnion.Int < 0) { - leftUnion.Int = unchecked((int)0x80000000 - leftUnion.Int); - } - if(rightUnion.Int < 0) { - rightUnion.Int = unchecked((int)0x80000000 - rightUnion.Int); - } - - return Math.Abs(rightUnion.Int - leftUnion.Int) <= maxUlps; - } -#if false - public static bool OldAreAlmostEqual(float left, float right, int maxUlps) { - FloatInt32Union leftUnion = new FloatInt32Union(); - FloatInt32Union rightUnion = new FloatInt32Union(); - - leftUnion.Float = left; - rightUnion.Float = right; - - uint leftSignMask = (leftUnion.UInt >> 31); - uint rightSignMask = (rightUnion.UInt >> 31); - - uint leftTemp = ((0x80000000 - leftUnion.UInt) & leftSignMask); - leftUnion.UInt = leftTemp | (leftUnion.UInt & ~leftSignMask); - - uint rightTemp = ((0x80000000 - rightUnion.UInt) & rightSignMask); - rightUnion.UInt = rightTemp | (rightUnion.UInt & ~rightSignMask); - - return (Math.Abs(leftUnion.Int - rightUnion.Int) <= maxUlps); - } -#endif - - /// Compares two double precision floating point values for equality - /// First double precision floating point value to be compared - /// Second double precision floating point value t be compared - /// - /// Maximum number of representable double precision floating point values that are - /// allowed to be between the left and the right double precision floating point values - /// - /// True if both numbers are equal or close to being equal - /// - /// - /// Double precision floating point values can only represent a limited series of - /// natural numbers. For example, the values 2.0000000000000000 and 2.0000000000000004 - /// can be stored in a double, but nothing inbetween them. - /// - /// - /// This comparison will count how many possible double precision floating point - /// values are between the left and the right number. If the number of possible - /// values between both numbers is less than or equal to maxUlps, then the numbers - /// are considered as being equal. - /// - /// - /// Implementation partially follows the code outlined here: - /// http://www.anttirt.net/2007/08/19/proper-floating-point-comparisons/ - /// And here: - /// http://www.altdevblogaday.com/2012/02/22/comparing-floating-point-numbers-2012-edition/ - /// - /// - public static bool AreAlmostEqual(double left, double right, long maxUlps) { - var leftUnion = new DoubleLongUnion(); - var rightUnion = new DoubleLongUnion(); - - leftUnion.Double = left; - rightUnion.Double = right; - - if(leftUnion.Long < 0) { - leftUnion.Long = unchecked((long)0x8000000000000000 - leftUnion.Long); - } - if(rightUnion.Long < 0) { - rightUnion.Long = unchecked((long)0x8000000000000000 - rightUnion.Long); - } - - return Math.Abs(rightUnion.Long - leftUnion.Long) <= maxUlps; - } -#if false - public static bool OldAreAlmostEqual(double left, double right, long maxUlps) { - DoubleInt64Union leftUnion = new DoubleInt64Union(); - DoubleInt64Union rightUnion = new DoubleInt64Union(); - - leftUnion.Double = left; - rightUnion.Double = right; - - ulong leftSignMask = (leftUnion.ULong >> 63); - ulong rightSignMask = (rightUnion.ULong >> 63); - - ulong leftTemp = ((0x8000000000000000 - leftUnion.ULong) & leftSignMask); - leftUnion.ULong = leftTemp | (leftUnion.ULong & ~leftSignMask); - - ulong rightTemp = ((0x8000000000000000 - rightUnion.ULong) & rightSignMask); - rightUnion.ULong = rightTemp | (rightUnion.ULong & ~rightSignMask); - - return (Math.Abs(leftUnion.Long - rightUnion.Long) <= maxUlps); - } -#endif - - /// - /// Reinterprets the memory contents of a floating point value as an integer value - /// - /// - /// Floating point value whose memory contents to reinterpret - /// - /// - /// The memory contents of the floating point value interpreted as an integer - /// - public static int ReinterpretAsInt(this float value) { - FloatIntUnion union = new FloatIntUnion(); - union.Float = value; - return union.Int; - } - - /// - /// Reinterprets the memory contents of a double precision floating point - /// value as an integer value - /// - /// - /// Double precision floating point value whose memory contents to reinterpret - /// - /// - /// The memory contents of the double precision floating point value - /// interpreted as an integer - /// - public static long ReinterpretAsLong(this double value) { - DoubleLongUnion union = new DoubleLongUnion(); - union.Double = value; - return union.Long; - } - - /// - /// Reinterprets the memory contents of an integer as a floating point value - /// - /// Integer value whose memory contents to reinterpret - /// - /// The memory contents of the integer value interpreted as a floating point value - /// - public static float ReinterpretAsFloat(this int value) { - FloatIntUnion union = new FloatIntUnion(); - union.Int = value; - return union.Float; - } - - /// - /// Reinterprets the memory contents of an integer value as a double precision - /// floating point value - /// - /// Integer whose memory contents to reinterpret - /// - /// The memory contents of the integer interpreted as a double precision - /// floating point value - /// - public static double ReinterpretAsDouble(this long value) { - DoubleLongUnion union = new DoubleLongUnion(); - union.Long = value; - return union.Double; - } - - } - -} // namespace Nuclex.Support +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Runtime.InteropServices; + +namespace Nuclex.Support { + + /// Helper routines for working with floating point numbers + /// + /// + /// The floating point comparison code is based on this excellent article: + /// http://www.cygnus-software.com/papers/comparingfloats/comparingfloats.htm + /// + /// + /// "ULP" means Unit in the Last Place and in the context of this library refers to + /// the distance between two adjacent floating point numbers. IEEE floating point + /// numbers can only represent a finite subset of natural numbers, with greater + /// accuracy for smaller numbers and lower accuracy for very large numbers. + /// + /// + /// If a comparison is allowed "2 ulps" of deviation, that means the values are + /// allowed to deviate by up to 2 adjacent floating point values, which might be + /// as low as 0.0000001 for small numbers or as high as 10.0 for large numbers. + /// + /// + public static class FloatHelper { + + #region struct FloatIntUnion + + /// Union of a floating point variable and an integer + [StructLayout(LayoutKind.Explicit)] + private struct FloatIntUnion { + + /// The union's value as a floating point variable + [FieldOffset(0)] + public float Float; + + /// The union's value as an integer + [FieldOffset(0)] + public int Int; + + /// The union's value as an unsigned integer + [FieldOffset(0)] + public uint UInt; + + } + + #endregion // struct FloatIntUnion + + #region struct DoubleLongUnion + + /// Union of a double precision floating point variable and a long + [StructLayout(LayoutKind.Explicit)] + private struct DoubleLongUnion { + + /// The union's value as a double precision floating point variable + [FieldOffset(0)] + public double Double; + + /// The union's value as a long + [FieldOffset(0)] + public long Long; + + /// The union's value as an unsigned long + [FieldOffset(0)] + public ulong ULong; + + } + + #endregion // struct DoubleLongUnion + + /// A floating point value that holds a positive zero + public const float PositiveZeroFloat = +0.0f; + + /// A floating point value that holds a negative zero + /// + /// Negative zeros have a special representation in IEEE 752 floating point math + /// + public const float NegativeZeroFloat = -0.0f; + + /// A double precision floating point value that holds a positive zero + public const double PositiveZeroDouble = +0.0; + + /// A doublep precision floating point value that holds a negative zero + /// + /// Negative zeros have a special representation in IEEE 752 floating point math + /// + public const double NegativeZeroDouble = -0.0; + + /// Checks whether the floating point value is exactly zero + /// Value that will be checked for being zero + /// True if the value is zero, false otherwise + public static bool IsZero(float value) { + return (value == PositiveZeroFloat) || (value == NegativeZeroFloat); + } + + /// + /// Checks whether the double precision floating point value is exactly zero + /// + /// Value that will be checked for being zero + /// True if the value is zero, false otherwise + public static bool IsZero(double value) { + return (value == PositiveZeroDouble) || (value == NegativeZeroDouble); + } + + /// Compares two floating point values for equality + /// First floating point value to be compared + /// Second floating point value t be compared + /// + /// Maximum number of representable floating point values that are allowed to + /// be between the left and the right floating point values + /// + /// True if both numbers are equal or close to being equal + /// + /// + /// Floating point values can only represent a finite subset of natural numbers. + /// For example, the values 2.00000000 and 2.00000024 can be stored in a float, + /// but nothing inbetween them. + /// + /// + /// This comparison will count how many possible floating point values are between + /// the left and the right number. If the number of possible values between both + /// numbers is less than or equal to maxUlps, then the numbers are considered as + /// being equal. + /// + /// + /// Implementation partially follows the code outlined here (link now defunct): + /// http://www.anttirt.net/2007/08/19/proper-floating-point-comparisons/ + /// And here: + /// http://www.altdevblogaday.com/2012/02/22/comparing-floating-point-numbers-2012-edition/ + /// + /// + public static bool AreAlmostEqual(float left, float right, int maxUlps) { + var leftUnion = new FloatIntUnion(); + var rightUnion = new FloatIntUnion(); + + leftUnion.Float = left; + rightUnion.Float = right; + + if(leftUnion.Int < 0) { + leftUnion.Int = unchecked((int)0x80000000 - leftUnion.Int); + } + if(rightUnion.Int < 0) { + rightUnion.Int = unchecked((int)0x80000000 - rightUnion.Int); + } + + return Math.Abs(rightUnion.Int - leftUnion.Int) <= maxUlps; + } +#if false + public static bool OldAreAlmostEqual(float left, float right, int maxUlps) { + FloatInt32Union leftUnion = new FloatInt32Union(); + FloatInt32Union rightUnion = new FloatInt32Union(); + + leftUnion.Float = left; + rightUnion.Float = right; + + uint leftSignMask = (leftUnion.UInt >> 31); + uint rightSignMask = (rightUnion.UInt >> 31); + + uint leftTemp = ((0x80000000 - leftUnion.UInt) & leftSignMask); + leftUnion.UInt = leftTemp | (leftUnion.UInt & ~leftSignMask); + + uint rightTemp = ((0x80000000 - rightUnion.UInt) & rightSignMask); + rightUnion.UInt = rightTemp | (rightUnion.UInt & ~rightSignMask); + + return (Math.Abs(leftUnion.Int - rightUnion.Int) <= maxUlps); + } +#endif + + /// Compares two double precision floating point values for equality + /// First double precision floating point value to be compared + /// Second double precision floating point value t be compared + /// + /// Maximum number of representable double precision floating point values that are + /// allowed to be between the left and the right double precision floating point values + /// + /// True if both numbers are equal or close to being equal + /// + /// + /// Double precision floating point values can only represent a limited series of + /// natural numbers. For example, the values 2.0000000000000000 and 2.0000000000000004 + /// can be stored in a double, but nothing inbetween them. + /// + /// + /// This comparison will count how many possible double precision floating point + /// values are between the left and the right number. If the number of possible + /// values between both numbers is less than or equal to maxUlps, then the numbers + /// are considered as being equal. + /// + /// + /// Implementation partially follows the code outlined here: + /// http://www.anttirt.net/2007/08/19/proper-floating-point-comparisons/ + /// And here: + /// http://www.altdevblogaday.com/2012/02/22/comparing-floating-point-numbers-2012-edition/ + /// + /// + public static bool AreAlmostEqual(double left, double right, long maxUlps) { + var leftUnion = new DoubleLongUnion(); + var rightUnion = new DoubleLongUnion(); + + leftUnion.Double = left; + rightUnion.Double = right; + + if(leftUnion.Long < 0) { + leftUnion.Long = unchecked((long)0x8000000000000000 - leftUnion.Long); + } + if(rightUnion.Long < 0) { + rightUnion.Long = unchecked((long)0x8000000000000000 - rightUnion.Long); + } + + return Math.Abs(rightUnion.Long - leftUnion.Long) <= maxUlps; + } +#if false + public static bool OldAreAlmostEqual(double left, double right, long maxUlps) { + DoubleInt64Union leftUnion = new DoubleInt64Union(); + DoubleInt64Union rightUnion = new DoubleInt64Union(); + + leftUnion.Double = left; + rightUnion.Double = right; + + ulong leftSignMask = (leftUnion.ULong >> 63); + ulong rightSignMask = (rightUnion.ULong >> 63); + + ulong leftTemp = ((0x8000000000000000 - leftUnion.ULong) & leftSignMask); + leftUnion.ULong = leftTemp | (leftUnion.ULong & ~leftSignMask); + + ulong rightTemp = ((0x8000000000000000 - rightUnion.ULong) & rightSignMask); + rightUnion.ULong = rightTemp | (rightUnion.ULong & ~rightSignMask); + + return (Math.Abs(leftUnion.Long - rightUnion.Long) <= maxUlps); + } +#endif + + /// + /// Reinterprets the memory contents of a floating point value as an integer value + /// + /// + /// Floating point value whose memory contents to reinterpret + /// + /// + /// The memory contents of the floating point value interpreted as an integer + /// + public static int ReinterpretAsInt(this float value) { + FloatIntUnion union = new FloatIntUnion(); + union.Float = value; + return union.Int; + } + + /// + /// Reinterprets the memory contents of a double precision floating point + /// value as an integer value + /// + /// + /// Double precision floating point value whose memory contents to reinterpret + /// + /// + /// The memory contents of the double precision floating point value + /// interpreted as an integer + /// + public static long ReinterpretAsLong(this double value) { + DoubleLongUnion union = new DoubleLongUnion(); + union.Double = value; + return union.Long; + } + + /// + /// Reinterprets the memory contents of an integer as a floating point value + /// + /// Integer value whose memory contents to reinterpret + /// + /// The memory contents of the integer value interpreted as a floating point value + /// + public static float ReinterpretAsFloat(this int value) { + FloatIntUnion union = new FloatIntUnion(); + union.Int = value; + return union.Float; + } + + /// + /// Reinterprets the memory contents of an integer value as a double precision + /// floating point value + /// + /// Integer whose memory contents to reinterpret + /// + /// The memory contents of the integer interpreted as a double precision + /// floating point value + /// + public static double ReinterpretAsDouble(this long value) { + DoubleLongUnion union = new DoubleLongUnion(); + union.Long = value; + return union.Double; + } + + } + +} // namespace Nuclex.Support diff --git a/Source/GarbagePolicy.cs b/Source/GarbagePolicy.cs index 844e9c3..260ce27 100644 --- a/Source/GarbagePolicy.cs +++ b/Source/GarbagePolicy.cs @@ -1,35 +1,34 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; - -namespace Nuclex.Support { - - /// How to behave in in respect to the garbage collector - public enum GarbagePolicy { - - /// Avoid feeding the garbage collector whenever possible - Avoid, - /// Accept garbage production - Accept - - } - -} // namespace Nuclex.Support +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +namespace Nuclex.Support { + + /// How to behave in in respect to the garbage collector + public enum GarbagePolicy { + + /// Avoid feeding the garbage collector whenever possible + Avoid, + /// Accept garbage production + Accept + + } + +} // namespace Nuclex.Support diff --git a/Source/IO/ChainStream.Test.cs b/Source/IO/ChainStream.Test.cs index 0d4e707..92c5e94 100644 --- a/Source/IO/ChainStream.Test.cs +++ b/Source/IO/ChainStream.Test.cs @@ -1,562 +1,561 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.IO; - -using NUnit.Framework; - -namespace Nuclex.Support.IO { - - /// Unit Test for the stream chainer - [TestFixture] - internal class ChainStreamTest { - - #region class TestStream - - /// Testing stream that allows specific features to be disabled - private class TestStream : Stream { - - /// Initializes a new test stream - /// Stream that will be wrapped - /// Whether to allow reading from the stream - /// Whether to allow writing to the stream - /// Whether to allow seeking within the stream - public TestStream( - Stream wrappedStream, bool allowRead, bool allowWrite, bool allowSeek - ) { - this.stream = wrappedStream; - this.readAllowed = allowRead; - this.writeAllowed = allowWrite; - this.seekAllowed = allowSeek; - } - - /// Whether data can be read from the stream - public override bool CanRead { - get { return this.readAllowed; } - } - - /// Whether the stream supports seeking - public override bool CanSeek { - get { return this.seekAllowed; } - } - - /// Whether data can be written into the stream - public override bool CanWrite { - get { return this.writeAllowed; } - } - - /// - /// Clears all buffers for this stream and causes any buffered data to be written - /// to the underlying device. - /// - public override void Flush() { - ++this.flushCallCount; - this.stream.Flush(); - } - - /// Length of the stream in bytes - public override long Length { - get { - enforceSeekAllowed(); - return this.stream.Length; - } - } - - /// Absolute position of the file pointer within the stream - /// - /// At least one of the chained streams does not support seeking - /// - public override long Position { - get { - enforceSeekAllowed(); - return this.stream.Position; - } - set { - enforceSeekAllowed(); - this.stream.Position = value; - } - } - - /// - /// Reads a sequence of bytes from the stream and advances the position of - /// the file pointer by the number of bytes read. - /// - /// Buffer that will receive the data read from the stream - /// - /// Offset in the buffer at which the stream will place the data read - /// - /// Maximum number of bytes that will be read - /// - /// The number of bytes that were actually read from the stream and written into - /// the provided buffer - /// - public override int Read(byte[] buffer, int offset, int count) { - enforceReadAllowed(); - return this.stream.Read(buffer, offset, count); - } - - /// Changes the position of the file pointer - /// - /// Offset to move the file pointer by, relative to the position indicated by - /// the parameter. - /// - /// - /// Reference point relative to which the file pointer is placed - /// - /// The new absolute position within the stream - public override long Seek(long offset, SeekOrigin origin) { - enforceSeekAllowed(); - return this.stream.Seek(offset, origin); - } - - /// Changes the length of the stream - /// New length the stream shall have - public override void SetLength(long value) { - enforceSeekAllowed(); - this.stream.SetLength(value); - } - - /// - /// Writes a sequence of bytes to the stream and advances the position of - /// the file pointer by the number of bytes written. - /// - /// - /// Buffer containing the data that will be written to the stream - /// - /// - /// Offset in the buffer at which the data to be written starts - /// - /// Number of bytes that will be written into the stream - public override void Write(byte[] buffer, int offset, int count) { - enforceWriteAllowed(); - this.stream.Write(buffer, offset, count); - } - - /// Number of times the Flush() method has been called - public int FlushCallCount { - get { return this.flushCallCount; } - } - - /// Throws an exception if reading is not allowed - private void enforceReadAllowed() { - if(!this.readAllowed) { - throw new NotSupportedException("Reading has been disabled"); - } - } - - /// Throws an exception if writing is not allowed - private void enforceWriteAllowed() { - if(!this.writeAllowed) { - throw new NotSupportedException("Writing has been disabled"); - } - } - - /// Throws an exception if seeking is not allowed - private void enforceSeekAllowed() { - if(!this.seekAllowed) { - throw new NotSupportedException("Seeking has been disabled"); - } - } - - /// Stream being wrapped for testing - private Stream stream; - /// whether to allow reading from the wrapped stream - private bool readAllowed; - /// Whether to allow writing to the wrapped stream - private bool writeAllowed; - /// Whether to allow seeking within the wrapped stream - private bool seekAllowed; - /// Number of times the Flush() method has been called - private int flushCallCount; - - } - - #endregion // class TestStream - - /// - /// Tests whether the stream chainer correctly partitions a long write request - /// over its chained streams and appends any remaining data to the end of - /// the last chained stream. - /// - [Test] - public void TestPartitionedWrite() { - ChainStream chainer = chainTwoStreamsOfTenBytes(); - - byte[] testData = new byte[20]; - for(int index = 0; index < testData.Length; ++index) { - testData[index] = (byte)(index + 1); - } - - chainer.Position = 5; - chainer.Write(testData, 0, testData.Length); - - Assert.AreEqual( - new byte[] { 0, 0, 0, 0, 0, 1, 2, 3, 4, 5 }, - ((MemoryStream)chainer.ChainedStreams[0]).ToArray() - ); - Assert.AreEqual( - new byte[] { 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 }, - ((MemoryStream)chainer.ChainedStreams[1]).ToArray() - ); - } - - /// - /// Tests whether the stream chainer correctly partitions a long read request - /// over its chained streams. - /// - [Test] - public void TestPartitionedRead() { - ChainStream chainer = chainTwoStreamsOfTenBytes(); - - ((MemoryStream)chainer.ChainedStreams[0]).Write( - new byte[] { 1, 2, 3, 4, 5 }, 0, 5 - ); - ((MemoryStream)chainer.ChainedStreams[1]).Write( - new byte[] { 6, 7, 8, 9, 10 }, 0, 5 - ); - - chainer.Position = 3; - byte[] buffer = new byte[15]; - int bytesRead = chainer.Read(buffer, 0, 14); - - Assert.AreEqual(14, bytesRead); - Assert.AreEqual(new byte[] { 4, 5, 0, 0, 0, 0, 0, 6, 7, 8, 9, 10, 0, 0, 0 }, buffer); - } - - /// - /// Tests whether the stream chainer can handle a stream resize - /// - [Test] - public void TestWriteAfterResize() { - ChainStream chainer = chainTwoStreamsOfTenBytes(); - - // The first stream has a size of 10 bytes, so this goes into the second stream - chainer.Position = 11; - chainer.Write(new byte[] { 12, 34 }, 0, 2); - - // Now we resize the first stream to 15 bytes, so this goes into the first stream - ((MemoryStream)chainer.ChainedStreams[0]).SetLength(15); - chainer.Write(new byte[] { 56, 78, 11, 22 }, 0, 4); - - Assert.AreEqual( - new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 56, 78 }, - ((MemoryStream)chainer.ChainedStreams[0]).ToArray() - ); - Assert.AreEqual( - new byte[] { 11, 22, 34, 0, 0, 0, 0, 0, 0, 0 }, - ((MemoryStream)chainer.ChainedStreams[1]).ToArray() - ); - } - - /// - /// Tests writing to a stream chainer that contains an unseekable stream - /// - [Test] - public void TestWriteToUnseekableStream() { - MemoryStream firstStream = new MemoryStream(); - - // Now the second stream _does_ support seeking. If the stream chainer ignores - // that, it would overwrite data in the second stream. - MemoryStream secondStream = new MemoryStream(); - secondStream.Write(new byte[] { 0, 9, 8, 7, 6 }, 0, 5); - secondStream.Position = 0; - - TestStream testStream = new TestStream(firstStream, true, true, false); - ChainStream chainer = new ChainStream(new Stream[] { testStream, secondStream }); - - chainer.Write(new byte[] { 1, 2, 3, 4, 5 }, 0, 5); - Assert.IsFalse(chainer.CanSeek); - Assert.AreEqual(0, firstStream.Length); - Assert.AreEqual(new byte[] { 0, 9, 8, 7, 6, 1, 2, 3, 4, 5 }, secondStream.ToArray()); - } - - /// - /// Tests reading from a stream chainer that contains an unseekable stream - /// - [Test] - public void TestReadFromUnseekableStream() { - MemoryStream firstStream = new MemoryStream(); - - // Now the second stream _does_ support seeking. If the stream chainer ignores - // that, it would overwrite data in the second stream. - MemoryStream secondStream = new MemoryStream(); - secondStream.Write(new byte[] { 0, 9, 8, 7, 6 }, 0, 5); - secondStream.Position = 3; - - TestStream testStream = new TestStream(firstStream, true, true, false); - ChainStream chainer = new ChainStream(new Stream[] { testStream, secondStream }); - - Assert.IsFalse(chainer.CanSeek); - - byte[] buffer = new byte[5]; - int readByteCount = chainer.Read(buffer, 0, 3); - - Assert.AreEqual(3, readByteCount); - Assert.AreEqual(new byte[] { 0, 9, 8, 0, 0 }, buffer); - - readByteCount = chainer.Read(buffer, 0, 3); - - Assert.AreEqual(2, readByteCount); - Assert.AreEqual(new byte[] { 7, 6, 8, 0, 0 }, buffer); - } - - /// - /// Tests reading from a stream chainer that contains an unreadable stream - /// - [Test] - public void TestThrowOnReadFromUnreadableStream() { - MemoryStream memoryStream = new MemoryStream(); - TestStream testStream = new TestStream(memoryStream, false, true, true); - ChainStream chainer = new ChainStream(new Stream[] { testStream }); - Assert.Throws( - delegate() { chainer.Read(new byte[5], 0, 5); } - ); - } - - /// - /// Tests writing to a stream chainer that contains an unwriteable stream - /// - [Test] - public void TestThrowOnWriteToUnwriteableStream() { - MemoryStream memoryStream = new MemoryStream(); - TestStream testStream = new TestStream(memoryStream, true, false, true); - ChainStream chainer = new ChainStream(new Stream[] { testStream }); - Assert.Throws( - delegate() { chainer.Write(new byte[] { 1, 2, 3, 4, 5 }, 0, 5); } - ); - } - - /// - /// Verifies that the stream chainer throws an exception if the attempt is - /// made to change the length of the stream - /// - [Test] - public void TestThrowOnLengthChange() { - ChainStream chainer = chainTwoStreamsOfTenBytes(); - Assert.Throws( - delegate() { chainer.SetLength(123); } - ); - } - - /// - /// Verifies that the CanRead property is correctly determined by the stream chainer - /// - [Test] - public void TestCanRead() { - MemoryStream yesStream = new MemoryStream(); - TestStream noStream = new TestStream(yesStream, false, true, true); - - Stream[] yesGroup = new Stream[] { yesStream, yesStream, yesStream, yesStream }; - Stream[] partialGroup = new Stream[] { yesStream, yesStream, noStream, yesStream }; - Stream[] noGroup = new Stream[] { noStream, noStream, noStream, noStream }; - - Assert.IsTrue(new ChainStream(yesGroup).CanRead); - Assert.IsFalse(new ChainStream(partialGroup).CanRead); - Assert.IsFalse(new ChainStream(noGroup).CanRead); - } - - /// - /// Verifies that the CanRead property is correctly determined by the stream chainer - /// - [Test] - public void TestCanWrite() { - MemoryStream yesStream = new MemoryStream(); - TestStream noStream = new TestStream(yesStream, true, false, true); - - Stream[] yesGroup = new Stream[] { yesStream, yesStream, yesStream, yesStream }; - Stream[] partialGroup = new Stream[] { yesStream, yesStream, noStream, yesStream }; - Stream[] noGroup = new Stream[] { noStream, noStream, noStream, noStream }; - - Assert.IsTrue(new ChainStream(yesGroup).CanWrite); - Assert.IsFalse(new ChainStream(partialGroup).CanWrite); - Assert.IsFalse(new ChainStream(noGroup).CanWrite); - } - - /// - /// Verifies that the CanSeek property is correctly determined by the stream chainer - /// - [Test] - public void TestCanSeek() { - MemoryStream yesStream = new MemoryStream(); - TestStream noStream = new TestStream(yesStream, true, true, false); - - Stream[] yesGroup = new Stream[] { yesStream, yesStream, yesStream, yesStream }; - Stream[] partialGroup = new Stream[] { yesStream, yesStream, noStream, yesStream }; - Stream[] noGroup = new Stream[] { noStream, noStream, noStream, noStream }; - - Assert.IsTrue(new ChainStream(yesGroup).CanSeek); - Assert.IsFalse(new ChainStream(partialGroup).CanSeek); - Assert.IsFalse(new ChainStream(noGroup).CanSeek); - } - - /// - /// Tests whether an exception is thrown if the Seek() method is called on - /// a stream chainer with streams that do not support seeking - /// - [Test] - public void TestThrowOnSeekWithUnseekableStream() { - MemoryStream memoryStream = new MemoryStream(); - TestStream testStream = new TestStream(memoryStream, true, true, false); - - ChainStream chainer = new ChainStream(new Stream[] { testStream }); - Assert.Throws( - delegate() { chainer.Seek(123, SeekOrigin.Begin); } - ); - } - - /// - /// Tests whether an exception is thrown if the Position property is retrieved - /// on a stream chainer with streams that do not support seeking - /// - [Test] - public void TestThrowOnGetPositionWithUnseekableStream() { - MemoryStream memoryStream = new MemoryStream(); - TestStream testStream = new TestStream(memoryStream, true, true, false); - - ChainStream chainer = new ChainStream(new Stream[] { testStream }); - Assert.Throws( - delegate() { Console.WriteLine(chainer.Position); } - ); - } - - /// - /// Tests whether an exception is thrown if the Position property is set - /// on a stream chainer with streams that do not support seeking - /// - [Test] - public void TestThrowOnSetPositionWithUnseekableStream() { - MemoryStream memoryStream = new MemoryStream(); - TestStream testStream = new TestStream(memoryStream, true, true, false); - - ChainStream chainer = new ChainStream(new Stream[] { testStream }); - Assert.Throws( - delegate() { chainer.Position = 123; } - ); - } - - /// - /// Tests whether an exception is thrown if the Length property is retrieved - /// on a stream chainer with streams that do not support seeking - /// - [Test] - public void TestThrowOnGetLengthWithUnseekableStream() { - MemoryStream memoryStream = new MemoryStream(); - TestStream testStream = new TestStream(memoryStream, true, true, false); - - ChainStream chainer = new ChainStream(new Stream[] { testStream }); - Assert.Throws( - delegate() { Assert.IsTrue(chainer.Length != chainer.Length); } - ); - } - - /// - /// Tests whether the Seek() method of the stream chainer is working - /// - [Test] - public void TestSeeking() { - ChainStream chainer = chainTwoStreamsOfTenBytes(); - - Assert.AreEqual(7, chainer.Seek(-13, SeekOrigin.End)); - Assert.AreEqual(14, chainer.Seek(7, SeekOrigin.Current)); - Assert.AreEqual(11, chainer.Seek(11, SeekOrigin.Begin)); - } - - /// - /// Tests whether the stream behaves correctly if data is read from beyond its end - /// - [Test] - public void TestReadBeyondEndOfStream() { - ChainStream chainer = chainTwoStreamsOfTenBytes(); - chainer.Seek(10, SeekOrigin.End); - - // This is how the MemoryStream behaves: it returns 0 bytes. - int readByteCount = chainer.Read(new byte[1], 0, 1); - Assert.AreEqual(0, readByteCount); - } - - /// - /// Tests whether the Seek() method throws an exception if an invalid - /// reference point is provided - /// - [Test] - public void TestThrowOnInvalidSeekReferencePoint() { - ChainStream chainer = chainTwoStreamsOfTenBytes(); - Assert.Throws( - delegate() { chainer.Seek(1, (SeekOrigin)12345); } - ); - } - - /// Verifies that the position property works correctly - [Test] - public void TestPositionChange() { - ChainStream chainer = chainTwoStreamsOfTenBytes(); - - chainer.Position = 7; - Assert.AreEqual(chainer.Position, 7); - chainer.Position = 14; - Assert.AreEqual(chainer.Position, 14); - } - - /// Tests the Flush() method of the stream chainer - [Test] - public void TestFlush() { - MemoryStream firstStream = new MemoryStream(); - TestStream firstTestStream = new TestStream(firstStream, true, true, true); - - MemoryStream secondStream = new MemoryStream(); - TestStream secondTestStream = new TestStream(secondStream, true, true, true); - - ChainStream chainer = new ChainStream( - new Stream[] { firstTestStream, secondTestStream } - ); - - Assert.AreEqual(0, firstTestStream.FlushCallCount); - Assert.AreEqual(0, secondTestStream.FlushCallCount); - - chainer.Flush(); - - Assert.AreEqual(1, firstTestStream.FlushCallCount); - Assert.AreEqual(1, secondTestStream.FlushCallCount); - } - - /// - /// Creates a stream chainer with two streams that each have a size of 10 bytes - /// - /// The new stream chainer with two chained 10-byte streams - private static ChainStream chainTwoStreamsOfTenBytes() { - MemoryStream firstStream = new MemoryStream(10); - MemoryStream secondStream = new MemoryStream(10); - - firstStream.SetLength(10); - secondStream.SetLength(10); - - return new ChainStream( - new Stream[] { firstStream, secondStream } - ); - } - - } - -} // namespace Nuclex.Support.IO - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.IO; + +using NUnit.Framework; + +namespace Nuclex.Support.IO { + + /// Unit Test for the stream chainer + [TestFixture] + internal class ChainStreamTest { + + #region class TestStream + + /// Testing stream that allows specific features to be disabled + private class TestStream : Stream { + + /// Initializes a new test stream + /// Stream that will be wrapped + /// Whether to allow reading from the stream + /// Whether to allow writing to the stream + /// Whether to allow seeking within the stream + public TestStream( + Stream wrappedStream, bool allowRead, bool allowWrite, bool allowSeek + ) { + this.stream = wrappedStream; + this.readAllowed = allowRead; + this.writeAllowed = allowWrite; + this.seekAllowed = allowSeek; + } + + /// Whether data can be read from the stream + public override bool CanRead { + get { return this.readAllowed; } + } + + /// Whether the stream supports seeking + public override bool CanSeek { + get { return this.seekAllowed; } + } + + /// Whether data can be written into the stream + public override bool CanWrite { + get { return this.writeAllowed; } + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + public override void Flush() { + ++this.flushCallCount; + this.stream.Flush(); + } + + /// Length of the stream in bytes + public override long Length { + get { + enforceSeekAllowed(); + return this.stream.Length; + } + } + + /// Absolute position of the file pointer within the stream + /// + /// At least one of the chained streams does not support seeking + /// + public override long Position { + get { + enforceSeekAllowed(); + return this.stream.Position; + } + set { + enforceSeekAllowed(); + this.stream.Position = value; + } + } + + /// + /// Reads a sequence of bytes from the stream and advances the position of + /// the file pointer by the number of bytes read. + /// + /// Buffer that will receive the data read from the stream + /// + /// Offset in the buffer at which the stream will place the data read + /// + /// Maximum number of bytes that will be read + /// + /// The number of bytes that were actually read from the stream and written into + /// the provided buffer + /// + public override int Read(byte[] buffer, int offset, int count) { + enforceReadAllowed(); + return this.stream.Read(buffer, offset, count); + } + + /// Changes the position of the file pointer + /// + /// Offset to move the file pointer by, relative to the position indicated by + /// the parameter. + /// + /// + /// Reference point relative to which the file pointer is placed + /// + /// The new absolute position within the stream + public override long Seek(long offset, SeekOrigin origin) { + enforceSeekAllowed(); + return this.stream.Seek(offset, origin); + } + + /// Changes the length of the stream + /// New length the stream shall have + public override void SetLength(long value) { + enforceSeekAllowed(); + this.stream.SetLength(value); + } + + /// + /// Writes a sequence of bytes to the stream and advances the position of + /// the file pointer by the number of bytes written. + /// + /// + /// Buffer containing the data that will be written to the stream + /// + /// + /// Offset in the buffer at which the data to be written starts + /// + /// Number of bytes that will be written into the stream + public override void Write(byte[] buffer, int offset, int count) { + enforceWriteAllowed(); + this.stream.Write(buffer, offset, count); + } + + /// Number of times the Flush() method has been called + public int FlushCallCount { + get { return this.flushCallCount; } + } + + /// Throws an exception if reading is not allowed + private void enforceReadAllowed() { + if(!this.readAllowed) { + throw new NotSupportedException("Reading has been disabled"); + } + } + + /// Throws an exception if writing is not allowed + private void enforceWriteAllowed() { + if(!this.writeAllowed) { + throw new NotSupportedException("Writing has been disabled"); + } + } + + /// Throws an exception if seeking is not allowed + private void enforceSeekAllowed() { + if(!this.seekAllowed) { + throw new NotSupportedException("Seeking has been disabled"); + } + } + + /// Stream being wrapped for testing + private Stream stream; + /// whether to allow reading from the wrapped stream + private bool readAllowed; + /// Whether to allow writing to the wrapped stream + private bool writeAllowed; + /// Whether to allow seeking within the wrapped stream + private bool seekAllowed; + /// Number of times the Flush() method has been called + private int flushCallCount; + + } + + #endregion // class TestStream + + /// + /// Tests whether the stream chainer correctly partitions a long write request + /// over its chained streams and appends any remaining data to the end of + /// the last chained stream. + /// + [Test] + public void TestPartitionedWrite() { + ChainStream chainer = chainTwoStreamsOfTenBytes(); + + byte[] testData = new byte[20]; + for(int index = 0; index < testData.Length; ++index) { + testData[index] = (byte)(index + 1); + } + + chainer.Position = 5; + chainer.Write(testData, 0, testData.Length); + + Assert.AreEqual( + new byte[] { 0, 0, 0, 0, 0, 1, 2, 3, 4, 5 }, + ((MemoryStream)chainer.ChainedStreams[0]).ToArray() + ); + Assert.AreEqual( + new byte[] { 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 }, + ((MemoryStream)chainer.ChainedStreams[1]).ToArray() + ); + } + + /// + /// Tests whether the stream chainer correctly partitions a long read request + /// over its chained streams. + /// + [Test] + public void TestPartitionedRead() { + ChainStream chainer = chainTwoStreamsOfTenBytes(); + + ((MemoryStream)chainer.ChainedStreams[0]).Write( + new byte[] { 1, 2, 3, 4, 5 }, 0, 5 + ); + ((MemoryStream)chainer.ChainedStreams[1]).Write( + new byte[] { 6, 7, 8, 9, 10 }, 0, 5 + ); + + chainer.Position = 3; + byte[] buffer = new byte[15]; + int bytesRead = chainer.Read(buffer, 0, 14); + + Assert.AreEqual(14, bytesRead); + Assert.AreEqual(new byte[] { 4, 5, 0, 0, 0, 0, 0, 6, 7, 8, 9, 10, 0, 0, 0 }, buffer); + } + + /// + /// Tests whether the stream chainer can handle a stream resize + /// + [Test] + public void TestWriteAfterResize() { + ChainStream chainer = chainTwoStreamsOfTenBytes(); + + // The first stream has a size of 10 bytes, so this goes into the second stream + chainer.Position = 11; + chainer.Write(new byte[] { 12, 34 }, 0, 2); + + // Now we resize the first stream to 15 bytes, so this goes into the first stream + ((MemoryStream)chainer.ChainedStreams[0]).SetLength(15); + chainer.Write(new byte[] { 56, 78, 11, 22 }, 0, 4); + + Assert.AreEqual( + new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 56, 78 }, + ((MemoryStream)chainer.ChainedStreams[0]).ToArray() + ); + Assert.AreEqual( + new byte[] { 11, 22, 34, 0, 0, 0, 0, 0, 0, 0 }, + ((MemoryStream)chainer.ChainedStreams[1]).ToArray() + ); + } + + /// + /// Tests writing to a stream chainer that contains an unseekable stream + /// + [Test] + public void TestWriteToUnseekableStream() { + MemoryStream firstStream = new MemoryStream(); + + // Now the second stream _does_ support seeking. If the stream chainer ignores + // that, it would overwrite data in the second stream. + MemoryStream secondStream = new MemoryStream(); + secondStream.Write(new byte[] { 0, 9, 8, 7, 6 }, 0, 5); + secondStream.Position = 0; + + TestStream testStream = new TestStream(firstStream, true, true, false); + ChainStream chainer = new ChainStream(new Stream[] { testStream, secondStream }); + + chainer.Write(new byte[] { 1, 2, 3, 4, 5 }, 0, 5); + Assert.IsFalse(chainer.CanSeek); + Assert.AreEqual(0, firstStream.Length); + Assert.AreEqual(new byte[] { 0, 9, 8, 7, 6, 1, 2, 3, 4, 5 }, secondStream.ToArray()); + } + + /// + /// Tests reading from a stream chainer that contains an unseekable stream + /// + [Test] + public void TestReadFromUnseekableStream() { + MemoryStream firstStream = new MemoryStream(); + + // Now the second stream _does_ support seeking. If the stream chainer ignores + // that, it would overwrite data in the second stream. + MemoryStream secondStream = new MemoryStream(); + secondStream.Write(new byte[] { 0, 9, 8, 7, 6 }, 0, 5); + secondStream.Position = 3; + + TestStream testStream = new TestStream(firstStream, true, true, false); + ChainStream chainer = new ChainStream(new Stream[] { testStream, secondStream }); + + Assert.IsFalse(chainer.CanSeek); + + byte[] buffer = new byte[5]; + int readByteCount = chainer.Read(buffer, 0, 3); + + Assert.AreEqual(3, readByteCount); + Assert.AreEqual(new byte[] { 0, 9, 8, 0, 0 }, buffer); + + readByteCount = chainer.Read(buffer, 0, 3); + + Assert.AreEqual(2, readByteCount); + Assert.AreEqual(new byte[] { 7, 6, 8, 0, 0 }, buffer); + } + + /// + /// Tests reading from a stream chainer that contains an unreadable stream + /// + [Test] + public void TestThrowOnReadFromUnreadableStream() { + MemoryStream memoryStream = new MemoryStream(); + TestStream testStream = new TestStream(memoryStream, false, true, true); + ChainStream chainer = new ChainStream(new Stream[] { testStream }); + Assert.Throws( + delegate() { chainer.Read(new byte[5], 0, 5); } + ); + } + + /// + /// Tests writing to a stream chainer that contains an unwriteable stream + /// + [Test] + public void TestThrowOnWriteToUnwriteableStream() { + MemoryStream memoryStream = new MemoryStream(); + TestStream testStream = new TestStream(memoryStream, true, false, true); + ChainStream chainer = new ChainStream(new Stream[] { testStream }); + Assert.Throws( + delegate() { chainer.Write(new byte[] { 1, 2, 3, 4, 5 }, 0, 5); } + ); + } + + /// + /// Verifies that the stream chainer throws an exception if the attempt is + /// made to change the length of the stream + /// + [Test] + public void TestThrowOnLengthChange() { + ChainStream chainer = chainTwoStreamsOfTenBytes(); + Assert.Throws( + delegate() { chainer.SetLength(123); } + ); + } + + /// + /// Verifies that the CanRead property is correctly determined by the stream chainer + /// + [Test] + public void TestCanRead() { + MemoryStream yesStream = new MemoryStream(); + TestStream noStream = new TestStream(yesStream, false, true, true); + + Stream[] yesGroup = new Stream[] { yesStream, yesStream, yesStream, yesStream }; + Stream[] partialGroup = new Stream[] { yesStream, yesStream, noStream, yesStream }; + Stream[] noGroup = new Stream[] { noStream, noStream, noStream, noStream }; + + Assert.IsTrue(new ChainStream(yesGroup).CanRead); + Assert.IsFalse(new ChainStream(partialGroup).CanRead); + Assert.IsFalse(new ChainStream(noGroup).CanRead); + } + + /// + /// Verifies that the CanRead property is correctly determined by the stream chainer + /// + [Test] + public void TestCanWrite() { + MemoryStream yesStream = new MemoryStream(); + TestStream noStream = new TestStream(yesStream, true, false, true); + + Stream[] yesGroup = new Stream[] { yesStream, yesStream, yesStream, yesStream }; + Stream[] partialGroup = new Stream[] { yesStream, yesStream, noStream, yesStream }; + Stream[] noGroup = new Stream[] { noStream, noStream, noStream, noStream }; + + Assert.IsTrue(new ChainStream(yesGroup).CanWrite); + Assert.IsFalse(new ChainStream(partialGroup).CanWrite); + Assert.IsFalse(new ChainStream(noGroup).CanWrite); + } + + /// + /// Verifies that the CanSeek property is correctly determined by the stream chainer + /// + [Test] + public void TestCanSeek() { + MemoryStream yesStream = new MemoryStream(); + TestStream noStream = new TestStream(yesStream, true, true, false); + + Stream[] yesGroup = new Stream[] { yesStream, yesStream, yesStream, yesStream }; + Stream[] partialGroup = new Stream[] { yesStream, yesStream, noStream, yesStream }; + Stream[] noGroup = new Stream[] { noStream, noStream, noStream, noStream }; + + Assert.IsTrue(new ChainStream(yesGroup).CanSeek); + Assert.IsFalse(new ChainStream(partialGroup).CanSeek); + Assert.IsFalse(new ChainStream(noGroup).CanSeek); + } + + /// + /// Tests whether an exception is thrown if the Seek() method is called on + /// a stream chainer with streams that do not support seeking + /// + [Test] + public void TestThrowOnSeekWithUnseekableStream() { + MemoryStream memoryStream = new MemoryStream(); + TestStream testStream = new TestStream(memoryStream, true, true, false); + + ChainStream chainer = new ChainStream(new Stream[] { testStream }); + Assert.Throws( + delegate() { chainer.Seek(123, SeekOrigin.Begin); } + ); + } + + /// + /// Tests whether an exception is thrown if the Position property is retrieved + /// on a stream chainer with streams that do not support seeking + /// + [Test] + public void TestThrowOnGetPositionWithUnseekableStream() { + MemoryStream memoryStream = new MemoryStream(); + TestStream testStream = new TestStream(memoryStream, true, true, false); + + ChainStream chainer = new ChainStream(new Stream[] { testStream }); + Assert.Throws( + delegate() { Console.WriteLine(chainer.Position); } + ); + } + + /// + /// Tests whether an exception is thrown if the Position property is set + /// on a stream chainer with streams that do not support seeking + /// + [Test] + public void TestThrowOnSetPositionWithUnseekableStream() { + MemoryStream memoryStream = new MemoryStream(); + TestStream testStream = new TestStream(memoryStream, true, true, false); + + ChainStream chainer = new ChainStream(new Stream[] { testStream }); + Assert.Throws( + delegate() { chainer.Position = 123; } + ); + } + + /// + /// Tests whether an exception is thrown if the Length property is retrieved + /// on a stream chainer with streams that do not support seeking + /// + [Test] + public void TestThrowOnGetLengthWithUnseekableStream() { + MemoryStream memoryStream = new MemoryStream(); + TestStream testStream = new TestStream(memoryStream, true, true, false); + + ChainStream chainer = new ChainStream(new Stream[] { testStream }); + Assert.Throws( + delegate() { Assert.IsTrue(chainer.Length != chainer.Length); } + ); + } + + /// + /// Tests whether the Seek() method of the stream chainer is working + /// + [Test] + public void TestSeeking() { + ChainStream chainer = chainTwoStreamsOfTenBytes(); + + Assert.AreEqual(7, chainer.Seek(-13, SeekOrigin.End)); + Assert.AreEqual(14, chainer.Seek(7, SeekOrigin.Current)); + Assert.AreEqual(11, chainer.Seek(11, SeekOrigin.Begin)); + } + + /// + /// Tests whether the stream behaves correctly if data is read from beyond its end + /// + [Test] + public void TestReadBeyondEndOfStream() { + ChainStream chainer = chainTwoStreamsOfTenBytes(); + chainer.Seek(10, SeekOrigin.End); + + // This is how the MemoryStream behaves: it returns 0 bytes. + int readByteCount = chainer.Read(new byte[1], 0, 1); + Assert.AreEqual(0, readByteCount); + } + + /// + /// Tests whether the Seek() method throws an exception if an invalid + /// reference point is provided + /// + [Test] + public void TestThrowOnInvalidSeekReferencePoint() { + ChainStream chainer = chainTwoStreamsOfTenBytes(); + Assert.Throws( + delegate() { chainer.Seek(1, (SeekOrigin)12345); } + ); + } + + /// Verifies that the position property works correctly + [Test] + public void TestPositionChange() { + ChainStream chainer = chainTwoStreamsOfTenBytes(); + + chainer.Position = 7; + Assert.AreEqual(chainer.Position, 7); + chainer.Position = 14; + Assert.AreEqual(chainer.Position, 14); + } + + /// Tests the Flush() method of the stream chainer + [Test] + public void TestFlush() { + MemoryStream firstStream = new MemoryStream(); + TestStream firstTestStream = new TestStream(firstStream, true, true, true); + + MemoryStream secondStream = new MemoryStream(); + TestStream secondTestStream = new TestStream(secondStream, true, true, true); + + ChainStream chainer = new ChainStream( + new Stream[] { firstTestStream, secondTestStream } + ); + + Assert.AreEqual(0, firstTestStream.FlushCallCount); + Assert.AreEqual(0, secondTestStream.FlushCallCount); + + chainer.Flush(); + + Assert.AreEqual(1, firstTestStream.FlushCallCount); + Assert.AreEqual(1, secondTestStream.FlushCallCount); + } + + /// + /// Creates a stream chainer with two streams that each have a size of 10 bytes + /// + /// The new stream chainer with two chained 10-byte streams + private static ChainStream chainTwoStreamsOfTenBytes() { + MemoryStream firstStream = new MemoryStream(10); + MemoryStream secondStream = new MemoryStream(10); + + firstStream.SetLength(10); + secondStream.SetLength(10); + + return new ChainStream( + new Stream[] { firstStream, secondStream } + ); + } + + } + +} // namespace Nuclex.Support.IO + +#endif // UNITTEST diff --git a/Source/IO/ChainStream.cs b/Source/IO/ChainStream.cs index f968b58..bc17b73 100644 --- a/Source/IO/ChainStream.cs +++ b/Source/IO/ChainStream.cs @@ -1,459 +1,458 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Diagnostics; -using System.IO; - -namespace Nuclex.Support.IO { - - /// Chains a series of independent streams into a single stream - /// - /// - /// This class can be used to chain multiple independent streams into a single - /// stream that acts as if its chained streams were only one combined stream. - /// It is useful to avoid creating huge memory streams or temporary files when - /// you just need to prepend or append some data to a stream or if you need to - /// read a file that was split into several parts as if it was a single file. - /// - /// - /// It is not recommended to change the size of any chained stream after it - /// has become part of a stream chainer, though the stream chainer will do its - /// best to cope with the changes as they occur. Increasing the length of a - /// chained stream is generally not an issue for streams that support seeking, - /// but reducing the length might invalidate the stream chainer's file pointer, - /// resulting in an IOException when Read() or Write() is next called. - /// - /// - public class ChainStream : Stream { - - /// Initializes a new stream chainer - /// Array of streams that will be chained together - public ChainStream(params Stream[] streams) { - this.streams = (Stream[])streams.Clone(); - - determineCapabilities(); - } - - /// Whether data can be read from the stream - public override bool CanRead { - get { return this.allStreamsCanRead; } - } - - /// Whether the stream supports seeking - public override bool CanSeek { - get { return this.allStreamsCanSeek; } - } - - /// Whether data can be written into the stream - public override bool CanWrite { - get { return this.allStreamsCanWrite; } - } - - /// - /// Clears all buffers for this stream and causes any buffered data to be written - /// to the underlying device. - /// - public override void Flush() { - for(int index = 0; index < this.streams.Length; ++index) { - this.streams[index].Flush(); - } - } - - /// Length of the stream in bytes - /// - /// At least one of the chained streams does not support seeking - /// - public override long Length { - get { - if(!this.allStreamsCanSeek) { - throw makeSeekNotSupportedException("determine length"); - } - - // Sum up the length of all chained streams - long length = 0; - for(int index = 0; index < this.streams.Length; ++index) { - length += this.streams[index].Length; - } - - return length; - } - } - - /// Absolute position of the file pointer within the stream - /// - /// At least one of the chained streams does not support seeking - /// - public override long Position { - get { - if(!this.allStreamsCanSeek) { - throw makeSeekNotSupportedException("seek"); - } - - return this.position; - } - set { moveFilePointer(value); } - } - - /// - /// Reads a sequence of bytes from the stream and advances the position of - /// the file pointer by the number of bytes read. - /// - /// Buffer that will receive the data read from the stream - /// - /// Offset in the buffer at which the stream will place the data read - /// - /// Maximum number of bytes that will be read - /// - /// The number of bytes that were actually read from the stream and written into - /// the provided buffer - /// - /// - /// The chained stream at the current position does not support reading - /// - public override int Read(byte[] buffer, int offset, int count) { - if(!this.allStreamsCanRead) { - throw new NotSupportedException( - "Can't read: at least one of the chained streams doesn't support reading" - ); - } - - int totalBytesRead = 0; - int lastStreamIndex = this.streams.Length - 1; - - if(this.allStreamsCanSeek) { - - // Find out from which stream and at which position we need to begin reading - int streamIndex; - long streamOffset; - findStreamIndexAndOffset(this.position, out streamIndex, out streamOffset); - - // Try to read from the stream our current file pointer falls into. If more - // data was requested than the stream contains, read each stream to its end - // until we either have enough data or run out of streams. - while(count > 0) { - Stream currentStream = this.streams[streamIndex]; - - // Read up to count bytes from the current stream. Count is decreased each - // time we successfully get data and holds the number of bytes remaining - // to be read - long maximumBytes = Math.Min(count, currentStream.Length - streamOffset); - currentStream.Position = streamOffset; - int bytesRead = currentStream.Read(buffer, offset, (int)maximumBytes); - - // Accumulate the total number of bytes we read for the return value - totalBytesRead += bytesRead; - - // If the stream returned partial data, stop here. Also, if this was the - // last stream we queried, this is as far as we can go. - if((bytesRead < maximumBytes) || (streamIndex == lastStreamIndex)) { - break; - } - - // Move on to the next stream in the chain - ++streamIndex; - streamOffset = 0; - count -= bytesRead; - offset += bytesRead; - } - - this.position += totalBytesRead; - - } else { - - // Try to read from the active read stream. If the end of the active read - // stream is reached, switch to the next stream in the chain until we have - // no more streams left to read from - while(this.activeReadStreamIndex <= lastStreamIndex) { - - // Try to read from the stream. The stream can either return any amount - // of data > 0 if there's still data left ot be read or 0 if the end of - // the stream was reached - Stream activeStream = this.streams[this.activeReadStreamIndex]; - if(activeStream.CanSeek) { - activeStream.Position = this.activeReadStreamPosition; - } - totalBytesRead = activeStream.Read(buffer, offset, count); - - // If we got any data, we're done, exit the loop - if(totalBytesRead != 0) { - break; - } else { // Otherwise, go to the next stream in the chain - this.activeReadStreamPosition = 0; - ++this.activeReadStreamIndex; - } - } - - this.activeReadStreamPosition += totalBytesRead; - - } - - return totalBytesRead; - } - - /// Changes the position of the file pointer - /// - /// Offset to move the file pointer by, relative to the position indicated by - /// the parameter. - /// - /// - /// Reference point relative to which the file pointer is placed - /// - /// The new absolute position within the stream - public override long Seek(long offset, SeekOrigin origin) { - switch(origin) { - case SeekOrigin.Begin: { - return Position = offset; - } - case SeekOrigin.Current: { - return Position += offset; - } - case SeekOrigin.End: { - return Position = (Length + offset); - } - default: { - throw new ArgumentException("Invalid seek origin", "origin"); - } - } - } - - /// Changes the length of the stream - /// New length the stream shall have - /// - /// Always, the stream chainer does not support the SetLength() operation - /// - public override void SetLength(long value) { - throw new NotSupportedException("Resizing chained streams is not supported"); - } - - /// - /// Writes a sequence of bytes to the stream and advances the position of - /// the file pointer by the number of bytes written. - /// - /// - /// Buffer containing the data that will be written to the stream - /// - /// - /// Offset in the buffer at which the data to be written starts - /// - /// Number of bytes that will be written into the stream - /// - /// The behavior of this method is as follows: If one or more chained streams - /// do not support seeking, all data is appended to the final stream in the - /// chain. Otherwise, writing will begin with the stream the current file pointer - /// offset falls into. If the end of that stream is reached, writing continues - /// in the next stream. On the last stream, writing more data into the stream - /// that it current size allows will enlarge the stream. - /// - public override void Write(byte[] buffer, int offset, int count) { - if(!this.allStreamsCanWrite) { - throw new NotSupportedException( - "Can't write: at least one of the chained streams doesn't support writing" - ); - } - - int remaining = count; - - // If seeking is supported, we can write into the mid of the stream, - // if the user so desires - if(this.allStreamsCanSeek) { - - // Find out in which stream and at which position we need to begin writing - int streamIndex; - long streamOffset; - findStreamIndexAndOffset(this.position, out streamIndex, out streamOffset); - - // Write data into the streams, switching over to the next stream if data is - // too large to fit into the current stream, until all data is spent. - int lastStreamIndex = this.streams.Length - 1; - while(remaining > 0) { - Stream currentStream = this.streams[streamIndex]; - - // If this is the last stream, just write. If the data is larger than the last - // stream's remaining bytes, it will append to that stream, enlarging it. - if(streamIndex == lastStreamIndex) { - - // Write all remaining data into the last stream - currentStream.Position = streamOffset; - currentStream.Write(buffer, offset, remaining); - remaining = 0; - - } else { // We're writing into a stream that's followed by another stream - - // Find out how much data we can put into the current stream without - // enlarging it (if seeking is supported, so is the Length property) - long currentStreamRemaining = currentStream.Length - streamOffset; - int bytesToWrite = (int)Math.Min((long)remaining, currentStreamRemaining); - - // Write all data that can fit into the current stream - currentStream.Position = streamOffset; - currentStream.Write(buffer, offset, bytesToWrite); - - // Adjust the offsets and count for the next stream - offset += bytesToWrite; - remaining -= bytesToWrite; - streamOffset = 0; - ++streamIndex; - - } - } - - } else { // Seeking not supported, append everything to the last stream - Stream lastStream = this.streams[this.streams.Length - 1]; - if(lastStream.CanSeek) { - lastStream.Seek(0, SeekOrigin.End); - } - lastStream.Write(buffer, offset, remaining); - } - - this.position += count; - } - - /// Streams being combined by the stream chainer - public Stream[] ChainedStreams { - get { return this.streams; } - } - - /// Moves the file pointer - /// New position the file pointer will be moved to - private void moveFilePointer(long position) { - if(!this.allStreamsCanSeek) { - throw makeSeekNotSupportedException("seek"); - } - - // Seemingly, it is okay to move the file pointer beyond the end of - // the stream until you try to Read() or Write() - this.position = position; - } - - /// - /// Finds the stream index and local offset for an absolute position within - /// the combined streams. - /// - /// Absolute position within the combined streams - /// - /// Index of the stream the overall position falls into - /// - /// - /// Local position within the stream indicated by - /// - private void findStreamIndexAndOffset( - long overallPosition, out int streamIndex, out long streamPosition - ) { - Debug.Assert( - this.allStreamsCanSeek, "Call to findStreamIndexAndOffset() but no seek support" - ); - - // In case the position is beyond the stream's end, this is what we will - // return to the caller - streamIndex = (this.streams.Length - 1); - - // Search until we have found the stream the position must lie in - for(int index = 0; index < this.streams.Length; ++index) { - long streamLength = this.streams[index].Length; - - if(overallPosition < streamLength) { - streamIndex = index; - break; - } - - overallPosition -= streamLength; - } - - // The overall position will have been decreased by each skipped stream's length, - // so it should now contain the local position for the final stream we checked. - streamPosition = overallPosition; - } - - /// Determines the capabilities of the chained streams - /// - /// - /// Theoretically, it would be possible to create a stream chainer that supported - /// writing only when the file pointer was on a chained stream with write support, - /// that could seek within the beginning of the stream until the first chained - /// stream with no seek capability was encountered and so on. - /// - /// - /// However, the interface of the Stream class requires us to make a definitive - /// statement as to whether the Stream supports seeking, reading and writing. - /// We can't return "maybe" or "mostly" in CanSeek, so the only sane choice that - /// doesn't violate the Stream interface is to implement these capabilities as - /// all or nothing - either all streams support a feature, or the stream chainer - /// will report the feature as unsupported. - /// - /// - private void determineCapabilities() { - this.allStreamsCanSeek = true; - this.allStreamsCanRead = true; - this.allStreamsCanWrite = true; - - for(int index = 0; index < this.streams.Length; ++index) { - this.allStreamsCanSeek &= this.streams[index].CanSeek; - this.allStreamsCanRead &= this.streams[index].CanRead; - this.allStreamsCanWrite &= this.streams[index].CanWrite; - } - } - - /// - /// Constructs a NotSupportException for an error caused by one of the chained - /// streams having no seek support - /// - /// Action that was tried to perform - /// The newly constructed NotSupportedException - private static NotSupportedException makeSeekNotSupportedException(string action) { - return new NotSupportedException( - string.Format( - "Can't {0}: at least one of the chained streams does not support seeking", - action - ) - ); - } - - /// Streams that have been chained together - private Stream[] streams; - /// Current position of the overall file pointer - private long position; - /// Stream we're currently reading from if seeking is not supported - /// - /// If seeking is not supported, the stream chainer will read from each stream - /// until the end was reached - /// sequentially - /// - private int activeReadStreamIndex; - /// Position in the current read stream if seeking is not supported - /// - /// If there is a mix of streams supporting seeking and not supporting seeking, we - /// need to keep track of the read index for those streams that do. If, for example, - /// the last stream is written to and read from in succession, the file pointer - /// of that stream would have been moved to the end by the write attempt, skipping - /// data that should have been read in the following read attempt. - /// - private long activeReadStreamPosition; - - /// Whether all of the chained streams support seeking - private bool allStreamsCanSeek; - /// Whether all of the chained streams support reading - private bool allStreamsCanRead; - /// Whether all of the chained streams support writing - private bool allStreamsCanWrite; - - } - -} // namespace Nuclex.Support.IO +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Diagnostics; +using System.IO; + +namespace Nuclex.Support.IO { + + /// Chains a series of independent streams into a single stream + /// + /// + /// This class can be used to chain multiple independent streams into a single + /// stream that acts as if its chained streams were only one combined stream. + /// It is useful to avoid creating huge memory streams or temporary files when + /// you just need to prepend or append some data to a stream or if you need to + /// read a file that was split into several parts as if it was a single file. + /// + /// + /// It is not recommended to change the size of any chained stream after it + /// has become part of a stream chainer, though the stream chainer will do its + /// best to cope with the changes as they occur. Increasing the length of a + /// chained stream is generally not an issue for streams that support seeking, + /// but reducing the length might invalidate the stream chainer's file pointer, + /// resulting in an IOException when Read() or Write() is next called. + /// + /// + public class ChainStream : Stream { + + /// Initializes a new stream chainer + /// Array of streams that will be chained together + public ChainStream(params Stream[] streams) { + this.streams = (Stream[])streams.Clone(); + + determineCapabilities(); + } + + /// Whether data can be read from the stream + public override bool CanRead { + get { return this.allStreamsCanRead; } + } + + /// Whether the stream supports seeking + public override bool CanSeek { + get { return this.allStreamsCanSeek; } + } + + /// Whether data can be written into the stream + public override bool CanWrite { + get { return this.allStreamsCanWrite; } + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + public override void Flush() { + for(int index = 0; index < this.streams.Length; ++index) { + this.streams[index].Flush(); + } + } + + /// Length of the stream in bytes + /// + /// At least one of the chained streams does not support seeking + /// + public override long Length { + get { + if(!this.allStreamsCanSeek) { + throw makeSeekNotSupportedException("determine length"); + } + + // Sum up the length of all chained streams + long length = 0; + for(int index = 0; index < this.streams.Length; ++index) { + length += this.streams[index].Length; + } + + return length; + } + } + + /// Absolute position of the file pointer within the stream + /// + /// At least one of the chained streams does not support seeking + /// + public override long Position { + get { + if(!this.allStreamsCanSeek) { + throw makeSeekNotSupportedException("seek"); + } + + return this.position; + } + set { moveFilePointer(value); } + } + + /// + /// Reads a sequence of bytes from the stream and advances the position of + /// the file pointer by the number of bytes read. + /// + /// Buffer that will receive the data read from the stream + /// + /// Offset in the buffer at which the stream will place the data read + /// + /// Maximum number of bytes that will be read + /// + /// The number of bytes that were actually read from the stream and written into + /// the provided buffer + /// + /// + /// The chained stream at the current position does not support reading + /// + public override int Read(byte[] buffer, int offset, int count) { + if(!this.allStreamsCanRead) { + throw new NotSupportedException( + "Can't read: at least one of the chained streams doesn't support reading" + ); + } + + int totalBytesRead = 0; + int lastStreamIndex = this.streams.Length - 1; + + if(this.allStreamsCanSeek) { + + // Find out from which stream and at which position we need to begin reading + int streamIndex; + long streamOffset; + findStreamIndexAndOffset(this.position, out streamIndex, out streamOffset); + + // Try to read from the stream our current file pointer falls into. If more + // data was requested than the stream contains, read each stream to its end + // until we either have enough data or run out of streams. + while(count > 0) { + Stream currentStream = this.streams[streamIndex]; + + // Read up to count bytes from the current stream. Count is decreased each + // time we successfully get data and holds the number of bytes remaining + // to be read + long maximumBytes = Math.Min(count, currentStream.Length - streamOffset); + currentStream.Position = streamOffset; + int bytesRead = currentStream.Read(buffer, offset, (int)maximumBytes); + + // Accumulate the total number of bytes we read for the return value + totalBytesRead += bytesRead; + + // If the stream returned partial data, stop here. Also, if this was the + // last stream we queried, this is as far as we can go. + if((bytesRead < maximumBytes) || (streamIndex == lastStreamIndex)) { + break; + } + + // Move on to the next stream in the chain + ++streamIndex; + streamOffset = 0; + count -= bytesRead; + offset += bytesRead; + } + + this.position += totalBytesRead; + + } else { + + // Try to read from the active read stream. If the end of the active read + // stream is reached, switch to the next stream in the chain until we have + // no more streams left to read from + while(this.activeReadStreamIndex <= lastStreamIndex) { + + // Try to read from the stream. The stream can either return any amount + // of data > 0 if there's still data left ot be read or 0 if the end of + // the stream was reached + Stream activeStream = this.streams[this.activeReadStreamIndex]; + if(activeStream.CanSeek) { + activeStream.Position = this.activeReadStreamPosition; + } + totalBytesRead = activeStream.Read(buffer, offset, count); + + // If we got any data, we're done, exit the loop + if(totalBytesRead != 0) { + break; + } else { // Otherwise, go to the next stream in the chain + this.activeReadStreamPosition = 0; + ++this.activeReadStreamIndex; + } + } + + this.activeReadStreamPosition += totalBytesRead; + + } + + return totalBytesRead; + } + + /// Changes the position of the file pointer + /// + /// Offset to move the file pointer by, relative to the position indicated by + /// the parameter. + /// + /// + /// Reference point relative to which the file pointer is placed + /// + /// The new absolute position within the stream + public override long Seek(long offset, SeekOrigin origin) { + switch(origin) { + case SeekOrigin.Begin: { + return Position = offset; + } + case SeekOrigin.Current: { + return Position += offset; + } + case SeekOrigin.End: { + return Position = (Length + offset); + } + default: { + throw new ArgumentException("Invalid seek origin", "origin"); + } + } + } + + /// Changes the length of the stream + /// New length the stream shall have + /// + /// Always, the stream chainer does not support the SetLength() operation + /// + public override void SetLength(long value) { + throw new NotSupportedException("Resizing chained streams is not supported"); + } + + /// + /// Writes a sequence of bytes to the stream and advances the position of + /// the file pointer by the number of bytes written. + /// + /// + /// Buffer containing the data that will be written to the stream + /// + /// + /// Offset in the buffer at which the data to be written starts + /// + /// Number of bytes that will be written into the stream + /// + /// The behavior of this method is as follows: If one or more chained streams + /// do not support seeking, all data is appended to the final stream in the + /// chain. Otherwise, writing will begin with the stream the current file pointer + /// offset falls into. If the end of that stream is reached, writing continues + /// in the next stream. On the last stream, writing more data into the stream + /// that it current size allows will enlarge the stream. + /// + public override void Write(byte[] buffer, int offset, int count) { + if(!this.allStreamsCanWrite) { + throw new NotSupportedException( + "Can't write: at least one of the chained streams doesn't support writing" + ); + } + + int remaining = count; + + // If seeking is supported, we can write into the mid of the stream, + // if the user so desires + if(this.allStreamsCanSeek) { + + // Find out in which stream and at which position we need to begin writing + int streamIndex; + long streamOffset; + findStreamIndexAndOffset(this.position, out streamIndex, out streamOffset); + + // Write data into the streams, switching over to the next stream if data is + // too large to fit into the current stream, until all data is spent. + int lastStreamIndex = this.streams.Length - 1; + while(remaining > 0) { + Stream currentStream = this.streams[streamIndex]; + + // If this is the last stream, just write. If the data is larger than the last + // stream's remaining bytes, it will append to that stream, enlarging it. + if(streamIndex == lastStreamIndex) { + + // Write all remaining data into the last stream + currentStream.Position = streamOffset; + currentStream.Write(buffer, offset, remaining); + remaining = 0; + + } else { // We're writing into a stream that's followed by another stream + + // Find out how much data we can put into the current stream without + // enlarging it (if seeking is supported, so is the Length property) + long currentStreamRemaining = currentStream.Length - streamOffset; + int bytesToWrite = (int)Math.Min((long)remaining, currentStreamRemaining); + + // Write all data that can fit into the current stream + currentStream.Position = streamOffset; + currentStream.Write(buffer, offset, bytesToWrite); + + // Adjust the offsets and count for the next stream + offset += bytesToWrite; + remaining -= bytesToWrite; + streamOffset = 0; + ++streamIndex; + + } + } + + } else { // Seeking not supported, append everything to the last stream + Stream lastStream = this.streams[this.streams.Length - 1]; + if(lastStream.CanSeek) { + lastStream.Seek(0, SeekOrigin.End); + } + lastStream.Write(buffer, offset, remaining); + } + + this.position += count; + } + + /// Streams being combined by the stream chainer + public Stream[] ChainedStreams { + get { return this.streams; } + } + + /// Moves the file pointer + /// New position the file pointer will be moved to + private void moveFilePointer(long position) { + if(!this.allStreamsCanSeek) { + throw makeSeekNotSupportedException("seek"); + } + + // Seemingly, it is okay to move the file pointer beyond the end of + // the stream until you try to Read() or Write() + this.position = position; + } + + /// + /// Finds the stream index and local offset for an absolute position within + /// the combined streams. + /// + /// Absolute position within the combined streams + /// + /// Index of the stream the overall position falls into + /// + /// + /// Local position within the stream indicated by + /// + private void findStreamIndexAndOffset( + long overallPosition, out int streamIndex, out long streamPosition + ) { + Debug.Assert( + this.allStreamsCanSeek, "Call to findStreamIndexAndOffset() but no seek support" + ); + + // In case the position is beyond the stream's end, this is what we will + // return to the caller + streamIndex = (this.streams.Length - 1); + + // Search until we have found the stream the position must lie in + for(int index = 0; index < this.streams.Length; ++index) { + long streamLength = this.streams[index].Length; + + if(overallPosition < streamLength) { + streamIndex = index; + break; + } + + overallPosition -= streamLength; + } + + // The overall position will have been decreased by each skipped stream's length, + // so it should now contain the local position for the final stream we checked. + streamPosition = overallPosition; + } + + /// Determines the capabilities of the chained streams + /// + /// + /// Theoretically, it would be possible to create a stream chainer that supported + /// writing only when the file pointer was on a chained stream with write support, + /// that could seek within the beginning of the stream until the first chained + /// stream with no seek capability was encountered and so on. + /// + /// + /// However, the interface of the Stream class requires us to make a definitive + /// statement as to whether the Stream supports seeking, reading and writing. + /// We can't return "maybe" or "mostly" in CanSeek, so the only sane choice that + /// doesn't violate the Stream interface is to implement these capabilities as + /// all or nothing - either all streams support a feature, or the stream chainer + /// will report the feature as unsupported. + /// + /// + private void determineCapabilities() { + this.allStreamsCanSeek = true; + this.allStreamsCanRead = true; + this.allStreamsCanWrite = true; + + for(int index = 0; index < this.streams.Length; ++index) { + this.allStreamsCanSeek &= this.streams[index].CanSeek; + this.allStreamsCanRead &= this.streams[index].CanRead; + this.allStreamsCanWrite &= this.streams[index].CanWrite; + } + } + + /// + /// Constructs a NotSupportException for an error caused by one of the chained + /// streams having no seek support + /// + /// Action that was tried to perform + /// The newly constructed NotSupportedException + private static NotSupportedException makeSeekNotSupportedException(string action) { + return new NotSupportedException( + string.Format( + "Can't {0}: at least one of the chained streams does not support seeking", + action + ) + ); + } + + /// Streams that have been chained together + private Stream[] streams; + /// Current position of the overall file pointer + private long position; + /// Stream we're currently reading from if seeking is not supported + /// + /// If seeking is not supported, the stream chainer will read from each stream + /// until the end was reached + /// sequentially + /// + private int activeReadStreamIndex; + /// Position in the current read stream if seeking is not supported + /// + /// If there is a mix of streams supporting seeking and not supporting seeking, we + /// need to keep track of the read index for those streams that do. If, for example, + /// the last stream is written to and read from in succession, the file pointer + /// of that stream would have been moved to the end by the write attempt, skipping + /// data that should have been read in the following read attempt. + /// + private long activeReadStreamPosition; + + /// Whether all of the chained streams support seeking + private bool allStreamsCanSeek; + /// Whether all of the chained streams support reading + private bool allStreamsCanRead; + /// Whether all of the chained streams support writing + private bool allStreamsCanWrite; + + } + +} // namespace Nuclex.Support.IO diff --git a/Source/IO/PartialStream.Test.cs b/Source/IO/PartialStream.Test.cs index f6cf2dd..b928e7b 100644 --- a/Source/IO/PartialStream.Test.cs +++ b/Source/IO/PartialStream.Test.cs @@ -1,527 +1,526 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.IO; - -using NUnit.Framework; - -namespace Nuclex.Support.IO { - - /// Unit Test for the partial stream - [TestFixture] - internal class PartialStreamTest { - - #region class TestStream - - /// Testing stream that allows specific features to be disabled - private class TestStream : Stream { - - /// Initializes a new test stream - /// Stream that will be wrapped - /// Whether to allow reading from the stream - /// Whether to allow writing to the stream - /// Whether to allow seeking within the stream - public TestStream( - Stream wrappedStream, bool allowRead, bool allowWrite, bool allowSeek - ) { - this.stream = wrappedStream; - this.readAllowed = allowRead; - this.writeAllowed = allowWrite; - this.seekAllowed = allowSeek; - } - - /// Whether data can be read from the stream - public override bool CanRead { - get { return this.readAllowed; } - } - - /// Whether the stream supports seeking - public override bool CanSeek { - get { return this.seekAllowed; } - } - - /// Whether data can be written into the stream - public override bool CanWrite { - get { return this.writeAllowed; } - } - - /// - /// Clears all buffers for this stream and causes any buffered data to be written - /// to the underlying device. - /// - public override void Flush() { - ++this.flushCallCount; - this.stream.Flush(); - } - - /// Length of the stream in bytes - public override long Length { - get { - enforceSeekAllowed(); - return this.stream.Length; - } - } - - /// Absolute position of the file pointer within the stream - /// - /// At least one of the chained streams does not support seeking - /// - public override long Position { - get { - enforceSeekAllowed(); - return this.stream.Position; - } - set { - enforceSeekAllowed(); - this.stream.Position = value; - } - } - - /// - /// Reads a sequence of bytes from the stream and advances the position of - /// the file pointer by the number of bytes read. - /// - /// Buffer that will receive the data read from the stream - /// - /// Offset in the buffer at which the stream will place the data read - /// - /// Maximum number of bytes that will be read - /// - /// The number of bytes that were actually read from the stream and written into - /// the provided buffer - /// - public override int Read(byte[] buffer, int offset, int count) { - enforceReadAllowed(); - return this.stream.Read(buffer, offset, count); - } - - /// Changes the position of the file pointer - /// - /// Offset to move the file pointer by, relative to the position indicated by - /// the parameter. - /// - /// - /// Reference point relative to which the file pointer is placed - /// - /// The new absolute position within the stream - public override long Seek(long offset, SeekOrigin origin) { - enforceSeekAllowed(); - return this.stream.Seek(offset, origin); - } - - /// Changes the length of the stream - /// New length the stream shall have - public override void SetLength(long value) { - enforceSeekAllowed(); - this.stream.SetLength(value); - } - - /// - /// Writes a sequence of bytes to the stream and advances the position of - /// the file pointer by the number of bytes written. - /// - /// - /// Buffer containing the data that will be written to the stream - /// - /// - /// Offset in the buffer at which the data to be written starts - /// - /// Number of bytes that will be written into the stream - public override void Write(byte[] buffer, int offset, int count) { - enforceWriteAllowed(); - this.stream.Write(buffer, offset, count); - } - - /// Number of times the Flush() method has been called - public int FlushCallCount { - get { return this.flushCallCount; } - } - - /// Throws an exception if reading is not allowed - private void enforceReadAllowed() { - if(!this.readAllowed) { - throw new NotSupportedException("Reading has been disabled"); - } - } - - /// Throws an exception if writing is not allowed - private void enforceWriteAllowed() { - if(!this.writeAllowed) { - throw new NotSupportedException("Writing has been disabled"); - } - } - - /// Throws an exception if seeking is not allowed - private void enforceSeekAllowed() { - if(!this.seekAllowed) { - throw new NotSupportedException("Seeking has been disabled"); - } - } - - /// Stream being wrapped for testing - private Stream stream; - /// whether to allow reading from the wrapped stream - private bool readAllowed; - /// Whether to allow writing to the wrapped stream - private bool writeAllowed; - /// Whether to allow seeking within the wrapped stream - private bool seekAllowed; - /// Number of times the Flush() method has been called - private int flushCallCount; - - } - - #endregion // class TestStream - - /// Tests whether the partial stream constructor is working - [Test] - public void TestConstructor() { - using(MemoryStream memoryStream = new MemoryStream()) { - memoryStream.SetLength(123); - PartialStream partialStream = new PartialStream(memoryStream, 23, 100); - Assert.AreEqual(100, partialStream.Length); - } - } - - /// - /// Verifies that the partial stream constructor throws an exception if - /// it's invoked with an invalid start offset - /// - [Test] - public void TestThrowOnInvalidStartInConstructor() { - using(MemoryStream memoryStream = new MemoryStream()) { - memoryStream.SetLength(123); - Assert.Throws( - delegate() { Console.WriteLine(new PartialStream(memoryStream, -1, 10)); } - ); - } - } - - /// - /// Verifies that the partial stream constructor throws an exception if - /// it's invoked with an invalid start offset - /// - [Test] - public void TestThrowOnInvalidLengthInConstructor() { - using(MemoryStream memoryStream = new MemoryStream()) { - memoryStream.SetLength(123); - Assert.Throws( - delegate() { Console.WriteLine(new PartialStream(memoryStream, 100, 24)); } - ); - } - } - - /// - /// Verifies that the partial stream constructor throws an exception if - /// it's invoked with a start offset on an unseekable stream - /// - [Test] - public void TestThrowOnUnseekableStreamWithOffsetInConstructor() { - using(MemoryStream memoryStream = new MemoryStream()) { - memoryStream.SetLength(123); - TestStream testStream = new TestStream(memoryStream, true, true, false); - Assert.Throws( - delegate() { Console.WriteLine(new PartialStream(testStream, 23, 100)); } - ); - } - } - - /// - /// Tests whether the CanRead property reports its status correctly - /// - [Test] - public void TestCanReadProperty() { - using(MemoryStream memoryStream = new MemoryStream()) { - TestStream yesStream = new TestStream(memoryStream, true, true, true); - TestStream noStream = new TestStream(memoryStream, false, true, true); - - Assert.IsTrue(new PartialStream(yesStream, 0, 0).CanRead); - Assert.IsFalse(new PartialStream(noStream, 0, 0).CanRead); - } - } - - /// - /// Tests whether the CanWrite property reports its status correctly - /// - [Test] - public void TestCanWriteProperty() { - using(MemoryStream memoryStream = new MemoryStream()) { - TestStream yesStream = new TestStream(memoryStream, true, true, true); - TestStream noStream = new TestStream(memoryStream, true, false, true); - - Assert.IsTrue(new PartialStream(yesStream, 0, 0).CanWrite); - Assert.IsFalse(new PartialStream(noStream, 0, 0).CanWrite); - } - } - - /// - /// Tests whether the CanSeek property reports its status correctly - /// - [Test] - public void TestCanSeekProperty() { - using(MemoryStream memoryStream = new MemoryStream()) { - TestStream yesStream = new TestStream(memoryStream, true, true, true); - TestStream noStream = new TestStream(memoryStream, true, true, false); - - Assert.IsTrue(new PartialStream(yesStream, 0, 0).CanSeek); - Assert.IsFalse(new PartialStream(noStream, 0, 0).CanSeek); - } - } - - /// - /// Tests whether the CompleteStream property returns the original stream - /// - [Test] - public void TestCompleteStreamProperty() { - using(MemoryStream memoryStream = new MemoryStream()) { - PartialStream partialStream = new PartialStream(memoryStream, 0, 0); - Assert.AreSame(memoryStream, partialStream.CompleteStream); - } - } - - /// Tests whether the Flush() method can be called - [Test] - public void TestFlush() { - using(MemoryStream memoryStream = new MemoryStream()) { - PartialStream partialStream = new PartialStream(memoryStream, 0, 0); - partialStream.Flush(); - } - } - - /// - /// Tests whether the Position property correctly reports the file pointer position - /// - [Test] - public void TestGetPosition() { - using(MemoryStream memoryStream = new MemoryStream()) { - memoryStream.SetLength(123); - PartialStream partialStream = new PartialStream(memoryStream, 23, 100); - - Assert.AreEqual(0, partialStream.Position); - - byte[] test = new byte[10]; - int bytesRead = partialStream.Read(test, 0, 10); - - Assert.AreEqual(10, bytesRead); - Assert.AreEqual(10, partialStream.Position); - - bytesRead = partialStream.Read(test, 0, 10); - - Assert.AreEqual(10, bytesRead); - Assert.AreEqual(20, partialStream.Position); - } - } - - /// - /// Tests whether the Position property is correctly updated - /// - [Test] - public void TestSetPosition() { - using(MemoryStream memoryStream = new MemoryStream()) { - memoryStream.SetLength(123); - PartialStream partialStream = new PartialStream(memoryStream, 23, 100); - - Assert.AreEqual(0, partialStream.Position); - partialStream.Position = 7; - Assert.AreEqual(partialStream.Position, 7); - partialStream.Position = 14; - Assert.AreEqual(partialStream.Position, 14); - } - } - - /// - /// Tests whether the Position property throws an exception if the stream does - /// not support seeking. - /// - [Test] - public void TestThrowOnGetPositionOnUnseekableStream() { - using(MemoryStream memoryStream = new MemoryStream()) { - TestStream testStream = new TestStream(memoryStream, true, true, false); - - PartialStream partialStream = new PartialStream(testStream, 0, 0); - Assert.Throws( - delegate() { Console.WriteLine(partialStream.Position); } - ); - } - } - - /// - /// Tests whether the Position property throws an exception if the stream does - /// not support seeking. - /// - [Test] - public void TestThrowOnSetPositionOnUnseekableStream() { - using(MemoryStream memoryStream = new MemoryStream()) { - TestStream testStream = new TestStream(memoryStream, true, true, false); - - PartialStream partialStream = new PartialStream(testStream, 0, 0); - Assert.Throws( - delegate() { partialStream.Position = 0; } - ); - } - } - - /// - /// Tests whether the Read() method throws an exception if the stream does - /// not support reading - /// - [Test] - public void TestThrowOnReadFromUnreadableStream() { - using(MemoryStream memoryStream = new MemoryStream()) { - TestStream testStream = new TestStream(memoryStream, false, true, true); - PartialStream partialStream = new PartialStream(testStream, 0, 0); - - byte[] test = new byte[10]; - Assert.Throws( - delegate() { Console.WriteLine(partialStream.Read(test, 0, 10)); } - ); - } - } - - /// - /// Tests whether the Seek() method of the partial stream is working - /// - [Test] - public void TestSeeking() { - using(MemoryStream memoryStream = new MemoryStream()) { - memoryStream.SetLength(20); - PartialStream partialStream = new PartialStream(memoryStream, 0, 20); - - Assert.AreEqual(7, partialStream.Seek(-13, SeekOrigin.End)); - Assert.AreEqual(14, partialStream.Seek(7, SeekOrigin.Current)); - Assert.AreEqual(11, partialStream.Seek(11, SeekOrigin.Begin)); - } - } - - /// - /// Tests whether the Seek() method throws an exception if an invalid - /// reference point is provided - /// - [Test] - public void TestThrowOnInvalidSeekReferencePoint() { - using(MemoryStream memoryStream = new MemoryStream()) { - PartialStream partialStream = new PartialStream(memoryStream, 0, 0); - Assert.Throws( - delegate() { partialStream.Seek(1, (SeekOrigin)12345); } - ); - } - } - - /// - /// Verifies that the partial stream throws an exception if the attempt is - /// made to change the length of the stream - /// - [Test] - public void TestThrowOnLengthChange() { - using(MemoryStream memoryStream = new MemoryStream()) { - PartialStream partialStream = new PartialStream(memoryStream, 0, 0); - Assert.Throws( - delegate() { partialStream.SetLength(123); } - ); - } - } - - /// - /// Tests whether the Read() method returns 0 bytes if the attempt is made - /// to read data from an invalid position - /// - [Test] - public void TestReadFromInvalidPosition() { - using(MemoryStream memoryStream = new MemoryStream()) { - memoryStream.SetLength(123); - - memoryStream.Position = 1123; - byte[] test = new byte[10]; - - Assert.AreEqual(0, memoryStream.Read(test, 0, 10)); - } - } - - /// Verifies that the Read() method is working - [Test] - public void TestReadFromPartialStream() { - using(MemoryStream memoryStream = new MemoryStream()) { - memoryStream.SetLength(123); - - memoryStream.Position = 100; - memoryStream.Write(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, 0, 10); - - PartialStream partialStream = new PartialStream(memoryStream, 95, 10); - - byte[] buffer = new byte[15]; - int bytesRead = partialStream.Read(buffer, 0, 15); - - Assert.AreEqual(10, bytesRead); - Assert.AreEqual( - new byte[] { 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 0, 0, 0, 0, 0 }, buffer - ); - } - } - - /// Verifies that the Write() method is working - [Test] - public void TestWriteToPartialStream() { - using(MemoryStream memoryStream = new MemoryStream()) { - memoryStream.SetLength(123); - - memoryStream.Position = 60; - memoryStream.Write(new byte[] { 11, 12, 13, 14, 15 }, 0, 5); - - PartialStream partialStream = new PartialStream(memoryStream, 50, 15); - partialStream.Position = 3; - partialStream.Write(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, 0, 10); - - byte[] buffer = new byte[17]; - memoryStream.Position = 49; - int bytesRead = memoryStream.Read(buffer, 0, 17); - - Assert.AreEqual(17, bytesRead); - Assert.AreEqual( - new byte[] { 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 14, 15, 0 }, - buffer - ); - } - } - - /// - /// Verifies that an exception is thrown if the Write() method of the partial stream - /// is attempted to be used to extend the partial stream's length - /// - [Test] - public void TestThrowOnExtendPartialStream() { - using(MemoryStream memoryStream = new MemoryStream()) { - memoryStream.SetLength(25); - - PartialStream partialStream = new PartialStream(memoryStream, 10, 10); - partialStream.Position = 5; - Assert.Throws( - delegate() { partialStream.Write(new byte[] { 1, 2, 3, 4, 5, 6 }, 0, 6); } - ); - } - } - - } - -} // namespace Nuclex.Support.IO - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.IO; + +using NUnit.Framework; + +namespace Nuclex.Support.IO { + + /// Unit Test for the partial stream + [TestFixture] + internal class PartialStreamTest { + + #region class TestStream + + /// Testing stream that allows specific features to be disabled + private class TestStream : Stream { + + /// Initializes a new test stream + /// Stream that will be wrapped + /// Whether to allow reading from the stream + /// Whether to allow writing to the stream + /// Whether to allow seeking within the stream + public TestStream( + Stream wrappedStream, bool allowRead, bool allowWrite, bool allowSeek + ) { + this.stream = wrappedStream; + this.readAllowed = allowRead; + this.writeAllowed = allowWrite; + this.seekAllowed = allowSeek; + } + + /// Whether data can be read from the stream + public override bool CanRead { + get { return this.readAllowed; } + } + + /// Whether the stream supports seeking + public override bool CanSeek { + get { return this.seekAllowed; } + } + + /// Whether data can be written into the stream + public override bool CanWrite { + get { return this.writeAllowed; } + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + public override void Flush() { + ++this.flushCallCount; + this.stream.Flush(); + } + + /// Length of the stream in bytes + public override long Length { + get { + enforceSeekAllowed(); + return this.stream.Length; + } + } + + /// Absolute position of the file pointer within the stream + /// + /// At least one of the chained streams does not support seeking + /// + public override long Position { + get { + enforceSeekAllowed(); + return this.stream.Position; + } + set { + enforceSeekAllowed(); + this.stream.Position = value; + } + } + + /// + /// Reads a sequence of bytes from the stream and advances the position of + /// the file pointer by the number of bytes read. + /// + /// Buffer that will receive the data read from the stream + /// + /// Offset in the buffer at which the stream will place the data read + /// + /// Maximum number of bytes that will be read + /// + /// The number of bytes that were actually read from the stream and written into + /// the provided buffer + /// + public override int Read(byte[] buffer, int offset, int count) { + enforceReadAllowed(); + return this.stream.Read(buffer, offset, count); + } + + /// Changes the position of the file pointer + /// + /// Offset to move the file pointer by, relative to the position indicated by + /// the parameter. + /// + /// + /// Reference point relative to which the file pointer is placed + /// + /// The new absolute position within the stream + public override long Seek(long offset, SeekOrigin origin) { + enforceSeekAllowed(); + return this.stream.Seek(offset, origin); + } + + /// Changes the length of the stream + /// New length the stream shall have + public override void SetLength(long value) { + enforceSeekAllowed(); + this.stream.SetLength(value); + } + + /// + /// Writes a sequence of bytes to the stream and advances the position of + /// the file pointer by the number of bytes written. + /// + /// + /// Buffer containing the data that will be written to the stream + /// + /// + /// Offset in the buffer at which the data to be written starts + /// + /// Number of bytes that will be written into the stream + public override void Write(byte[] buffer, int offset, int count) { + enforceWriteAllowed(); + this.stream.Write(buffer, offset, count); + } + + /// Number of times the Flush() method has been called + public int FlushCallCount { + get { return this.flushCallCount; } + } + + /// Throws an exception if reading is not allowed + private void enforceReadAllowed() { + if(!this.readAllowed) { + throw new NotSupportedException("Reading has been disabled"); + } + } + + /// Throws an exception if writing is not allowed + private void enforceWriteAllowed() { + if(!this.writeAllowed) { + throw new NotSupportedException("Writing has been disabled"); + } + } + + /// Throws an exception if seeking is not allowed + private void enforceSeekAllowed() { + if(!this.seekAllowed) { + throw new NotSupportedException("Seeking has been disabled"); + } + } + + /// Stream being wrapped for testing + private Stream stream; + /// whether to allow reading from the wrapped stream + private bool readAllowed; + /// Whether to allow writing to the wrapped stream + private bool writeAllowed; + /// Whether to allow seeking within the wrapped stream + private bool seekAllowed; + /// Number of times the Flush() method has been called + private int flushCallCount; + + } + + #endregion // class TestStream + + /// Tests whether the partial stream constructor is working + [Test] + public void TestConstructor() { + using(MemoryStream memoryStream = new MemoryStream()) { + memoryStream.SetLength(123); + PartialStream partialStream = new PartialStream(memoryStream, 23, 100); + Assert.AreEqual(100, partialStream.Length); + } + } + + /// + /// Verifies that the partial stream constructor throws an exception if + /// it's invoked with an invalid start offset + /// + [Test] + public void TestThrowOnInvalidStartInConstructor() { + using(MemoryStream memoryStream = new MemoryStream()) { + memoryStream.SetLength(123); + Assert.Throws( + delegate() { Console.WriteLine(new PartialStream(memoryStream, -1, 10)); } + ); + } + } + + /// + /// Verifies that the partial stream constructor throws an exception if + /// it's invoked with an invalid start offset + /// + [Test] + public void TestThrowOnInvalidLengthInConstructor() { + using(MemoryStream memoryStream = new MemoryStream()) { + memoryStream.SetLength(123); + Assert.Throws( + delegate() { Console.WriteLine(new PartialStream(memoryStream, 100, 24)); } + ); + } + } + + /// + /// Verifies that the partial stream constructor throws an exception if + /// it's invoked with a start offset on an unseekable stream + /// + [Test] + public void TestThrowOnUnseekableStreamWithOffsetInConstructor() { + using(MemoryStream memoryStream = new MemoryStream()) { + memoryStream.SetLength(123); + TestStream testStream = new TestStream(memoryStream, true, true, false); + Assert.Throws( + delegate() { Console.WriteLine(new PartialStream(testStream, 23, 100)); } + ); + } + } + + /// + /// Tests whether the CanRead property reports its status correctly + /// + [Test] + public void TestCanReadProperty() { + using(MemoryStream memoryStream = new MemoryStream()) { + TestStream yesStream = new TestStream(memoryStream, true, true, true); + TestStream noStream = new TestStream(memoryStream, false, true, true); + + Assert.IsTrue(new PartialStream(yesStream, 0, 0).CanRead); + Assert.IsFalse(new PartialStream(noStream, 0, 0).CanRead); + } + } + + /// + /// Tests whether the CanWrite property reports its status correctly + /// + [Test] + public void TestCanWriteProperty() { + using(MemoryStream memoryStream = new MemoryStream()) { + TestStream yesStream = new TestStream(memoryStream, true, true, true); + TestStream noStream = new TestStream(memoryStream, true, false, true); + + Assert.IsTrue(new PartialStream(yesStream, 0, 0).CanWrite); + Assert.IsFalse(new PartialStream(noStream, 0, 0).CanWrite); + } + } + + /// + /// Tests whether the CanSeek property reports its status correctly + /// + [Test] + public void TestCanSeekProperty() { + using(MemoryStream memoryStream = new MemoryStream()) { + TestStream yesStream = new TestStream(memoryStream, true, true, true); + TestStream noStream = new TestStream(memoryStream, true, true, false); + + Assert.IsTrue(new PartialStream(yesStream, 0, 0).CanSeek); + Assert.IsFalse(new PartialStream(noStream, 0, 0).CanSeek); + } + } + + /// + /// Tests whether the CompleteStream property returns the original stream + /// + [Test] + public void TestCompleteStreamProperty() { + using(MemoryStream memoryStream = new MemoryStream()) { + PartialStream partialStream = new PartialStream(memoryStream, 0, 0); + Assert.AreSame(memoryStream, partialStream.CompleteStream); + } + } + + /// Tests whether the Flush() method can be called + [Test] + public void TestFlush() { + using(MemoryStream memoryStream = new MemoryStream()) { + PartialStream partialStream = new PartialStream(memoryStream, 0, 0); + partialStream.Flush(); + } + } + + /// + /// Tests whether the Position property correctly reports the file pointer position + /// + [Test] + public void TestGetPosition() { + using(MemoryStream memoryStream = new MemoryStream()) { + memoryStream.SetLength(123); + PartialStream partialStream = new PartialStream(memoryStream, 23, 100); + + Assert.AreEqual(0, partialStream.Position); + + byte[] test = new byte[10]; + int bytesRead = partialStream.Read(test, 0, 10); + + Assert.AreEqual(10, bytesRead); + Assert.AreEqual(10, partialStream.Position); + + bytesRead = partialStream.Read(test, 0, 10); + + Assert.AreEqual(10, bytesRead); + Assert.AreEqual(20, partialStream.Position); + } + } + + /// + /// Tests whether the Position property is correctly updated + /// + [Test] + public void TestSetPosition() { + using(MemoryStream memoryStream = new MemoryStream()) { + memoryStream.SetLength(123); + PartialStream partialStream = new PartialStream(memoryStream, 23, 100); + + Assert.AreEqual(0, partialStream.Position); + partialStream.Position = 7; + Assert.AreEqual(partialStream.Position, 7); + partialStream.Position = 14; + Assert.AreEqual(partialStream.Position, 14); + } + } + + /// + /// Tests whether the Position property throws an exception if the stream does + /// not support seeking. + /// + [Test] + public void TestThrowOnGetPositionOnUnseekableStream() { + using(MemoryStream memoryStream = new MemoryStream()) { + TestStream testStream = new TestStream(memoryStream, true, true, false); + + PartialStream partialStream = new PartialStream(testStream, 0, 0); + Assert.Throws( + delegate() { Console.WriteLine(partialStream.Position); } + ); + } + } + + /// + /// Tests whether the Position property throws an exception if the stream does + /// not support seeking. + /// + [Test] + public void TestThrowOnSetPositionOnUnseekableStream() { + using(MemoryStream memoryStream = new MemoryStream()) { + TestStream testStream = new TestStream(memoryStream, true, true, false); + + PartialStream partialStream = new PartialStream(testStream, 0, 0); + Assert.Throws( + delegate() { partialStream.Position = 0; } + ); + } + } + + /// + /// Tests whether the Read() method throws an exception if the stream does + /// not support reading + /// + [Test] + public void TestThrowOnReadFromUnreadableStream() { + using(MemoryStream memoryStream = new MemoryStream()) { + TestStream testStream = new TestStream(memoryStream, false, true, true); + PartialStream partialStream = new PartialStream(testStream, 0, 0); + + byte[] test = new byte[10]; + Assert.Throws( + delegate() { Console.WriteLine(partialStream.Read(test, 0, 10)); } + ); + } + } + + /// + /// Tests whether the Seek() method of the partial stream is working + /// + [Test] + public void TestSeeking() { + using(MemoryStream memoryStream = new MemoryStream()) { + memoryStream.SetLength(20); + PartialStream partialStream = new PartialStream(memoryStream, 0, 20); + + Assert.AreEqual(7, partialStream.Seek(-13, SeekOrigin.End)); + Assert.AreEqual(14, partialStream.Seek(7, SeekOrigin.Current)); + Assert.AreEqual(11, partialStream.Seek(11, SeekOrigin.Begin)); + } + } + + /// + /// Tests whether the Seek() method throws an exception if an invalid + /// reference point is provided + /// + [Test] + public void TestThrowOnInvalidSeekReferencePoint() { + using(MemoryStream memoryStream = new MemoryStream()) { + PartialStream partialStream = new PartialStream(memoryStream, 0, 0); + Assert.Throws( + delegate() { partialStream.Seek(1, (SeekOrigin)12345); } + ); + } + } + + /// + /// Verifies that the partial stream throws an exception if the attempt is + /// made to change the length of the stream + /// + [Test] + public void TestThrowOnLengthChange() { + using(MemoryStream memoryStream = new MemoryStream()) { + PartialStream partialStream = new PartialStream(memoryStream, 0, 0); + Assert.Throws( + delegate() { partialStream.SetLength(123); } + ); + } + } + + /// + /// Tests whether the Read() method returns 0 bytes if the attempt is made + /// to read data from an invalid position + /// + [Test] + public void TestReadFromInvalidPosition() { + using(MemoryStream memoryStream = new MemoryStream()) { + memoryStream.SetLength(123); + + memoryStream.Position = 1123; + byte[] test = new byte[10]; + + Assert.AreEqual(0, memoryStream.Read(test, 0, 10)); + } + } + + /// Verifies that the Read() method is working + [Test] + public void TestReadFromPartialStream() { + using(MemoryStream memoryStream = new MemoryStream()) { + memoryStream.SetLength(123); + + memoryStream.Position = 100; + memoryStream.Write(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, 0, 10); + + PartialStream partialStream = new PartialStream(memoryStream, 95, 10); + + byte[] buffer = new byte[15]; + int bytesRead = partialStream.Read(buffer, 0, 15); + + Assert.AreEqual(10, bytesRead); + Assert.AreEqual( + new byte[] { 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 0, 0, 0, 0, 0 }, buffer + ); + } + } + + /// Verifies that the Write() method is working + [Test] + public void TestWriteToPartialStream() { + using(MemoryStream memoryStream = new MemoryStream()) { + memoryStream.SetLength(123); + + memoryStream.Position = 60; + memoryStream.Write(new byte[] { 11, 12, 13, 14, 15 }, 0, 5); + + PartialStream partialStream = new PartialStream(memoryStream, 50, 15); + partialStream.Position = 3; + partialStream.Write(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, 0, 10); + + byte[] buffer = new byte[17]; + memoryStream.Position = 49; + int bytesRead = memoryStream.Read(buffer, 0, 17); + + Assert.AreEqual(17, bytesRead); + Assert.AreEqual( + new byte[] { 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 14, 15, 0 }, + buffer + ); + } + } + + /// + /// Verifies that an exception is thrown if the Write() method of the partial stream + /// is attempted to be used to extend the partial stream's length + /// + [Test] + public void TestThrowOnExtendPartialStream() { + using(MemoryStream memoryStream = new MemoryStream()) { + memoryStream.SetLength(25); + + PartialStream partialStream = new PartialStream(memoryStream, 10, 10); + partialStream.Position = 5; + Assert.Throws( + delegate() { partialStream.Write(new byte[] { 1, 2, 3, 4, 5, 6 }, 0, 6); } + ); + } + } + + } + +} // namespace Nuclex.Support.IO + +#endif // UNITTEST diff --git a/Source/IO/PartialStream.cs b/Source/IO/PartialStream.cs index 91db86e..e5525be 100644 --- a/Source/IO/PartialStream.cs +++ b/Source/IO/PartialStream.cs @@ -1,261 +1,260 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.IO; - -namespace Nuclex.Support.IO { - - /// Wraps a stream and exposes only a limited region of its data - public class PartialStream : Stream { - - /// Initializes a new partial stream - /// - /// Stream the wrapper will make a limited region accessible of - /// - /// - /// Start index in the stream which becomes the beginning for the wrapper - /// - /// - /// Length the wrapped stream should report and allow access to - /// - public PartialStream(Stream stream, long start, long length) { - if(start < 0) { - throw new ArgumentException("Start index must not be less than 0", "start"); - } - - if(stream.CanSeek) { - if(start + length > stream.Length) { - throw new ArgumentException( - "Partial stream exceeds end of full stream", "length" - ); - } - } else { - if(start != 0) { - throw new ArgumentException( - "The only valid start for unseekable streams is 0", "start" - ); - } - } - - this.stream = stream; - this.start = start; - this.length = length; - } - - /// Whether data can be read from the stream - public override bool CanRead { - get { return this.stream.CanRead; } - } - - /// Whether the stream supports seeking - public override bool CanSeek { - get { return this.stream.CanSeek; } - } - - /// Whether data can be written into the stream - public override bool CanWrite { - get { return this.stream.CanWrite; } - } - - /// - /// Clears all buffers for this stream and causes any buffered data to be written - /// to the underlying device. - /// - public override void Flush() { - this.stream.Flush(); - } - - /// Length of the stream in bytes - /// - /// The wrapped stream does not support seeking - /// - public override long Length { - get { return this.length; } - } - - /// Absolute position of the file pointer within the stream - /// - /// The wrapped stream does not support seeking - /// - public override long Position { - get { - if(!this.stream.CanSeek) { - throw makeSeekNotSupportedException("seek"); - } - - return this.position; - } - set { moveFilePointer(value); } - } - - /// - /// Reads a sequence of bytes from the stream and advances the position of - /// the file pointer by the number of bytes read. - /// - /// Buffer that will receive the data read from the stream - /// - /// Offset in the buffer at which the stream will place the data read - /// - /// Maximum number of bytes that will be read - /// - /// The number of bytes that were actually read from the stream and written into - /// the provided buffer - /// - /// - /// The wrapped stream does not support reading - /// - public override int Read(byte[] buffer, int offset, int count) { - if(!this.stream.CanRead) { - throw new NotSupportedException( - "Can't read: the wrapped stream doesn't support reading" - ); - } - - long remaining = this.length - this.position; - int bytesToRead = (int)Math.Min(count, remaining); - - if(this.stream.CanSeek) { - this.stream.Position = this.position + this.start; - } - int bytesRead = this.stream.Read(buffer, offset, bytesToRead); - this.position += bytesRead; - - return bytesRead; - } - - /// Changes the position of the file pointer - /// - /// Offset to move the file pointer by, relative to the position indicated by - /// the parameter. - /// - /// - /// Reference point relative to which the file pointer is placed - /// - /// The new absolute position within the stream - public override long Seek(long offset, SeekOrigin origin) { - switch(origin) { - case SeekOrigin.Begin: { - return Position = offset; - } - case SeekOrigin.Current: { - return Position += offset; - } - case SeekOrigin.End: { - return Position = (Length + offset); - } - default: { - throw new ArgumentException("Invalid seek origin", "origin"); - } - } - } - - /// Changes the length of the stream - /// New length the stream shall have - /// - /// Always, the stream chainer does not support the SetLength() operation - /// - public override void SetLength(long value) { - throw new NotSupportedException("Resizing partial streams is not supported"); - } - - /// - /// Writes a sequence of bytes to the stream and advances the position of - /// the file pointer by the number of bytes written. - /// - /// - /// Buffer containing the data that will be written to the stream - /// - /// - /// Offset in the buffer at which the data to be written starts - /// - /// Number of bytes that will be written into the stream - /// - /// The behavior of this method is as follows: If one or more chained streams - /// do not support seeking, all data is appended to the final stream in the - /// chain. Otherwise, writing will begin with the stream the current file pointer - /// offset falls into. If the end of that stream is reached, writing continues - /// in the next stream. On the last stream, writing more data into the stream - /// that it current size allows will enlarge the stream. - /// - public override void Write(byte[] buffer, int offset, int count) { - long remaining = this.length - this.position; - if(count > remaining) { - throw new NotSupportedException( - "Cannot extend the length of the partial stream" - ); - } - - if(this.stream.CanSeek) { - this.stream.Position = this.position + this.start; - } - this.stream.Write(buffer, offset, count); - - this.position += count; - } - - /// Stream being wrapped by the partial stream wrapper - public Stream CompleteStream { - get { return this.stream; } - } - - /// Moves the file pointer - /// New position the file pointer will be moved to - private void moveFilePointer(long position) { - if(!this.stream.CanSeek) { - throw makeSeekNotSupportedException("seek"); - } - - // Seemingly, it is okay to move the file pointer beyond the end of - // the stream until you try to Read() or Write() - this.position = position; - } - - /// - /// Constructs a NotSupportException for an error caused by the wrapped - /// stream having no seek support - /// - /// Action that was tried to perform - /// The newly constructed NotSupportedException - private static NotSupportedException makeSeekNotSupportedException(string action) { - return new NotSupportedException( - string.Format( - "Can't {0}: the wrapped stream does not support seeking", - action - ) - ); - } - - /// Streams that have been chained together - private Stream stream; - /// Start index of the partial stream in the wrapped stream - private long start; - /// Zero-based position of the partial stream's file pointer - /// - /// If the stream does not support seeking, the position will simply be counted - /// up until it reaches . - /// - private long position; - /// Length of the partial stream - private long length; - - } - -} // namespace Nuclex.Support.IO +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.IO; + +namespace Nuclex.Support.IO { + + /// Wraps a stream and exposes only a limited region of its data + public class PartialStream : Stream { + + /// Initializes a new partial stream + /// + /// Stream the wrapper will make a limited region accessible of + /// + /// + /// Start index in the stream which becomes the beginning for the wrapper + /// + /// + /// Length the wrapped stream should report and allow access to + /// + public PartialStream(Stream stream, long start, long length) { + if(start < 0) { + throw new ArgumentException("Start index must not be less than 0", "start"); + } + + if(stream.CanSeek) { + if(start + length > stream.Length) { + throw new ArgumentException( + "Partial stream exceeds end of full stream", "length" + ); + } + } else { + if(start != 0) { + throw new ArgumentException( + "The only valid start for unseekable streams is 0", "start" + ); + } + } + + this.stream = stream; + this.start = start; + this.length = length; + } + + /// Whether data can be read from the stream + public override bool CanRead { + get { return this.stream.CanRead; } + } + + /// Whether the stream supports seeking + public override bool CanSeek { + get { return this.stream.CanSeek; } + } + + /// Whether data can be written into the stream + public override bool CanWrite { + get { return this.stream.CanWrite; } + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written + /// to the underlying device. + /// + public override void Flush() { + this.stream.Flush(); + } + + /// Length of the stream in bytes + /// + /// The wrapped stream does not support seeking + /// + public override long Length { + get { return this.length; } + } + + /// Absolute position of the file pointer within the stream + /// + /// The wrapped stream does not support seeking + /// + public override long Position { + get { + if(!this.stream.CanSeek) { + throw makeSeekNotSupportedException("seek"); + } + + return this.position; + } + set { moveFilePointer(value); } + } + + /// + /// Reads a sequence of bytes from the stream and advances the position of + /// the file pointer by the number of bytes read. + /// + /// Buffer that will receive the data read from the stream + /// + /// Offset in the buffer at which the stream will place the data read + /// + /// Maximum number of bytes that will be read + /// + /// The number of bytes that were actually read from the stream and written into + /// the provided buffer + /// + /// + /// The wrapped stream does not support reading + /// + public override int Read(byte[] buffer, int offset, int count) { + if(!this.stream.CanRead) { + throw new NotSupportedException( + "Can't read: the wrapped stream doesn't support reading" + ); + } + + long remaining = this.length - this.position; + int bytesToRead = (int)Math.Min(count, remaining); + + if(this.stream.CanSeek) { + this.stream.Position = this.position + this.start; + } + int bytesRead = this.stream.Read(buffer, offset, bytesToRead); + this.position += bytesRead; + + return bytesRead; + } + + /// Changes the position of the file pointer + /// + /// Offset to move the file pointer by, relative to the position indicated by + /// the parameter. + /// + /// + /// Reference point relative to which the file pointer is placed + /// + /// The new absolute position within the stream + public override long Seek(long offset, SeekOrigin origin) { + switch(origin) { + case SeekOrigin.Begin: { + return Position = offset; + } + case SeekOrigin.Current: { + return Position += offset; + } + case SeekOrigin.End: { + return Position = (Length + offset); + } + default: { + throw new ArgumentException("Invalid seek origin", "origin"); + } + } + } + + /// Changes the length of the stream + /// New length the stream shall have + /// + /// Always, the stream chainer does not support the SetLength() operation + /// + public override void SetLength(long value) { + throw new NotSupportedException("Resizing partial streams is not supported"); + } + + /// + /// Writes a sequence of bytes to the stream and advances the position of + /// the file pointer by the number of bytes written. + /// + /// + /// Buffer containing the data that will be written to the stream + /// + /// + /// Offset in the buffer at which the data to be written starts + /// + /// Number of bytes that will be written into the stream + /// + /// The behavior of this method is as follows: If one or more chained streams + /// do not support seeking, all data is appended to the final stream in the + /// chain. Otherwise, writing will begin with the stream the current file pointer + /// offset falls into. If the end of that stream is reached, writing continues + /// in the next stream. On the last stream, writing more data into the stream + /// that it current size allows will enlarge the stream. + /// + public override void Write(byte[] buffer, int offset, int count) { + long remaining = this.length - this.position; + if(count > remaining) { + throw new NotSupportedException( + "Cannot extend the length of the partial stream" + ); + } + + if(this.stream.CanSeek) { + this.stream.Position = this.position + this.start; + } + this.stream.Write(buffer, offset, count); + + this.position += count; + } + + /// Stream being wrapped by the partial stream wrapper + public Stream CompleteStream { + get { return this.stream; } + } + + /// Moves the file pointer + /// New position the file pointer will be moved to + private void moveFilePointer(long position) { + if(!this.stream.CanSeek) { + throw makeSeekNotSupportedException("seek"); + } + + // Seemingly, it is okay to move the file pointer beyond the end of + // the stream until you try to Read() or Write() + this.position = position; + } + + /// + /// Constructs a NotSupportException for an error caused by the wrapped + /// stream having no seek support + /// + /// Action that was tried to perform + /// The newly constructed NotSupportedException + private static NotSupportedException makeSeekNotSupportedException(string action) { + return new NotSupportedException( + string.Format( + "Can't {0}: the wrapped stream does not support seeking", + action + ) + ); + } + + /// Streams that have been chained together + private Stream stream; + /// Start index of the partial stream in the wrapped stream + private long start; + /// Zero-based position of the partial stream's file pointer + /// + /// If the stream does not support seeking, the position will simply be counted + /// up until it reaches . + /// + private long position; + /// Length of the partial stream + private long length; + + } + +} // namespace Nuclex.Support.IO diff --git a/Source/IO/RingMemoryStream.Test.cs b/Source/IO/RingMemoryStream.Test.cs index beb2e77..c71092f 100644 --- a/Source/IO/RingMemoryStream.Test.cs +++ b/Source/IO/RingMemoryStream.Test.cs @@ -1,330 +1,329 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.IO; - -#if UNITTEST - -using NUnit.Framework; - -namespace Nuclex.Support.IO { - - /// Unit Test for the ring buffer class - [TestFixture] - internal class RingMemoryStreamTest { - - /// Prepares some test data for the units test methods - [TestFixtureSetUp] - public void Setup() { - this.testBytes = new byte[20]; - for(int i = 0; i < 20; ++i) - this.testBytes[i] = (byte)i; - } - - /// - /// Ensures that the ring buffer blocks write attempts that would exceed its capacity - /// - [Test] - public void TestWriteTooLargeChunk() { - Assert.Throws( - delegate() { new RingMemoryStream(10).Write(this.testBytes, 0, 11); } - ); - } - - /// - /// Ensures that the ring buffer still accepts write attempts that would fill the - /// entire buffer in one go. - /// - [Test] - public void TestWriteBarelyFittingChunk() { - new RingMemoryStream(10).Write(this.testBytes, 0, 10); - } - - /// - /// Ensures that the ring buffer correctly manages write attempts that have to - /// be split at the end of the ring buffer - /// - [Test] - public void TestWriteSplitBlock() { - RingMemoryStream testRing = new RingMemoryStream(10); - testRing.Write(this.testBytes, 0, 8); - testRing.Read(this.testBytes, 0, 5); - testRing.Write(this.testBytes, 0, 7); - - byte[] actual = new byte[10]; - testRing.Read(actual, 0, 10); - Assert.AreEqual(new byte[] { 5, 6, 7, 0, 1, 2, 3, 4, 5, 6 }, actual); - } - - /// - /// Ensures that the ring buffer correctly manages write attempts that write into - /// the gap after the ring buffer's data has become split - /// - [Test] - public void TestWriteSplitAndLinearBlock() { - RingMemoryStream testRing = new RingMemoryStream(10); - testRing.Write(this.testBytes, 0, 8); - testRing.Read(this.testBytes, 0, 5); - testRing.Write(this.testBytes, 0, 5); - testRing.Write(this.testBytes, 0, 2); - - byte[] actual = new byte[10]; - testRing.Read(actual, 0, 10); - Assert.AreEqual(new byte[] { 5, 6, 7, 0, 1, 2, 3, 4, 0, 1 }, actual); - } - - /// - /// Ensures that the ring buffer still detects write that would exceed its capacity - /// if they write into the gap after the ring buffer's data has become split - /// - [Test] - public void TestWriteSplitAndLinearTooLargeBlock() { - RingMemoryStream testRing = new RingMemoryStream(10); - testRing.Write(this.testBytes, 0, 8); - testRing.Read(this.testBytes, 0, 5); - testRing.Write(this.testBytes, 0, 5); - Assert.Throws( - delegate() { testRing.Write(this.testBytes, 0, 3); } - ); - } - - /// Tests whether the ring buffer correctly handles fragmentation - [Test] - public void TestSplitBlockWrappedRead() { - RingMemoryStream testRing = new RingMemoryStream(10); - testRing.Write(this.testBytes, 0, 10); - testRing.Read(this.testBytes, 0, 5); - testRing.Write(this.testBytes, 0, 5); - - byte[] actual = new byte[10]; - testRing.Read(actual, 0, 10); - Assert.AreEqual(new byte[] { 5, 6, 7, 8, 9, 0, 1, 2, 3, 4 }, actual); - } - - /// Tests whether the ring buffer correctly handles fragmentation - [Test] - public void TestSplitBlockLinearRead() { - RingMemoryStream testRing = new RingMemoryStream(10); - testRing.Write(this.testBytes, 0, 10); - testRing.Read(this.testBytes, 0, 5); - testRing.Write(this.testBytes, 0, 5); - - byte[] actual = new byte[5]; - testRing.Read(actual, 0, 5); - Assert.AreEqual(new byte[] { 5, 6, 7, 8, 9 }, actual); - } - - /// - /// Tests whether the ring buffer correctly returns partial data if more - /// data is requested than is contained in it. - /// - [Test] - public void TestEndOfStream() { - byte[] tempBytes = new byte[10]; - - RingMemoryStream testRing = new RingMemoryStream(10); - Assert.AreEqual(0, testRing.Read(tempBytes, 0, 5)); - - testRing.Write(this.testBytes, 0, 5); - Assert.AreEqual(5, testRing.Read(tempBytes, 0, 10)); - - testRing.Write(this.testBytes, 0, 6); - testRing.Read(tempBytes, 0, 5); - testRing.Write(this.testBytes, 0, 9); - Assert.AreEqual(10, testRing.Read(tempBytes, 0, 20)); - } - - /// - /// Validates that the ring buffer can extend its capacity without loosing data - /// - [Test] - public void TestCapacityIncrease() { - RingMemoryStream testRing = new RingMemoryStream(10); - testRing.Write(this.testBytes, 0, 10); - - testRing.Capacity = 20; - byte[] actual = new byte[10]; - testRing.Read(actual, 0, 10); - - Assert.AreEqual(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }, actual); - } - - /// - /// Validates that the ring buffer can reduce its capacity without loosing data - /// - [Test] - public void TestCapacityDecrease() { - RingMemoryStream testRing = new RingMemoryStream(20); - testRing.Write(this.testBytes, 0, 10); - - testRing.Capacity = 10; - byte[] actual = new byte[10]; - testRing.Read(actual, 0, 10); - - Assert.AreEqual(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }, actual); - } - - /// - /// Checks that an exception is thrown when the ring buffer's capacity is - /// reduced so much it would have to give up some of its contained data - /// - [Test] - public void TestCapacityDecreaseException() { - RingMemoryStream testRing = new RingMemoryStream(20); - testRing.Write(this.testBytes, 0, 20); - - Assert.Throws( - delegate() { testRing.Capacity = 10; } - ); - } - - /// Tests whether the Capacity property returns the current capacity - [Test] - public void TestCapacity() { - RingMemoryStream testRing = new RingMemoryStream(123); - - Assert.AreEqual(123, testRing.Capacity); - } - - /// Ensures that the CanRead property returns true - [Test] - public void TestCanRead() { - Assert.IsTrue(new RingMemoryStream(10).CanRead); - } - - /// Ensures that the CanSeek property returns false - [Test] - public void TestCanSeek() { - Assert.IsFalse(new RingMemoryStream(10).CanSeek); - } - - /// Ensures that the CanWrite property returns true - [Test] - public void TestCanWrite() { - Assert.IsTrue(new RingMemoryStream(10).CanWrite); - } - - /// - /// Tests whether the auto reset feature works (resets the buffer pointer to the - /// left end of the buffer when it gets empty; mainly a performance feature). - /// - [Test] - public void TestAutoReset() { - byte[] tempBytes = new byte[10]; - RingMemoryStream testRing = new RingMemoryStream(10); - - testRing.Write(this.testBytes, 0, 8); - testRing.Read(tempBytes, 0, 2); - testRing.Read(tempBytes, 0, 2); - testRing.Read(tempBytes, 0, 1); - testRing.Read(tempBytes, 0, 1); - - Assert.AreEqual(2, testRing.Length); - } - - /// - /// Verifies that an exception is thrown when the Position property of the ring - /// memory stream is used to retrieve the current file pointer position - /// - [Test] - public void TestThrowOnRetrievePosition() { - Assert.Throws( - delegate() { Console.WriteLine(new RingMemoryStream(10).Position); } - ); - } - - /// - /// Verifies that an exception is thrown when the Position property of the ring - /// memory stream is used to modify the current file pointer position - /// - [Test] - public void TestThrowOnAssignPosition() { - Assert.Throws( - delegate() { new RingMemoryStream(10).Position = 0; } - ); - } - - /// - /// Verifies that an exception is thrown when the Seek() method of the ring memory - /// stream is attempted to be used - /// - [Test] - public void TestThrowOnSeek() { - Assert.Throws( - delegate() { new RingMemoryStream(10).Seek(0, SeekOrigin.Begin); } - ); - } - - /// - /// Verifies that an exception is thrown when the SetLength() method of the ring - /// memory stream is attempted to be used - /// - [Test] - public void TestThrowOnSetLength() { - Assert.Throws( - delegate() { new RingMemoryStream(10).SetLength(10); } - ); - } - - /// - /// Tests the Flush() method of the ring memory stream, which is either a dummy - /// implementation or has no side effects - /// - [Test] - public void TestFlush() { - new RingMemoryStream(10).Flush(); - } - - /// - /// Tests whether the length property is updated in accordance to the data written - /// into the ring memory stream - /// - [Test] - public void TestLengthOnLinearBlock() { - RingMemoryStream testRing = new RingMemoryStream(10); - testRing.Write(new byte[10], 0, 10); - - Assert.AreEqual(10, testRing.Length); - } - - /// - /// Tests whether the length property is updated in accordance to the data written - /// into the ring memory stream when the data is split within the stream - /// - [Test] - public void TestLengthOnSplitBlock() { - RingMemoryStream testRing = new RingMemoryStream(10); - - testRing.Write(new byte[10], 0, 10); - testRing.Read(new byte[5], 0, 5); - testRing.Write(new byte[5], 0, 5); - - Assert.AreEqual(10, testRing.Length); - } - - /// Test data for the ring buffer unit tests - private byte[] testBytes; - - } - -} // namespace Nuclex.Support.IO - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.IO; + +#if UNITTEST + +using NUnit.Framework; + +namespace Nuclex.Support.IO { + + /// Unit Test for the ring buffer class + [TestFixture] + internal class RingMemoryStreamTest { + + /// Prepares some test data for the units test methods + [TestFixtureSetUp] + public void Setup() { + this.testBytes = new byte[20]; + for(int i = 0; i < 20; ++i) + this.testBytes[i] = (byte)i; + } + + /// + /// Ensures that the ring buffer blocks write attempts that would exceed its capacity + /// + [Test] + public void TestWriteTooLargeChunk() { + Assert.Throws( + delegate() { new RingMemoryStream(10).Write(this.testBytes, 0, 11); } + ); + } + + /// + /// Ensures that the ring buffer still accepts write attempts that would fill the + /// entire buffer in one go. + /// + [Test] + public void TestWriteBarelyFittingChunk() { + new RingMemoryStream(10).Write(this.testBytes, 0, 10); + } + + /// + /// Ensures that the ring buffer correctly manages write attempts that have to + /// be split at the end of the ring buffer + /// + [Test] + public void TestWriteSplitBlock() { + RingMemoryStream testRing = new RingMemoryStream(10); + testRing.Write(this.testBytes, 0, 8); + testRing.Read(this.testBytes, 0, 5); + testRing.Write(this.testBytes, 0, 7); + + byte[] actual = new byte[10]; + testRing.Read(actual, 0, 10); + Assert.AreEqual(new byte[] { 5, 6, 7, 0, 1, 2, 3, 4, 5, 6 }, actual); + } + + /// + /// Ensures that the ring buffer correctly manages write attempts that write into + /// the gap after the ring buffer's data has become split + /// + [Test] + public void TestWriteSplitAndLinearBlock() { + RingMemoryStream testRing = new RingMemoryStream(10); + testRing.Write(this.testBytes, 0, 8); + testRing.Read(this.testBytes, 0, 5); + testRing.Write(this.testBytes, 0, 5); + testRing.Write(this.testBytes, 0, 2); + + byte[] actual = new byte[10]; + testRing.Read(actual, 0, 10); + Assert.AreEqual(new byte[] { 5, 6, 7, 0, 1, 2, 3, 4, 0, 1 }, actual); + } + + /// + /// Ensures that the ring buffer still detects write that would exceed its capacity + /// if they write into the gap after the ring buffer's data has become split + /// + [Test] + public void TestWriteSplitAndLinearTooLargeBlock() { + RingMemoryStream testRing = new RingMemoryStream(10); + testRing.Write(this.testBytes, 0, 8); + testRing.Read(this.testBytes, 0, 5); + testRing.Write(this.testBytes, 0, 5); + Assert.Throws( + delegate() { testRing.Write(this.testBytes, 0, 3); } + ); + } + + /// Tests whether the ring buffer correctly handles fragmentation + [Test] + public void TestSplitBlockWrappedRead() { + RingMemoryStream testRing = new RingMemoryStream(10); + testRing.Write(this.testBytes, 0, 10); + testRing.Read(this.testBytes, 0, 5); + testRing.Write(this.testBytes, 0, 5); + + byte[] actual = new byte[10]; + testRing.Read(actual, 0, 10); + Assert.AreEqual(new byte[] { 5, 6, 7, 8, 9, 0, 1, 2, 3, 4 }, actual); + } + + /// Tests whether the ring buffer correctly handles fragmentation + [Test] + public void TestSplitBlockLinearRead() { + RingMemoryStream testRing = new RingMemoryStream(10); + testRing.Write(this.testBytes, 0, 10); + testRing.Read(this.testBytes, 0, 5); + testRing.Write(this.testBytes, 0, 5); + + byte[] actual = new byte[5]; + testRing.Read(actual, 0, 5); + Assert.AreEqual(new byte[] { 5, 6, 7, 8, 9 }, actual); + } + + /// + /// Tests whether the ring buffer correctly returns partial data if more + /// data is requested than is contained in it. + /// + [Test] + public void TestEndOfStream() { + byte[] tempBytes = new byte[10]; + + RingMemoryStream testRing = new RingMemoryStream(10); + Assert.AreEqual(0, testRing.Read(tempBytes, 0, 5)); + + testRing.Write(this.testBytes, 0, 5); + Assert.AreEqual(5, testRing.Read(tempBytes, 0, 10)); + + testRing.Write(this.testBytes, 0, 6); + testRing.Read(tempBytes, 0, 5); + testRing.Write(this.testBytes, 0, 9); + Assert.AreEqual(10, testRing.Read(tempBytes, 0, 20)); + } + + /// + /// Validates that the ring buffer can extend its capacity without loosing data + /// + [Test] + public void TestCapacityIncrease() { + RingMemoryStream testRing = new RingMemoryStream(10); + testRing.Write(this.testBytes, 0, 10); + + testRing.Capacity = 20; + byte[] actual = new byte[10]; + testRing.Read(actual, 0, 10); + + Assert.AreEqual(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }, actual); + } + + /// + /// Validates that the ring buffer can reduce its capacity without loosing data + /// + [Test] + public void TestCapacityDecrease() { + RingMemoryStream testRing = new RingMemoryStream(20); + testRing.Write(this.testBytes, 0, 10); + + testRing.Capacity = 10; + byte[] actual = new byte[10]; + testRing.Read(actual, 0, 10); + + Assert.AreEqual(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }, actual); + } + + /// + /// Checks that an exception is thrown when the ring buffer's capacity is + /// reduced so much it would have to give up some of its contained data + /// + [Test] + public void TestCapacityDecreaseException() { + RingMemoryStream testRing = new RingMemoryStream(20); + testRing.Write(this.testBytes, 0, 20); + + Assert.Throws( + delegate() { testRing.Capacity = 10; } + ); + } + + /// Tests whether the Capacity property returns the current capacity + [Test] + public void TestCapacity() { + RingMemoryStream testRing = new RingMemoryStream(123); + + Assert.AreEqual(123, testRing.Capacity); + } + + /// Ensures that the CanRead property returns true + [Test] + public void TestCanRead() { + Assert.IsTrue(new RingMemoryStream(10).CanRead); + } + + /// Ensures that the CanSeek property returns false + [Test] + public void TestCanSeek() { + Assert.IsFalse(new RingMemoryStream(10).CanSeek); + } + + /// Ensures that the CanWrite property returns true + [Test] + public void TestCanWrite() { + Assert.IsTrue(new RingMemoryStream(10).CanWrite); + } + + /// + /// Tests whether the auto reset feature works (resets the buffer pointer to the + /// left end of the buffer when it gets empty; mainly a performance feature). + /// + [Test] + public void TestAutoReset() { + byte[] tempBytes = new byte[10]; + RingMemoryStream testRing = new RingMemoryStream(10); + + testRing.Write(this.testBytes, 0, 8); + testRing.Read(tempBytes, 0, 2); + testRing.Read(tempBytes, 0, 2); + testRing.Read(tempBytes, 0, 1); + testRing.Read(tempBytes, 0, 1); + + Assert.AreEqual(2, testRing.Length); + } + + /// + /// Verifies that an exception is thrown when the Position property of the ring + /// memory stream is used to retrieve the current file pointer position + /// + [Test] + public void TestThrowOnRetrievePosition() { + Assert.Throws( + delegate() { Console.WriteLine(new RingMemoryStream(10).Position); } + ); + } + + /// + /// Verifies that an exception is thrown when the Position property of the ring + /// memory stream is used to modify the current file pointer position + /// + [Test] + public void TestThrowOnAssignPosition() { + Assert.Throws( + delegate() { new RingMemoryStream(10).Position = 0; } + ); + } + + /// + /// Verifies that an exception is thrown when the Seek() method of the ring memory + /// stream is attempted to be used + /// + [Test] + public void TestThrowOnSeek() { + Assert.Throws( + delegate() { new RingMemoryStream(10).Seek(0, SeekOrigin.Begin); } + ); + } + + /// + /// Verifies that an exception is thrown when the SetLength() method of the ring + /// memory stream is attempted to be used + /// + [Test] + public void TestThrowOnSetLength() { + Assert.Throws( + delegate() { new RingMemoryStream(10).SetLength(10); } + ); + } + + /// + /// Tests the Flush() method of the ring memory stream, which is either a dummy + /// implementation or has no side effects + /// + [Test] + public void TestFlush() { + new RingMemoryStream(10).Flush(); + } + + /// + /// Tests whether the length property is updated in accordance to the data written + /// into the ring memory stream + /// + [Test] + public void TestLengthOnLinearBlock() { + RingMemoryStream testRing = new RingMemoryStream(10); + testRing.Write(new byte[10], 0, 10); + + Assert.AreEqual(10, testRing.Length); + } + + /// + /// Tests whether the length property is updated in accordance to the data written + /// into the ring memory stream when the data is split within the stream + /// + [Test] + public void TestLengthOnSplitBlock() { + RingMemoryStream testRing = new RingMemoryStream(10); + + testRing.Write(new byte[10], 0, 10); + testRing.Read(new byte[5], 0, 5); + testRing.Write(new byte[5], 0, 5); + + Assert.AreEqual(10, testRing.Length); + } + + /// Test data for the ring buffer unit tests + private byte[] testBytes; + + } + +} // namespace Nuclex.Support.IO + +#endif // UNITTEST diff --git a/Source/IO/RingMemoryStream.cs b/Source/IO/RingMemoryStream.cs index e7de7ee..8c91d35 100644 --- a/Source/IO/RingMemoryStream.cs +++ b/Source/IO/RingMemoryStream.cs @@ -1,256 +1,255 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.IO; - -namespace Nuclex.Support.IO { - - /// Specialized memory stream for ring buffers - /// - /// This ring buffer class is specialized for binary data and tries to achieve - /// optimal efficiency when storing and retrieving chunks of several bytes - /// at once. Typical use cases include audio and network buffers where one party - /// is responsible for refilling the buffer at regular intervals while the other - /// constantly streams data out of it. - /// - public class RingMemoryStream : Stream { - - /// Initializes a new ring memory stream - /// Maximum capacity of the stream - public RingMemoryStream(int capacity) { - this.ringBuffer = new MemoryStream(capacity); - this.ringBuffer.SetLength(capacity); - this.empty = true; - } - - /// Maximum amount of data that will fit into the ring memory stream - /// - /// Thrown if the new capacity is too small for the data already contained - /// in the ring buffer. - /// - public long Capacity { - get { return this.ringBuffer.Length; } - set { - int length = (int)Length; - if(value < length) { - throw new ArgumentOutOfRangeException( - "New capacity is less than the stream's current length" - ); - } - - // This could be done in a more efficient manner than just replacing - // the entire buffer, but since this operation will probably be only called - // once during the lifetime of the application, if at all, I don't see - // the need to optimize it... - MemoryStream newBuffer = new MemoryStream((int)value); - - newBuffer.SetLength(value); - if(length > 0) { - Read(newBuffer.GetBuffer(), 0, length); - } - - this.ringBuffer.Close(); // Equals dispose of the old buffer - this.ringBuffer = newBuffer; - this.startIndex = 0; - this.endIndex = length; - } - - } - - /// Whether it's possible to read from this stream - public override bool CanRead { get { return true; } } - /// Whether this stream supports random access - public override bool CanSeek { get { return false; } } - /// Whether it's possible to write into this stream - public override bool CanWrite { get { return true; } } - /// Flushes the buffers and writes down unsaved data - public override void Flush() { } - - /// Current length of the stream - public override long Length { - get { - if((this.endIndex > this.startIndex) || this.empty) { - return this.endIndex - this.startIndex; - } else { - return this.ringBuffer.Length - this.startIndex + this.endIndex; - } - } - } - - /// Current cursor position within the stream - /// Always - public override long Position { - get { throw new NotSupportedException("The ring buffer does not support seeking"); } - set { throw new NotSupportedException("The ring buffer does not support seeking"); } - } - - /// Reads data from the beginning of the stream - /// Buffer in which to store the data - /// Starting index at which to begin writing the buffer - /// Number of bytes to read from the stream - /// Die Number of bytes actually read - public override int Read(byte[] buffer, int offset, int count) { - - // The end index lies behind the start index (usual case), so the - // ring memory is not fragmented. Example: |-----<#######>-----| - if((this.startIndex < this.endIndex) || this.empty) { - - // The Stream interface requires us to return less than the requested - // number of bytes if we don't have enough data - count = Math.Min(count, this.endIndex - this.startIndex); - if(count > 0) { - this.ringBuffer.Position = this.startIndex; - this.ringBuffer.Read(buffer, offset, count); - this.startIndex += count; - - if(this.startIndex == this.endIndex) { - setEmpty(); - } - } - - } else { // The end index lies in front of the start index - - // With the end before the start index, the data in the ring memory - // stream is fragmented. Example: |#####>-------<#####| - int linearAvailable = (int)this.ringBuffer.Length - this.startIndex; - - // Will this read process cross the end of the ring buffer, requiring us to - // read the data in 2 steps? - if(count > linearAvailable) { - - // The Stream interface requires us to return less than the requested - // number of bytes if we don't have enough data - count = Math.Min(count, linearAvailable + this.endIndex); - - this.ringBuffer.Position = this.startIndex; - this.ringBuffer.Read(buffer, offset, linearAvailable); - this.ringBuffer.Position = 0; - this.startIndex = count - linearAvailable; - this.ringBuffer.Read(buffer, offset + linearAvailable, this.startIndex); - - } else { // Nope, the amount of requested data can be read in one piece - this.ringBuffer.Position = this.startIndex; - this.ringBuffer.Read(buffer, offset, count); - this.startIndex += count; - - } - - // If we consumed the entire ring buffer, set the empty flag and move - // the indexes back to zero for better performance - if(this.startIndex == this.endIndex) { - setEmpty(); - } - - } - - return count; - } - - /// Appends data to the end of the stream - /// Buffer containing the data to append - /// Starting index of the data in the buffer - /// Number of bytes to write to the stream - /// When the ring buffer is full - public override void Write(byte[] buffer, int offset, int count) { - - // The end index lies behind the start index (usual case), so the - // unused buffer space is fragmented. Example: |-----<#######>-----| - if((this.startIndex < this.endIndex) || this.empty) { - int linearAvailable = (int)(this.ringBuffer.Length - this.endIndex); - - // If the data to be written would cross the ring memory stream's end, - // we have to check that there's enough space at the beginning of the - // stream to contain the remainder of the data. - if(count > linearAvailable) { - if(count > (linearAvailable + this.startIndex)) - throw new OverflowException("Data does not fit in buffer"); - - this.ringBuffer.Position = this.endIndex; - this.ringBuffer.Write(buffer, offset, linearAvailable); - this.ringBuffer.Position = 0; - this.endIndex = count - linearAvailable; - this.ringBuffer.Write(buffer, offset + linearAvailable, this.endIndex); - - } else { // All data can be appended at the current stream position - this.ringBuffer.Position = this.endIndex; - this.ringBuffer.Write(buffer, offset, count); - this.endIndex += count; - } - - this.empty = false; - - } else { // The end index lies before the start index - - // The ring memory stream has been fragmented. This means the gap into which - // we are about to write is not fragmented. Example: |#####>-------<#####| - if(count > (this.startIndex - this.endIndex)) - throw new OverflowException("Data does not fit in buffer"); - - // Because the gap isn't fragmented, we can be sure that a single - // write call will suffice. - this.ringBuffer.Position = this.endIndex; - this.ringBuffer.Write(buffer, offset, count); - this.endIndex += count; - - } - - } - - /// Jumps to the specified location within the stream - /// Position to jump to - /// Origin towards which to interpret the offset - /// The new offset within the stream - /// Always - public override long Seek(long offset, SeekOrigin origin) { - throw new NotSupportedException("The ring buffer does not support seeking"); - } - - /// Changes the length of the stream - /// New length to resize the stream to - /// Always - public override void SetLength(long value) { - throw new NotSupportedException("This operation is not supported"); - } - - /// Resets the stream to its empty state - private void setEmpty() { - this.empty = true; - this.startIndex = 0; - this.endIndex = 0; - } - - /// Internal stream containing the ring buffer data - private MemoryStream ringBuffer; - /// Start index of the data within the ring buffer - private int startIndex; - /// End index of the data within the ring buffer - private int endIndex; - /// Whether the ring buffer is empty - /// - /// This field is required to differentiate between the ring buffer being - /// filled to the limit and being totally empty, because in both cases, - /// the start index and the end index will be the same. - /// - private bool empty; - - } - -} // namespace Nuclex.Support.IO +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.IO; + +namespace Nuclex.Support.IO { + + /// Specialized memory stream for ring buffers + /// + /// This ring buffer class is specialized for binary data and tries to achieve + /// optimal efficiency when storing and retrieving chunks of several bytes + /// at once. Typical use cases include audio and network buffers where one party + /// is responsible for refilling the buffer at regular intervals while the other + /// constantly streams data out of it. + /// + public class RingMemoryStream : Stream { + + /// Initializes a new ring memory stream + /// Maximum capacity of the stream + public RingMemoryStream(int capacity) { + this.ringBuffer = new MemoryStream(capacity); + this.ringBuffer.SetLength(capacity); + this.empty = true; + } + + /// Maximum amount of data that will fit into the ring memory stream + /// + /// Thrown if the new capacity is too small for the data already contained + /// in the ring buffer. + /// + public long Capacity { + get { return this.ringBuffer.Length; } + set { + int length = (int)Length; + if(value < length) { + throw new ArgumentOutOfRangeException( + "New capacity is less than the stream's current length" + ); + } + + // This could be done in a more efficient manner than just replacing + // the entire buffer, but since this operation will probably be only called + // once during the lifetime of the application, if at all, I don't see + // the need to optimize it... + MemoryStream newBuffer = new MemoryStream((int)value); + + newBuffer.SetLength(value); + if(length > 0) { + Read(newBuffer.GetBuffer(), 0, length); + } + + this.ringBuffer.Close(); // Equals dispose of the old buffer + this.ringBuffer = newBuffer; + this.startIndex = 0; + this.endIndex = length; + } + + } + + /// Whether it's possible to read from this stream + public override bool CanRead { get { return true; } } + /// Whether this stream supports random access + public override bool CanSeek { get { return false; } } + /// Whether it's possible to write into this stream + public override bool CanWrite { get { return true; } } + /// Flushes the buffers and writes down unsaved data + public override void Flush() { } + + /// Current length of the stream + public override long Length { + get { + if((this.endIndex > this.startIndex) || this.empty) { + return this.endIndex - this.startIndex; + } else { + return this.ringBuffer.Length - this.startIndex + this.endIndex; + } + } + } + + /// Current cursor position within the stream + /// Always + public override long Position { + get { throw new NotSupportedException("The ring buffer does not support seeking"); } + set { throw new NotSupportedException("The ring buffer does not support seeking"); } + } + + /// Reads data from the beginning of the stream + /// Buffer in which to store the data + /// Starting index at which to begin writing the buffer + /// Number of bytes to read from the stream + /// Die Number of bytes actually read + public override int Read(byte[] buffer, int offset, int count) { + + // The end index lies behind the start index (usual case), so the + // ring memory is not fragmented. Example: |-----<#######>-----| + if((this.startIndex < this.endIndex) || this.empty) { + + // The Stream interface requires us to return less than the requested + // number of bytes if we don't have enough data + count = Math.Min(count, this.endIndex - this.startIndex); + if(count > 0) { + this.ringBuffer.Position = this.startIndex; + this.ringBuffer.Read(buffer, offset, count); + this.startIndex += count; + + if(this.startIndex == this.endIndex) { + setEmpty(); + } + } + + } else { // The end index lies in front of the start index + + // With the end before the start index, the data in the ring memory + // stream is fragmented. Example: |#####>-------<#####| + int linearAvailable = (int)this.ringBuffer.Length - this.startIndex; + + // Will this read process cross the end of the ring buffer, requiring us to + // read the data in 2 steps? + if(count > linearAvailable) { + + // The Stream interface requires us to return less than the requested + // number of bytes if we don't have enough data + count = Math.Min(count, linearAvailable + this.endIndex); + + this.ringBuffer.Position = this.startIndex; + this.ringBuffer.Read(buffer, offset, linearAvailable); + this.ringBuffer.Position = 0; + this.startIndex = count - linearAvailable; + this.ringBuffer.Read(buffer, offset + linearAvailable, this.startIndex); + + } else { // Nope, the amount of requested data can be read in one piece + this.ringBuffer.Position = this.startIndex; + this.ringBuffer.Read(buffer, offset, count); + this.startIndex += count; + + } + + // If we consumed the entire ring buffer, set the empty flag and move + // the indexes back to zero for better performance + if(this.startIndex == this.endIndex) { + setEmpty(); + } + + } + + return count; + } + + /// Appends data to the end of the stream + /// Buffer containing the data to append + /// Starting index of the data in the buffer + /// Number of bytes to write to the stream + /// When the ring buffer is full + public override void Write(byte[] buffer, int offset, int count) { + + // The end index lies behind the start index (usual case), so the + // unused buffer space is fragmented. Example: |-----<#######>-----| + if((this.startIndex < this.endIndex) || this.empty) { + int linearAvailable = (int)(this.ringBuffer.Length - this.endIndex); + + // If the data to be written would cross the ring memory stream's end, + // we have to check that there's enough space at the beginning of the + // stream to contain the remainder of the data. + if(count > linearAvailable) { + if(count > (linearAvailable + this.startIndex)) + throw new OverflowException("Data does not fit in buffer"); + + this.ringBuffer.Position = this.endIndex; + this.ringBuffer.Write(buffer, offset, linearAvailable); + this.ringBuffer.Position = 0; + this.endIndex = count - linearAvailable; + this.ringBuffer.Write(buffer, offset + linearAvailable, this.endIndex); + + } else { // All data can be appended at the current stream position + this.ringBuffer.Position = this.endIndex; + this.ringBuffer.Write(buffer, offset, count); + this.endIndex += count; + } + + this.empty = false; + + } else { // The end index lies before the start index + + // The ring memory stream has been fragmented. This means the gap into which + // we are about to write is not fragmented. Example: |#####>-------<#####| + if(count > (this.startIndex - this.endIndex)) + throw new OverflowException("Data does not fit in buffer"); + + // Because the gap isn't fragmented, we can be sure that a single + // write call will suffice. + this.ringBuffer.Position = this.endIndex; + this.ringBuffer.Write(buffer, offset, count); + this.endIndex += count; + + } + + } + + /// Jumps to the specified location within the stream + /// Position to jump to + /// Origin towards which to interpret the offset + /// The new offset within the stream + /// Always + public override long Seek(long offset, SeekOrigin origin) { + throw new NotSupportedException("The ring buffer does not support seeking"); + } + + /// Changes the length of the stream + /// New length to resize the stream to + /// Always + public override void SetLength(long value) { + throw new NotSupportedException("This operation is not supported"); + } + + /// Resets the stream to its empty state + private void setEmpty() { + this.empty = true; + this.startIndex = 0; + this.endIndex = 0; + } + + /// Internal stream containing the ring buffer data + private MemoryStream ringBuffer; + /// Start index of the data within the ring buffer + private int startIndex; + /// End index of the data within the ring buffer + private int endIndex; + /// Whether the ring buffer is empty + /// + /// This field is required to differentiate between the ring buffer being + /// filled to the limit and being totally empty, because in both cases, + /// the start index and the end index will be the same. + /// + private bool empty; + + } + +} // namespace Nuclex.Support.IO diff --git a/Source/IntegerHelper.Test.cs b/Source/IntegerHelper.Test.cs index a3aa04b..1a0fd38 100644 --- a/Source/IntegerHelper.Test.cs +++ b/Source/IntegerHelper.Test.cs @@ -1,130 +1,129 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.Collections.Generic; - -using NUnit.Framework; - -namespace Nuclex.Support { - - /// Contains unit tests for the integer helper class - [TestFixture] - internal class IntegerHelperTest { - - /// - /// Verifies that the next power of 2 calculation works for long integers - /// - [Test] - public void TestNextPowerOf2ULong() { - Assert.AreEqual(1UL, IntegerHelper.NextPowerOf2(0UL)); - Assert.AreEqual(1UL, IntegerHelper.NextPowerOf2(1UL)); - Assert.AreEqual(2UL, IntegerHelper.NextPowerOf2(2UL)); - Assert.AreEqual(4UL, IntegerHelper.NextPowerOf2(3UL)); - Assert.AreEqual(4UL, IntegerHelper.NextPowerOf2(4UL)); - Assert.AreEqual( - 9223372036854775808UL, IntegerHelper.NextPowerOf2(4611686018427387905UL) - ); - Assert.AreEqual( - 9223372036854775808UL, IntegerHelper.NextPowerOf2(9223372036854775807UL) - ); - Assert.AreEqual( - 9223372036854775808UL, IntegerHelper.NextPowerOf2(9223372036854775808UL) - ); - } - - /// - /// Verifies that the next power of 2 calculation works for long integers - /// - [Test] - public void TestNextPowerOf2Long() { - Assert.AreEqual(1L, IntegerHelper.NextPowerOf2(0L)); - Assert.AreEqual(1L, IntegerHelper.NextPowerOf2(1L)); - Assert.AreEqual(2L, IntegerHelper.NextPowerOf2(2L)); - Assert.AreEqual(4L, IntegerHelper.NextPowerOf2(3L)); - Assert.AreEqual(4L, IntegerHelper.NextPowerOf2(4L)); - Assert.AreEqual(4611686018427387904L, IntegerHelper.NextPowerOf2(2305843009213693953L)); - Assert.AreEqual(4611686018427387904L, IntegerHelper.NextPowerOf2(4611686018427387903L)); - Assert.AreEqual(4611686018427387904L, IntegerHelper.NextPowerOf2(4611686018427387904L)); - } - - /// - /// Verifies that the next power of 2 calculation works for integers - /// - [Test] - public void TestNextPowerOf2UInt() { - Assert.AreEqual(1U, IntegerHelper.NextPowerOf2(0U)); - Assert.AreEqual(1U, IntegerHelper.NextPowerOf2(1U)); - Assert.AreEqual(2U, IntegerHelper.NextPowerOf2(2U)); - Assert.AreEqual(4U, IntegerHelper.NextPowerOf2(3U)); - Assert.AreEqual(4U, IntegerHelper.NextPowerOf2(4U)); - Assert.AreEqual(2147483648U, IntegerHelper.NextPowerOf2(1073741825U)); - Assert.AreEqual(2147483648U, IntegerHelper.NextPowerOf2(2147483647U)); - Assert.AreEqual(2147483648U, IntegerHelper.NextPowerOf2(2147483648U)); - } - - /// - /// Verifies that the next power of 2 calculation works for integers - /// - [Test] - public void TestNextPowerOf2Int() { - Assert.AreEqual(1, IntegerHelper.NextPowerOf2(0)); - Assert.AreEqual(1, IntegerHelper.NextPowerOf2(1)); - Assert.AreEqual(2, IntegerHelper.NextPowerOf2(2)); - Assert.AreEqual(4, IntegerHelper.NextPowerOf2(3)); - Assert.AreEqual(4, IntegerHelper.NextPowerOf2(4)); - Assert.AreEqual(1073741824, IntegerHelper.NextPowerOf2(536870913)); - Assert.AreEqual(1073741824, IntegerHelper.NextPowerOf2(1073741823)); - Assert.AreEqual(1073741824, IntegerHelper.NextPowerOf2(1073741824)); - } - - /// Verifies that the bit counting method for integers works - [Test] - public void TestCountBitsInInteger() { - Assert.AreEqual(0, IntegerHelper.CountBits(0)); - Assert.AreEqual(32, IntegerHelper.CountBits(-1)); - Assert.AreEqual(16, IntegerHelper.CountBits(0x55555555)); - Assert.AreEqual(16, IntegerHelper.CountBits(0xAAAAAAAA)); - - for (int bitIndex = 0; bitIndex < 32; ++bitIndex) { - Assert.AreEqual(1, IntegerHelper.CountBits(1 << bitIndex)); - } - } - - /// Verifies that the bit counting method for long integers works - [Test] - public void TestCountBitsInLongInteger() { - Assert.AreEqual(0, IntegerHelper.CountBits(0L)); - Assert.AreEqual(64, IntegerHelper.CountBits(-1L)); - Assert.AreEqual(32, IntegerHelper.CountBits(0x5555555555555555)); - Assert.AreEqual(32, IntegerHelper.CountBits(0xAAAAAAAAAAAAAAAA)); - - for (int bitIndex = 0; bitIndex < 64; ++bitIndex) { - Assert.AreEqual(1, IntegerHelper.CountBits(1 << bitIndex)); - } - } - - } - -} // namespace Nuclex.Support - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.Collections.Generic; + +using NUnit.Framework; + +namespace Nuclex.Support { + + /// Contains unit tests for the integer helper class + [TestFixture] + internal class IntegerHelperTest { + + /// + /// Verifies that the next power of 2 calculation works for long integers + /// + [Test] + public void TestNextPowerOf2ULong() { + Assert.AreEqual(1UL, IntegerHelper.NextPowerOf2(0UL)); + Assert.AreEqual(1UL, IntegerHelper.NextPowerOf2(1UL)); + Assert.AreEqual(2UL, IntegerHelper.NextPowerOf2(2UL)); + Assert.AreEqual(4UL, IntegerHelper.NextPowerOf2(3UL)); + Assert.AreEqual(4UL, IntegerHelper.NextPowerOf2(4UL)); + Assert.AreEqual( + 9223372036854775808UL, IntegerHelper.NextPowerOf2(4611686018427387905UL) + ); + Assert.AreEqual( + 9223372036854775808UL, IntegerHelper.NextPowerOf2(9223372036854775807UL) + ); + Assert.AreEqual( + 9223372036854775808UL, IntegerHelper.NextPowerOf2(9223372036854775808UL) + ); + } + + /// + /// Verifies that the next power of 2 calculation works for long integers + /// + [Test] + public void TestNextPowerOf2Long() { + Assert.AreEqual(1L, IntegerHelper.NextPowerOf2(0L)); + Assert.AreEqual(1L, IntegerHelper.NextPowerOf2(1L)); + Assert.AreEqual(2L, IntegerHelper.NextPowerOf2(2L)); + Assert.AreEqual(4L, IntegerHelper.NextPowerOf2(3L)); + Assert.AreEqual(4L, IntegerHelper.NextPowerOf2(4L)); + Assert.AreEqual(4611686018427387904L, IntegerHelper.NextPowerOf2(2305843009213693953L)); + Assert.AreEqual(4611686018427387904L, IntegerHelper.NextPowerOf2(4611686018427387903L)); + Assert.AreEqual(4611686018427387904L, IntegerHelper.NextPowerOf2(4611686018427387904L)); + } + + /// + /// Verifies that the next power of 2 calculation works for integers + /// + [Test] + public void TestNextPowerOf2UInt() { + Assert.AreEqual(1U, IntegerHelper.NextPowerOf2(0U)); + Assert.AreEqual(1U, IntegerHelper.NextPowerOf2(1U)); + Assert.AreEqual(2U, IntegerHelper.NextPowerOf2(2U)); + Assert.AreEqual(4U, IntegerHelper.NextPowerOf2(3U)); + Assert.AreEqual(4U, IntegerHelper.NextPowerOf2(4U)); + Assert.AreEqual(2147483648U, IntegerHelper.NextPowerOf2(1073741825U)); + Assert.AreEqual(2147483648U, IntegerHelper.NextPowerOf2(2147483647U)); + Assert.AreEqual(2147483648U, IntegerHelper.NextPowerOf2(2147483648U)); + } + + /// + /// Verifies that the next power of 2 calculation works for integers + /// + [Test] + public void TestNextPowerOf2Int() { + Assert.AreEqual(1, IntegerHelper.NextPowerOf2(0)); + Assert.AreEqual(1, IntegerHelper.NextPowerOf2(1)); + Assert.AreEqual(2, IntegerHelper.NextPowerOf2(2)); + Assert.AreEqual(4, IntegerHelper.NextPowerOf2(3)); + Assert.AreEqual(4, IntegerHelper.NextPowerOf2(4)); + Assert.AreEqual(1073741824, IntegerHelper.NextPowerOf2(536870913)); + Assert.AreEqual(1073741824, IntegerHelper.NextPowerOf2(1073741823)); + Assert.AreEqual(1073741824, IntegerHelper.NextPowerOf2(1073741824)); + } + + /// Verifies that the bit counting method for integers works + [Test] + public void TestCountBitsInInteger() { + Assert.AreEqual(0, IntegerHelper.CountBits(0)); + Assert.AreEqual(32, IntegerHelper.CountBits(-1)); + Assert.AreEqual(16, IntegerHelper.CountBits(0x55555555)); + Assert.AreEqual(16, IntegerHelper.CountBits(0xAAAAAAAA)); + + for (int bitIndex = 0; bitIndex < 32; ++bitIndex) { + Assert.AreEqual(1, IntegerHelper.CountBits(1 << bitIndex)); + } + } + + /// Verifies that the bit counting method for long integers works + [Test] + public void TestCountBitsInLongInteger() { + Assert.AreEqual(0, IntegerHelper.CountBits(0L)); + Assert.AreEqual(64, IntegerHelper.CountBits(-1L)); + Assert.AreEqual(32, IntegerHelper.CountBits(0x5555555555555555)); + Assert.AreEqual(32, IntegerHelper.CountBits(0xAAAAAAAAAAAAAAAA)); + + for (int bitIndex = 0; bitIndex < 64; ++bitIndex) { + Assert.AreEqual(1, IntegerHelper.CountBits(1 << bitIndex)); + } + } + + } + +} // namespace Nuclex.Support + +#endif // UNITTEST diff --git a/Source/IntegerHelper.cs b/Source/IntegerHelper.cs index e08fad5..05d1235 100644 --- a/Source/IntegerHelper.cs +++ b/Source/IntegerHelper.cs @@ -1,127 +1,126 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; - -namespace Nuclex.Support { - - /// Helper methods for working with integer types - public static class IntegerHelper { - - /// Returns the next highest power of 2 from the specified value - /// Value of which to return the next highest power of 2 - /// The next highest power of 2 to the value - public static long NextPowerOf2(this long value) { - return (long)NextPowerOf2((ulong)value); - } - - /// Returns the next highest power of 2 from the specified value - /// Value of which to return the next highest power of 2 - /// The next highest power of 2 to the value - public static ulong NextPowerOf2(this ulong value) { - if (value == 0) - return 1; - - --value; - value |= value >> 1; - value |= value >> 2; - value |= value >> 4; - value |= value >> 8; - value |= value >> 16; - value |= value >> 32; - ++value; - - return value; - } - - /// Returns the next highest power of 2 from the specified value - /// Value of which to return the next highest power of 2 - /// The next highest power of 2 to the value - public static int NextPowerOf2(this int value) { - return (int)NextPowerOf2((uint)value); - } - - /// Returns the next highest power of 2 from the specified value - /// Value of which to return the next highest power of 2 - /// The next highest power of 2 to the value - public static uint NextPowerOf2(this uint value) { - if (value == 0) - return 1; - - --value; - value |= value >> 1; - value |= value >> 2; - value |= value >> 4; - value |= value >> 8; - value |= value >> 16; - ++value; - - return value; - } - - /// Returns the number of bits set in an integer - /// Value whose bits will be counted - /// The number of bits set in the integer - public static int CountBits(this int value) { - return CountBits((uint)value); - } - - /// Returns the number of bits set in an unsigned integer - /// Value whose bits will be counted - /// The number of bits set in the unsigned integer - /// - /// Based on a trick revealed here: - /// http://stackoverflow.com/questions/109023 - /// - public static int CountBits(this uint value) { - value = value - ((value >> 1) & 0x55555555); - value = (value & 0x33333333) + ((value >> 2) & 0x33333333); - - return (int)unchecked( - ((value + (value >> 4) & 0xF0F0F0F) * 0x1010101) >> 24 - ); - } - - /// Returns the number of bits set in a long integer - /// Value whose bits will be counted - /// The number of bits set in the long integer - public static int CountBits(this long value) { - return CountBits((ulong)value); - } - - /// Returns the number of bits set in an unsigned long integer - /// Value whose bits will be counted - /// The number of bits set in the unsigned long integer - /// - /// Based on a trick revealed here: - /// http://stackoverflow.com/questions/2709430 - /// - public static int CountBits(this ulong value) { - value = value - ((value >> 1) & 0x5555555555555555UL); - value = (value & 0x3333333333333333UL) + ((value >> 2) & 0x3333333333333333UL); - - return (int)unchecked( - (((value + (value >> 4)) & 0xF0F0F0F0F0F0F0FUL) * 0x101010101010101UL) >> 56 - ); - } - - } - -} // namespace Nuclex.Support +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +namespace Nuclex.Support { + + /// Helper methods for working with integer types + public static class IntegerHelper { + + /// Returns the next highest power of 2 from the specified value + /// Value of which to return the next highest power of 2 + /// The next highest power of 2 to the value + public static long NextPowerOf2(this long value) { + return (long)NextPowerOf2((ulong)value); + } + + /// Returns the next highest power of 2 from the specified value + /// Value of which to return the next highest power of 2 + /// The next highest power of 2 to the value + public static ulong NextPowerOf2(this ulong value) { + if (value == 0) + return 1; + + --value; + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + value |= value >> 32; + ++value; + + return value; + } + + /// Returns the next highest power of 2 from the specified value + /// Value of which to return the next highest power of 2 + /// The next highest power of 2 to the value + public static int NextPowerOf2(this int value) { + return (int)NextPowerOf2((uint)value); + } + + /// Returns the next highest power of 2 from the specified value + /// Value of which to return the next highest power of 2 + /// The next highest power of 2 to the value + public static uint NextPowerOf2(this uint value) { + if (value == 0) + return 1; + + --value; + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + ++value; + + return value; + } + + /// Returns the number of bits set in an integer + /// Value whose bits will be counted + /// The number of bits set in the integer + public static int CountBits(this int value) { + return CountBits((uint)value); + } + + /// Returns the number of bits set in an unsigned integer + /// Value whose bits will be counted + /// The number of bits set in the unsigned integer + /// + /// Based on a trick revealed here: + /// http://stackoverflow.com/questions/109023 + /// + public static int CountBits(this uint value) { + value = value - ((value >> 1) & 0x55555555); + value = (value & 0x33333333) + ((value >> 2) & 0x33333333); + + return (int)unchecked( + ((value + (value >> 4) & 0xF0F0F0F) * 0x1010101) >> 24 + ); + } + + /// Returns the number of bits set in a long integer + /// Value whose bits will be counted + /// The number of bits set in the long integer + public static int CountBits(this long value) { + return CountBits((ulong)value); + } + + /// Returns the number of bits set in an unsigned long integer + /// Value whose bits will be counted + /// The number of bits set in the unsigned long integer + /// + /// Based on a trick revealed here: + /// http://stackoverflow.com/questions/2709430 + /// + public static int CountBits(this ulong value) { + value = value - ((value >> 1) & 0x5555555555555555UL); + value = (value & 0x3333333333333333UL) + ((value >> 2) & 0x3333333333333333UL); + + return (int)unchecked( + (((value + (value >> 4)) & 0xF0F0F0F0F0F0F0FUL) * 0x101010101010101UL) >> 56 + ); + } + + } + +} // namespace Nuclex.Support diff --git a/Source/Licensing/LicenseKey.Test.cs b/Source/Licensing/LicenseKey.Test.cs index 769c0ee..50cbea9 100644 --- a/Source/Licensing/LicenseKey.Test.cs +++ b/Source/Licensing/LicenseKey.Test.cs @@ -1,142 +1,141 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections; - -#if UNITTEST - -using NUnit.Framework; - -namespace Nuclex.Support.Licensing { - - /// Unit test for the license key class - [TestFixture] - internal class LicenseKeyTest { - - /// Tests the default constructor of the license key class - [Test] - public void DefaultConstructorCanBeUsed() { - Assert.IsNotNull(new LicenseKey()); // Nonsense, prevents compiler warning - } - - /// Validates the correct translation of keys to GUIDs and back - [Test] - public void LicenseKeysCanBeConvertedToGuidsAndBack() { - for(int i = 0; i < 128; ++i) { - - // Create a new BitArray with the n.th bit set - BitArray guidBits = new BitArray(128); - guidBits[i] = true; - - // Create a GUID from this Bitarray - byte[] guidBytes = new byte[16]; - guidBits.CopyTo(guidBytes, 0); - Guid originalGuid = new Guid(guidBytes); - - // Convert the GUID into a license key and back to a GUID - string licenseKey = new LicenseKey(originalGuid).ToString(); - Guid rebuiltGuid = LicenseKey.Parse(licenseKey).ToGuid(); - - // Verify that the original GUID matches the fore-and-back converted one - Assert.AreEqual(originalGuid, rebuiltGuid, "Test for GUID bit " + i); - - } - } - - /// Tests whether license keys can be modified without destroying them - [Test] - public void LicenseKeysCanBeModified() { - - for(int i = 0; i < 4; ++i) { - for(int j = 0; j < 8; ++j) { - - LicenseKey testKey = new LicenseKey( - new Guid(-1, -1, -1, 255, 255, 255, 255, 255, 255, 255, 255) - ); - - string originalString = testKey.ToString(); - testKey[i] &= ~(1 << j); - string modifiedString = testKey.ToString(); - - Assert.IsTrue( - originalString != modifiedString, "Modified string differs from original" - ); - - testKey[i] |= (1 << j); - string revertedString = testKey.ToString(); - - Assert.AreEqual( - originalString, revertedString, "Original state restorable" - ); - - } // for j - } // for i - - } - - /// Tests whether license keys can be modified without destroying them - [Test] - public void ParsingInvalidLicenseKeyThrowsArgumentException() { - Assert.Throws( - delegate() { LicenseKey.Parse("hello world"); } - ); - } - - /// - /// Tests whether an exception is thrown if the indexer of a license key is used - /// with an invalid index to retrieve a component of the key - /// - [Test] - public void ReadingInvalidIndexThrowsIndexOutOfRangeException() { - LicenseKey key = new LicenseKey(); - Assert.Throws( - delegate() { Console.WriteLine(key[-1]); } - ); - } - - /// - /// Tests whether an exception is thrown if the indexer of a license key is used - /// with an invalid index to set a component of the key - /// - [Test] - public void WritingInvalidIndexThrowsIndexOutOfRangeException() { - LicenseKey key = new LicenseKey(); - Assert.Throws( - delegate() { key[-1] = 0; } - ); - } - - /// - /// Verifies that a license key can be converted into a byte array - /// - [Test] - public void LicenseKeyCanBeConvertedToByteArray() { - Guid someGuid = Guid.NewGuid(); - LicenseKey someKey = new LicenseKey(someGuid); - - CollectionAssert.AreEqual(someGuid.ToByteArray(), someKey.ToByteArray()); - } - - } - -} // namespace Nuclex.Support.Licensing - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections; + +#if UNITTEST + +using NUnit.Framework; + +namespace Nuclex.Support.Licensing { + + /// Unit test for the license key class + [TestFixture] + internal class LicenseKeyTest { + + /// Tests the default constructor of the license key class + [Test] + public void DefaultConstructorCanBeUsed() { + Assert.IsNotNull(new LicenseKey()); // Nonsense, prevents compiler warning + } + + /// Validates the correct translation of keys to GUIDs and back + [Test] + public void LicenseKeysCanBeConvertedToGuidsAndBack() { + for(int i = 0; i < 128; ++i) { + + // Create a new BitArray with the n.th bit set + BitArray guidBits = new BitArray(128); + guidBits[i] = true; + + // Create a GUID from this Bitarray + byte[] guidBytes = new byte[16]; + guidBits.CopyTo(guidBytes, 0); + Guid originalGuid = new Guid(guidBytes); + + // Convert the GUID into a license key and back to a GUID + string licenseKey = new LicenseKey(originalGuid).ToString(); + Guid rebuiltGuid = LicenseKey.Parse(licenseKey).ToGuid(); + + // Verify that the original GUID matches the fore-and-back converted one + Assert.AreEqual(originalGuid, rebuiltGuid, "Test for GUID bit " + i); + + } + } + + /// Tests whether license keys can be modified without destroying them + [Test] + public void LicenseKeysCanBeModified() { + + for(int i = 0; i < 4; ++i) { + for(int j = 0; j < 8; ++j) { + + LicenseKey testKey = new LicenseKey( + new Guid(-1, -1, -1, 255, 255, 255, 255, 255, 255, 255, 255) + ); + + string originalString = testKey.ToString(); + testKey[i] &= ~(1 << j); + string modifiedString = testKey.ToString(); + + Assert.IsTrue( + originalString != modifiedString, "Modified string differs from original" + ); + + testKey[i] |= (1 << j); + string revertedString = testKey.ToString(); + + Assert.AreEqual( + originalString, revertedString, "Original state restorable" + ); + + } // for j + } // for i + + } + + /// Tests whether license keys can be modified without destroying them + [Test] + public void ParsingInvalidLicenseKeyThrowsArgumentException() { + Assert.Throws( + delegate() { LicenseKey.Parse("hello world"); } + ); + } + + /// + /// Tests whether an exception is thrown if the indexer of a license key is used + /// with an invalid index to retrieve a component of the key + /// + [Test] + public void ReadingInvalidIndexThrowsIndexOutOfRangeException() { + LicenseKey key = new LicenseKey(); + Assert.Throws( + delegate() { Console.WriteLine(key[-1]); } + ); + } + + /// + /// Tests whether an exception is thrown if the indexer of a license key is used + /// with an invalid index to set a component of the key + /// + [Test] + public void WritingInvalidIndexThrowsIndexOutOfRangeException() { + LicenseKey key = new LicenseKey(); + Assert.Throws( + delegate() { key[-1] = 0; } + ); + } + + /// + /// Verifies that a license key can be converted into a byte array + /// + [Test] + public void LicenseKeyCanBeConvertedToByteArray() { + Guid someGuid = Guid.NewGuid(); + LicenseKey someKey = new LicenseKey(someGuid); + + CollectionAssert.AreEqual(someGuid.ToByteArray(), someKey.ToByteArray()); + } + + } + +} // namespace Nuclex.Support.Licensing + +#endif // UNITTEST diff --git a/Source/Licensing/LicenseKey.cs b/Source/Licensing/LicenseKey.cs index 8d2ac24..0f04ace 100644 --- a/Source/Licensing/LicenseKey.cs +++ b/Source/Licensing/LicenseKey.cs @@ -1,257 +1,256 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections; -using System.Text; - -namespace Nuclex.Support.Licensing { - - /// Typical license key with 5x5 alphanumerical characters - /// - /// - /// This class manages a license key like it is used in Microsoft products. - /// Althought it is probably not the exact same technique used by Microsoft, - /// the textual representation of the license keys looks identical, - /// eg. O809J-RN5TD-IM3CU-4IG1O-O90X9. - /// - /// - /// Available storage space is used efficiently and allows for up to four - /// 32 bit integers to be stored within the key, that's enough for a full GUID. - /// The four integers can be modified directly, for example to store feature - /// lists, checksums or other data within the key. - /// - /// - public class LicenseKey { - - /// Parses the license key contained in a string - /// String containing a license key that is to be parsed - /// The license key parsed from provided string - /// - /// When the provided string is not a license key - /// - public static LicenseKey Parse(string key) { - key = key.Replace(" ", string.Empty).Replace("-", string.Empty).ToUpper(); - if(key.Length != 25) - throw new ArgumentException("This is not a license key"); - - BitArray bits = new BitArray(128); - uint sequence; - - // Convert the first 4 sequences of 6 chars into 124 bits - for(int j = 0; j < 4; j++) { - - sequence = - (uint)codeTable.IndexOf(key[j * 6 + 5]) * 60466176 + - (uint)codeTable.IndexOf(key[j * 6 + 4]) * 1679616 + - (uint)codeTable.IndexOf(key[j * 6 + 3]) * 46656 + - (uint)codeTable.IndexOf(key[j * 6 + 2]) * 1296 + - (uint)codeTable.IndexOf(key[j * 6 + 1]) * 36 + - (uint)codeTable.IndexOf(key[j * 6 + 0]); - - for(int i = 0; i < 31; i++) - bits[j * 31 + i] = (sequence & powersOfTwo[i, 1]) != 0; - - } - - // Append the remaining character's 4 bits - sequence = (uint)codeTable.IndexOf(key[24]); - bits[124] = (sequence & powersOfTwo[4, 1]) != 0; - bits[125] = (sequence & powersOfTwo[3, 1]) != 0; - bits[126] = (sequence & powersOfTwo[2, 1]) != 0; - bits[127] = (sequence & powersOfTwo[1, 1]) != 0; - - // Revert the mangling that was applied to the key when encoding... - unmangle(bits); - - // ...and we've got our GUID back! - byte[] guidBytes = new byte[16]; - bits.CopyTo(guidBytes, 0); - - return new LicenseKey(new Guid(guidBytes)); - } - - /// Initializes a new, empty license key - public LicenseKey() : this(Guid.Empty) { } - - /// Initializes the license key from a GUID - /// GUID that is used to create the license key - public LicenseKey(Guid source) { - this.guid = source; - } - - /// Accesses the four integer values within a license key - /// - /// When the index lies outside of the key's fields - /// - public int this[int index] { - get { - if((index < 0) || (index > 3)) - throw new IndexOutOfRangeException("Index out of range"); - - return BitConverter.ToInt32(this.guid.ToByteArray(), index * 4); - } - set { - if((index < 0) || (index > 3)) - throw new IndexOutOfRangeException("Index out of range"); - - // Convert the GUID into binary data so we can replace one of its values - byte[] guidBytes = this.guid.ToByteArray(); - - // Overwrite the section at the index specified by the user with the new value - Array.Copy( - BitConverter.GetBytes(value), 0, // source and start index - guidBytes, index * 4, // destination and start index - 4 // length - ); - - // Replacement finished, now we can reconstruct our guid - this.guid = new Guid(guidBytes); - } - } - - /// Converts the license key into a GUID - /// The GUID created from the license key - public Guid ToGuid() { - return this.guid; - } - - /// Converts the license key into a byte array - /// A byte array containing the converted license key - public byte[] ToByteArray() { - return this.guid.ToByteArray(); - } - - /// Converts the license key to a string - /// A string containing the converted license key - public override string ToString() { - StringBuilder resultBuilder = new StringBuilder(); - - // Build a bit array from the input data - BitArray bits = new BitArray(this.guid.ToByteArray()); - mangle(bits); - - int sequence = 0; - - // Build 4 sequences of 6 characters from the first 124 bits - for(int i = 0; i < 4; ++i) { - - // We take the next 31 bits from the buffer - for(int j = 0; j < 31; ++j) - sequence |= (int)powersOfTwo[j, bits[i * 31 + j] ? 1 : 0]; - - // Using 31 bits, a number up to 2.147.483.648 can be represented, - // while 6 alpha-numerical characters allow for 2.176.782.336 possible values, - // which means we can fit 31 bits into every 6 alpha-numerical characters. - for(int j = 0; j < 6; ++j) { - resultBuilder.Append(codeTable[sequence % 36]); - sequence /= 36; - } - - } - - // Use the remaining 4 bits to build the final character - resultBuilder.Append( - codeTable[ - (int)( - powersOfTwo[4, bits[124] ? 1 : 0] | - powersOfTwo[3, bits[125] ? 1 : 0] | - powersOfTwo[2, bits[126] ? 1 : 0] | - powersOfTwo[1, bits[127] ? 1 : 0] | - powersOfTwo[0, 1] // One bit remains unused :) - ) - ] - ); - - // Now build a nice, readable string from the decoded characters - resultBuilder.Insert(5, keyDelimiter, 0, 1); - resultBuilder.Insert(11, keyDelimiter, 0, 1); - resultBuilder.Insert(17, keyDelimiter, 0, 1); - resultBuilder.Insert(23, keyDelimiter, 0, 1); - return resultBuilder.ToString(); - } - - /// Mangles a bit array - /// Bit array that will be mangled - private static void mangle(BitArray bits) { - BitArray temp = new BitArray(bits); - - for(int i = 0; i < temp.Length; ++i) { - bits[i] = temp[shuffle[i]]; - - if((i & 1) != 0) - bits[i] = !bits[i]; - } - } - - /// Unmangles a bit array - /// Bit array that will be unmangled - private static void unmangle(BitArray bits) { - BitArray temp = new BitArray(bits); - - for(int i = 0; i < temp.Length; ++i) { - if((i & 1) != 0) - temp[i] = !temp[i]; - - bits[shuffle[i]] = temp[i]; - } - } - - /// Character used to delimit each 5 digit group in a license key - /// - /// Required to be a char array because the .NET Compact Framework only provides - /// an overload for char[] in the StringBuilder.Insert() method. - /// - private static char[] keyDelimiter = new char[] { '-' }; - - /// Table with the individual characters in a key - private static readonly string codeTable = - "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - - /// Helper array containing the precalculated powers of two - private static readonly uint[,] powersOfTwo = new uint[32, 2] { - { 0, 1 }, { 0, 2 }, { 0, 4 }, { 0, 8 }, - { 0, 16 }, { 0, 32 }, { 0, 64 }, { 0, 128 }, - { 0, 256 }, { 0, 512 }, { 0, 1024 }, { 0, 2048 }, - { 0, 4096 }, { 0, 8192 }, { 0, 16384 }, { 0, 32768 }, - { 0, 65536 }, { 0, 131072 }, { 0, 262144 }, { 0, 524288 }, - { 0, 1048576 }, { 0, 2097152 }, { 0, 4194304 }, { 0, 8388608 }, - { 0, 16777216 }, { 0, 33554432 }, { 0, 67108864 }, { 0, 134217728 }, - { 0, 268435456 }, { 0, 536870912 }, { 0, 1073741824 }, { 0, 2147483648 } - }; - - /// Index list for rotating the bit arrays - private static readonly byte[] shuffle = new byte[128] { - 99, 47, 19, 104, 40, 71, 35, 82, 88, 2, 117, 118, 105, 42, 84, 48, - 33, 54, 43, 27, 78, 53, 61, 50, 109, 87, 69, 66, 25, 76, 45, 14, - 92, 16, 123, 98, 95, 37, 34, 8, 1, 49, 20, 90, 15, 97, 22, 108, - 5, 32, 120, 106, 122, 70, 67, 55, 46, 89, 100, 0, 26, 94, 121, 7, - 56, 59, 103, 79, 107, 36, 125, 119, 126, 44, 18, 93, 75, 116, 31, 9, - 73, 113, 3, 41, 124, 60, 77, 91, 28, 114, 65, 12, 39, 127, 72, 17, - 112, 21, 96, 111, 83, 101, 85, 80, 23, 68, 57, 13, 4, 10, 51, 63, - 11, 30, 115, 102, 86, 81, 74, 110, 62, 38, 29, 64, 52, 6, 24, 58 - }; - - /// GUID in which the key is stored - private Guid guid; - - } - -} // namespace Nuclex.Support.Licensing +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections; +using System.Text; + +namespace Nuclex.Support.Licensing { + + /// Typical license key with 5x5 alphanumerical characters + /// + /// + /// This class manages a license key like it is used in Microsoft products. + /// Althought it is probably not the exact same technique used by Microsoft, + /// the textual representation of the license keys looks identical, + /// eg. O809J-RN5TD-IM3CU-4IG1O-O90X9. + /// + /// + /// Available storage space is used efficiently and allows for up to four + /// 32 bit integers to be stored within the key, that's enough for a full GUID. + /// The four integers can be modified directly, for example to store feature + /// lists, checksums or other data within the key. + /// + /// + public class LicenseKey { + + /// Parses the license key contained in a string + /// String containing a license key that is to be parsed + /// The license key parsed from provided string + /// + /// When the provided string is not a license key + /// + public static LicenseKey Parse(string key) { + key = key.Replace(" ", string.Empty).Replace("-", string.Empty).ToUpper(); + if(key.Length != 25) + throw new ArgumentException("This is not a license key"); + + BitArray bits = new BitArray(128); + uint sequence; + + // Convert the first 4 sequences of 6 chars into 124 bits + for(int j = 0; j < 4; j++) { + + sequence = + (uint)codeTable.IndexOf(key[j * 6 + 5]) * 60466176 + + (uint)codeTable.IndexOf(key[j * 6 + 4]) * 1679616 + + (uint)codeTable.IndexOf(key[j * 6 + 3]) * 46656 + + (uint)codeTable.IndexOf(key[j * 6 + 2]) * 1296 + + (uint)codeTable.IndexOf(key[j * 6 + 1]) * 36 + + (uint)codeTable.IndexOf(key[j * 6 + 0]); + + for(int i = 0; i < 31; i++) + bits[j * 31 + i] = (sequence & powersOfTwo[i, 1]) != 0; + + } + + // Append the remaining character's 4 bits + sequence = (uint)codeTable.IndexOf(key[24]); + bits[124] = (sequence & powersOfTwo[4, 1]) != 0; + bits[125] = (sequence & powersOfTwo[3, 1]) != 0; + bits[126] = (sequence & powersOfTwo[2, 1]) != 0; + bits[127] = (sequence & powersOfTwo[1, 1]) != 0; + + // Revert the mangling that was applied to the key when encoding... + unmangle(bits); + + // ...and we've got our GUID back! + byte[] guidBytes = new byte[16]; + bits.CopyTo(guidBytes, 0); + + return new LicenseKey(new Guid(guidBytes)); + } + + /// Initializes a new, empty license key + public LicenseKey() : this(Guid.Empty) { } + + /// Initializes the license key from a GUID + /// GUID that is used to create the license key + public LicenseKey(Guid source) { + this.guid = source; + } + + /// Accesses the four integer values within a license key + /// + /// When the index lies outside of the key's fields + /// + public int this[int index] { + get { + if((index < 0) || (index > 3)) + throw new IndexOutOfRangeException("Index out of range"); + + return BitConverter.ToInt32(this.guid.ToByteArray(), index * 4); + } + set { + if((index < 0) || (index > 3)) + throw new IndexOutOfRangeException("Index out of range"); + + // Convert the GUID into binary data so we can replace one of its values + byte[] guidBytes = this.guid.ToByteArray(); + + // Overwrite the section at the index specified by the user with the new value + Array.Copy( + BitConverter.GetBytes(value), 0, // source and start index + guidBytes, index * 4, // destination and start index + 4 // length + ); + + // Replacement finished, now we can reconstruct our guid + this.guid = new Guid(guidBytes); + } + } + + /// Converts the license key into a GUID + /// The GUID created from the license key + public Guid ToGuid() { + return this.guid; + } + + /// Converts the license key into a byte array + /// A byte array containing the converted license key + public byte[] ToByteArray() { + return this.guid.ToByteArray(); + } + + /// Converts the license key to a string + /// A string containing the converted license key + public override string ToString() { + StringBuilder resultBuilder = new StringBuilder(); + + // Build a bit array from the input data + BitArray bits = new BitArray(this.guid.ToByteArray()); + mangle(bits); + + int sequence = 0; + + // Build 4 sequences of 6 characters from the first 124 bits + for(int i = 0; i < 4; ++i) { + + // We take the next 31 bits from the buffer + for(int j = 0; j < 31; ++j) + sequence |= (int)powersOfTwo[j, bits[i * 31 + j] ? 1 : 0]; + + // Using 31 bits, a number up to 2.147.483.648 can be represented, + // while 6 alpha-numerical characters allow for 2.176.782.336 possible values, + // which means we can fit 31 bits into every 6 alpha-numerical characters. + for(int j = 0; j < 6; ++j) { + resultBuilder.Append(codeTable[sequence % 36]); + sequence /= 36; + } + + } + + // Use the remaining 4 bits to build the final character + resultBuilder.Append( + codeTable[ + (int)( + powersOfTwo[4, bits[124] ? 1 : 0] | + powersOfTwo[3, bits[125] ? 1 : 0] | + powersOfTwo[2, bits[126] ? 1 : 0] | + powersOfTwo[1, bits[127] ? 1 : 0] | + powersOfTwo[0, 1] // One bit remains unused :) + ) + ] + ); + + // Now build a nice, readable string from the decoded characters + resultBuilder.Insert(5, keyDelimiter, 0, 1); + resultBuilder.Insert(11, keyDelimiter, 0, 1); + resultBuilder.Insert(17, keyDelimiter, 0, 1); + resultBuilder.Insert(23, keyDelimiter, 0, 1); + return resultBuilder.ToString(); + } + + /// Mangles a bit array + /// Bit array that will be mangled + private static void mangle(BitArray bits) { + BitArray temp = new BitArray(bits); + + for(int i = 0; i < temp.Length; ++i) { + bits[i] = temp[shuffle[i]]; + + if((i & 1) != 0) + bits[i] = !bits[i]; + } + } + + /// Unmangles a bit array + /// Bit array that will be unmangled + private static void unmangle(BitArray bits) { + BitArray temp = new BitArray(bits); + + for(int i = 0; i < temp.Length; ++i) { + if((i & 1) != 0) + temp[i] = !temp[i]; + + bits[shuffle[i]] = temp[i]; + } + } + + /// Character used to delimit each 5 digit group in a license key + /// + /// Required to be a char array because the .NET Compact Framework only provides + /// an overload for char[] in the StringBuilder.Insert() method. + /// + private static char[] keyDelimiter = new char[] { '-' }; + + /// Table with the individual characters in a key + private static readonly string codeTable = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + /// Helper array containing the precalculated powers of two + private static readonly uint[,] powersOfTwo = new uint[32, 2] { + { 0, 1 }, { 0, 2 }, { 0, 4 }, { 0, 8 }, + { 0, 16 }, { 0, 32 }, { 0, 64 }, { 0, 128 }, + { 0, 256 }, { 0, 512 }, { 0, 1024 }, { 0, 2048 }, + { 0, 4096 }, { 0, 8192 }, { 0, 16384 }, { 0, 32768 }, + { 0, 65536 }, { 0, 131072 }, { 0, 262144 }, { 0, 524288 }, + { 0, 1048576 }, { 0, 2097152 }, { 0, 4194304 }, { 0, 8388608 }, + { 0, 16777216 }, { 0, 33554432 }, { 0, 67108864 }, { 0, 134217728 }, + { 0, 268435456 }, { 0, 536870912 }, { 0, 1073741824 }, { 0, 2147483648 } + }; + + /// Index list for rotating the bit arrays + private static readonly byte[] shuffle = new byte[128] { + 99, 47, 19, 104, 40, 71, 35, 82, 88, 2, 117, 118, 105, 42, 84, 48, + 33, 54, 43, 27, 78, 53, 61, 50, 109, 87, 69, 66, 25, 76, 45, 14, + 92, 16, 123, 98, 95, 37, 34, 8, 1, 49, 20, 90, 15, 97, 22, 108, + 5, 32, 120, 106, 122, 70, 67, 55, 46, 89, 100, 0, 26, 94, 121, 7, + 56, 59, 103, 79, 107, 36, 125, 119, 126, 44, 18, 93, 75, 116, 31, 9, + 73, 113, 3, 41, 124, 60, 77, 91, 28, 114, 65, 12, 39, 127, 72, 17, + 112, 21, 96, 111, 83, 101, 85, 80, 23, 68, 57, 13, 4, 10, 51, 63, + 11, 30, 115, 102, 86, 81, 74, 110, 62, 38, 29, 64, 52, 6, 24, 58 + }; + + /// GUID in which the key is stored + private Guid guid; + + } + +} // namespace Nuclex.Support.Licensing diff --git a/Source/Observable.Test.cs b/Source/Observable.Test.cs index eaa3a89..dd31bd5 100644 --- a/Source/Observable.Test.cs +++ b/Source/Observable.Test.cs @@ -1,172 +1,171 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.ComponentModel; - -using NUnit.Framework; -using NMock; - -namespace Nuclex.Support { - - /// Unit tests for observable class - [TestFixture] - internal class ObservableTest { - - #region class TestObservable - - /// Example class on which unit test generates change notifications - public class TestObservable : Observable { - - /// Triggers the property changed event for the specified property - /// - /// Name of the property that will be reported as changed - /// - public void FirePropertyChanged(string propertyName) { - OnPropertyChanged(propertyName); - } - - /// Fires the property changed event for the 'SomePropety' property - public void FireSomePropertyChanged() { - #pragma warning disable 0618 - OnPropertyChanged(() => SomeProperty); - #pragma warning restore 0618 - } - - /// Example property that will be reported to have changed - public int SomeProperty { get; set; } - - } - - #endregion // class TestObservable - - #region class MockedSubscriber - - /// Mocked change notification subscriber - public class MockedSubscriber { - - /// Called when the value of a property has changed - /// Object of which a property has changed - /// Contains the name of the changed property - public void PropertyChanged(object sender, PropertyChangedEventArgs arguments) { - this.wasNotified = true; - this.changedPropertyName = arguments.PropertyName; - } - - /// Whether the subscriber was notified of a property change - public bool WasNotified { - get { return this.wasNotified; } - } - - /// - /// Checks whether a change notification for the specified property was received - /// - /// Name of the property that will be checked for - /// - /// True if a change notification for the specified property was received - /// - public bool WasNotifiedOfChangeTo(string propertyName) { - if(!this.wasNotified) { - return false; - } - - if(string.IsNullOrEmpty(propertyName)) { - return string.IsNullOrEmpty(this.changedPropertyName); - } - - return (propertyName == this.changedPropertyName); - } - - /// Whether a change notification was received - private bool wasNotified; - /// Name of the property for which a change notification was received - private string changedPropertyName; - - } - - #endregion // class MockedSubscriber - - /// Called before each unit test is run - [SetUp] - public void Setup() { - this.testObservable = new TestObservable(); - this.subscriber = new MockedSubscriber(); - - this.testObservable.PropertyChanged += this.subscriber.PropertyChanged; - } - - /// - /// Verifies that the name of the changed property can be specified manually - /// when triggering the PropertyChanged event - /// - [Test] - public void PropertyNameCanBeSpecifiedManually() { - this.testObservable.FirePropertyChanged("SomeProperty"); - Assert.IsTrue(this.subscriber.WasNotifiedOfChangeTo("SomeProperty")); - } - -#if DEBUG // The check is conditionally performed only in debug mode - /// - /// Verifies that specifying the name of a property that doesn't exist - /// causes an ArgumentException to be thrown - /// - [Test] - public void SpecifyingInvalidPropertyNameThrowsArgumentException() { - Assert.Throws( - delegate() { this.testObservable.FirePropertyChanged("DoesntExist"); } - ); - } -#endif - - /// - /// Verifies that the observable is capable of deducing the name of the property - /// from a lambda expression - /// - [Test] - public void PropertyNameCanBeDeducedFromLambdaExpression() { - this.testObservable.FireSomePropertyChanged(); - Assert.IsTrue(this.subscriber.WasNotifiedOfChangeTo("SomeProperty")); - } - - /// - /// Verifies that change notifications for all properties of a type can - /// be generated - /// - [Test] - public void WildcardChangeNotificationsCanBeSent() { - this.testObservable.FirePropertyChanged(string.Empty); - Assert.IsTrue(this.subscriber.WasNotifiedOfChangeTo(null)); - - this.testObservable.FirePropertyChanged(null); - Assert.IsTrue(this.subscriber.WasNotifiedOfChangeTo(string.Empty)); - } - - /// Observable object being tested - private TestObservable testObservable; - /// Subscriber to the observable object being tested - private MockedSubscriber subscriber; - - } - -} // namespace Nuclex.Support - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.ComponentModel; + +using NUnit.Framework; +using NMock; + +namespace Nuclex.Support { + + /// Unit tests for observable class + [TestFixture] + internal class ObservableTest { + + #region class TestObservable + + /// Example class on which unit test generates change notifications + public class TestObservable : Observable { + + /// Triggers the property changed event for the specified property + /// + /// Name of the property that will be reported as changed + /// + public void FirePropertyChanged(string propertyName) { + OnPropertyChanged(propertyName); + } + + /// Fires the property changed event for the 'SomePropety' property + public void FireSomePropertyChanged() { + #pragma warning disable 0618 + OnPropertyChanged(() => SomeProperty); + #pragma warning restore 0618 + } + + /// Example property that will be reported to have changed + public int SomeProperty { get; set; } + + } + + #endregion // class TestObservable + + #region class MockedSubscriber + + /// Mocked change notification subscriber + public class MockedSubscriber { + + /// Called when the value of a property has changed + /// Object of which a property has changed + /// Contains the name of the changed property + public void PropertyChanged(object sender, PropertyChangedEventArgs arguments) { + this.wasNotified = true; + this.changedPropertyName = arguments.PropertyName; + } + + /// Whether the subscriber was notified of a property change + public bool WasNotified { + get { return this.wasNotified; } + } + + /// + /// Checks whether a change notification for the specified property was received + /// + /// Name of the property that will be checked for + /// + /// True if a change notification for the specified property was received + /// + public bool WasNotifiedOfChangeTo(string propertyName) { + if(!this.wasNotified) { + return false; + } + + if(string.IsNullOrEmpty(propertyName)) { + return string.IsNullOrEmpty(this.changedPropertyName); + } + + return (propertyName == this.changedPropertyName); + } + + /// Whether a change notification was received + private bool wasNotified; + /// Name of the property for which a change notification was received + private string changedPropertyName; + + } + + #endregion // class MockedSubscriber + + /// Called before each unit test is run + [SetUp] + public void Setup() { + this.testObservable = new TestObservable(); + this.subscriber = new MockedSubscriber(); + + this.testObservable.PropertyChanged += this.subscriber.PropertyChanged; + } + + /// + /// Verifies that the name of the changed property can be specified manually + /// when triggering the PropertyChanged event + /// + [Test] + public void PropertyNameCanBeSpecifiedManually() { + this.testObservable.FirePropertyChanged("SomeProperty"); + Assert.IsTrue(this.subscriber.WasNotifiedOfChangeTo("SomeProperty")); + } + +#if DEBUG // The check is conditionally performed only in debug mode + /// + /// Verifies that specifying the name of a property that doesn't exist + /// causes an ArgumentException to be thrown + /// + [Test] + public void SpecifyingInvalidPropertyNameThrowsArgumentException() { + Assert.Throws( + delegate() { this.testObservable.FirePropertyChanged("DoesntExist"); } + ); + } +#endif + + /// + /// Verifies that the observable is capable of deducing the name of the property + /// from a lambda expression + /// + [Test] + public void PropertyNameCanBeDeducedFromLambdaExpression() { + this.testObservable.FireSomePropertyChanged(); + Assert.IsTrue(this.subscriber.WasNotifiedOfChangeTo("SomeProperty")); + } + + /// + /// Verifies that change notifications for all properties of a type can + /// be generated + /// + [Test] + public void WildcardChangeNotificationsCanBeSent() { + this.testObservable.FirePropertyChanged(string.Empty); + Assert.IsTrue(this.subscriber.WasNotifiedOfChangeTo(null)); + + this.testObservable.FirePropertyChanged(null); + Assert.IsTrue(this.subscriber.WasNotifiedOfChangeTo(string.Empty)); + } + + /// Observable object being tested + private TestObservable testObservable; + /// Subscriber to the observable object being tested + private MockedSubscriber subscriber; + + } + +} // namespace Nuclex.Support + +#endif // UNITTEST diff --git a/Source/Observable.cs b/Source/Observable.cs index 5a1a76a..2c5a3e7 100644 --- a/Source/Observable.cs +++ b/Source/Observable.cs @@ -1,134 +1,133 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.ComponentModel; -using System.Diagnostics; -#if !NO_LINQ_EXPRESSIONS -using System.Linq.Expressions; -#endif -using System.Reflection; - -namespace Nuclex.Support { - - /// Base class for objects that support property change notifications -#if !NO_SERIALIZATION - [Serializable] -#endif - public abstract class Observable : INotifyPropertyChanged { - - /// Raised when a property of the instance has changed its value -#if !NO_SERIALIZATION - [field: NonSerialized] -#endif - public event PropertyChangedEventHandler PropertyChanged; - -#if !NO_LINQ_EXPRESSIONS - /// Triggers the PropertyChanged event for the specified property - /// - /// Lambda expression for the property that will be reported to have changed - /// - /// - /// - /// This notification should be fired post-change, i.e. when the property has - /// already changed its value. - /// - /// - /// - /// public int Limit { - /// get { return this.limit; } - /// set { - /// if(value != this.limit) { - /// this.limit = value; - /// OnPropertyChanged(() => Limit); - /// } - /// } - /// } - /// - /// - /// - [Obsolete("Prefer the C# 'nameof()' operator to using a Linq expression")] - protected void OnPropertyChanged(Expression> property) { - PropertyChangedEventHandler copy = PropertyChanged; - if(copy != null) { - copy(this, PropertyChangedEventArgsHelper.GetArgumentsFor(property)); - } - } -#endif // !NO_LINQ_EXPRESSIONS - - /// Triggers the PropertyChanged event for the specified property - /// Name of the property that has changed its value - /// - /// - /// This notification should be fired post-change, i.e. when the property has - /// already changed its value. If possible, use the other overload of this - /// method to ensure the property name will be updated during F2 refactoring. - /// - /// - /// - /// public int Limit { - /// get { return this.limit; } - /// set { - /// if(value != this.limit) { - /// this.limit = value; - /// OnPropertyChanged("Limit"); // Note: prefer lambda exp whenever possible - /// } - /// } - /// } - /// - /// - /// - protected virtual void OnPropertyChanged(string propertyName) { - enforceChangedPropertyExists(propertyName); - - PropertyChangedEventHandler copy = PropertyChanged; - if(copy != null) { - copy(this, PropertyChangedEventArgsHelper.GetArgumentsFor(propertyName)); - } - } - - /// Ensures that a property with the specified name exists in the type - /// Property name that will be checked - [Conditional("DEBUG")] - private void enforceChangedPropertyExists(string propertyName) { - - // An empty string or null indicates that all properties have changed - if(string.IsNullOrEmpty(propertyName)) { - return; - } - - // Any other string needs to match a property name - PropertyInfo property = GetType().GetProperty(propertyName); - if(property == null) { - throw new ArgumentException( - string.Format( - "Type '{0}' tried to raise a change notification for property '{1}', " + - "but no such property exists!", - GetType().Name, propertyName - ), - "propertyName" - ); - } - - } - - } - -} // namespace Nuclex.Support +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.ComponentModel; +using System.Diagnostics; +#if !NO_LINQ_EXPRESSIONS +using System.Linq.Expressions; +#endif +using System.Reflection; + +namespace Nuclex.Support { + + /// Base class for objects that support property change notifications +#if !NO_SERIALIZATION + [Serializable] +#endif + public abstract class Observable : INotifyPropertyChanged { + + /// Raised when a property of the instance has changed its value +#if !NO_SERIALIZATION + [field: NonSerialized] +#endif + public event PropertyChangedEventHandler PropertyChanged; + +#if !NO_LINQ_EXPRESSIONS + /// Triggers the PropertyChanged event for the specified property + /// + /// Lambda expression for the property that will be reported to have changed + /// + /// + /// + /// This notification should be fired post-change, i.e. when the property has + /// already changed its value. + /// + /// + /// + /// public int Limit { + /// get { return this.limit; } + /// set { + /// if(value != this.limit) { + /// this.limit = value; + /// OnPropertyChanged(() => Limit); + /// } + /// } + /// } + /// + /// + /// + [Obsolete("Prefer the C# 'nameof()' operator to using a Linq expression")] + protected void OnPropertyChanged(Expression> property) { + PropertyChangedEventHandler copy = PropertyChanged; + if(copy != null) { + copy(this, PropertyChangedEventArgsHelper.GetArgumentsFor(property)); + } + } +#endif // !NO_LINQ_EXPRESSIONS + + /// Triggers the PropertyChanged event for the specified property + /// Name of the property that has changed its value + /// + /// + /// This notification should be fired post-change, i.e. when the property has + /// already changed its value. If possible, use the other overload of this + /// method to ensure the property name will be updated during F2 refactoring. + /// + /// + /// + /// public int Limit { + /// get { return this.limit; } + /// set { + /// if(value != this.limit) { + /// this.limit = value; + /// OnPropertyChanged("Limit"); // Note: prefer lambda exp whenever possible + /// } + /// } + /// } + /// + /// + /// + protected virtual void OnPropertyChanged(string propertyName) { + enforceChangedPropertyExists(propertyName); + + PropertyChangedEventHandler copy = PropertyChanged; + if(copy != null) { + copy(this, PropertyChangedEventArgsHelper.GetArgumentsFor(propertyName)); + } + } + + /// Ensures that a property with the specified name exists in the type + /// Property name that will be checked + [Conditional("DEBUG")] + private void enforceChangedPropertyExists(string propertyName) { + + // An empty string or null indicates that all properties have changed + if(string.IsNullOrEmpty(propertyName)) { + return; + } + + // Any other string needs to match a property name + PropertyInfo property = GetType().GetProperty(propertyName); + if(property == null) { + throw new ArgumentException( + string.Format( + "Type '{0}' tried to raise a change notification for property '{1}', " + + "but no such property exists!", + GetType().Name, propertyName + ), + "propertyName" + ); + } + + } + + } + +} // namespace Nuclex.Support diff --git a/Source/ObservableHelper.Test.cs b/Source/ObservableHelper.Test.cs index 5137faf..c574633 100644 --- a/Source/ObservableHelper.Test.cs +++ b/Source/ObservableHelper.Test.cs @@ -1,91 +1,90 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; - -using NUnit.Framework; - -namespace Nuclex.Support { - - /// Unit tests for the observable helper - [TestFixture] - internal class ObservableHelperTest { - - #region class TestReferenceType - - /// Example class on which unit test generates change notifications - public class TestReferenceType { - - /// Example property that will be reported to have changed - public int SomeProperty { get; set; } - - } - - #endregion // class TestReferenceType - - #region struct TestValueType - - /// Example class on which unit test generates change notifications - public struct TestValueType { - - /// Example property that will be reported to have changed - public int SomeProperty { get; set; } - - } - - #endregion // struct TestValueType - - /// - /// Verifies that the name of a property accessed in a lambda expression - /// can be obtained. - /// - [Test] - public void CanObtainPropertyNameFromLambdaExpression() { - string propertyName = ObservableHelper.GetPropertyName( - () => SomeReferenceType.SomeProperty - ); - Assert.AreEqual("SomeProperty", propertyName); - } - - /// - /// Verifies that the name of a property assigned in a lambda expression - /// can be obtained. - /// - [Test] - public void CanObtainPropertyNameFromBoxedLambdaExpression() { - string propertyName = ObservableHelper.GetPropertyName( - () => (object)(SomeValueType.SomeProperty) - ); - Assert.AreEqual("SomeProperty", propertyName); - } - - /// Helper used to construct lambda expressions - protected static TestReferenceType SomeReferenceType { get; set; } - - /// Helper used to construct lambda expressions - protected static TestValueType SomeValueType { get; set; } - - } - -} - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; + +using NUnit.Framework; + +namespace Nuclex.Support { + + /// Unit tests for the observable helper + [TestFixture] + internal class ObservableHelperTest { + + #region class TestReferenceType + + /// Example class on which unit test generates change notifications + public class TestReferenceType { + + /// Example property that will be reported to have changed + public int SomeProperty { get; set; } + + } + + #endregion // class TestReferenceType + + #region struct TestValueType + + /// Example class on which unit test generates change notifications + public struct TestValueType { + + /// Example property that will be reported to have changed + public int SomeProperty { get; set; } + + } + + #endregion // struct TestValueType + + /// + /// Verifies that the name of a property accessed in a lambda expression + /// can be obtained. + /// + [Test] + public void CanObtainPropertyNameFromLambdaExpression() { + string propertyName = ObservableHelper.GetPropertyName( + () => SomeReferenceType.SomeProperty + ); + Assert.AreEqual("SomeProperty", propertyName); + } + + /// + /// Verifies that the name of a property assigned in a lambda expression + /// can be obtained. + /// + [Test] + public void CanObtainPropertyNameFromBoxedLambdaExpression() { + string propertyName = ObservableHelper.GetPropertyName( + () => (object)(SomeValueType.SomeProperty) + ); + Assert.AreEqual("SomeProperty", propertyName); + } + + /// Helper used to construct lambda expressions + protected static TestReferenceType SomeReferenceType { get; set; } + + /// Helper used to construct lambda expressions + protected static TestValueType SomeValueType { get; set; } + + } + +} + +#endif // UNITTEST diff --git a/Source/ObservableHelper.cs b/Source/ObservableHelper.cs index e1c4019..56629a2 100644 --- a/Source/ObservableHelper.cs +++ b/Source/ObservableHelper.cs @@ -1,70 +1,69 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -#if !NO_LINQ_EXPRESSIONS -using System.Linq.Expressions; -#endif - -namespace Nuclex.Support { - - /// Contains helper methods for observing property changed - public static class ObservableHelper { - -#if !NO_LINQ_EXPRESSIONS - /// Obtains the name of a property from a lambda expression - /// - /// Lambda expression for the property whose name will be returned - /// - /// The name of the property contained in the lamba expression - /// - /// - /// This method obtains the textual name of a property specified in a lambda - /// expression. By going through a lambda expression, the property will be - /// stated as actual code, allowing F2 refactoring to correctly update any - /// references to the property when it is renamed. - /// - /// - /// - /// string propertyName = ObservableHelper.GetPropertyName(() => SomeValue); - /// Assert.AreEqual("SomeValue", propertyName); - /// - /// - /// - public static string GetPropertyName(Expression> property) { - var lambda = (LambdaExpression)property; - - MemberExpression memberExpression; - { - var unaryExpression = lambda.Body as UnaryExpression; - if(unaryExpression != null) { - memberExpression = (MemberExpression)unaryExpression.Operand; - } else { - memberExpression = (MemberExpression)lambda.Body; - } - } - - return memberExpression.Member.Name; - } -#endif // !NO_LINQ_EXPRESSIONS - - } - -} // namespace Nuclex.Support +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +#if !NO_LINQ_EXPRESSIONS +using System.Linq.Expressions; +#endif + +namespace Nuclex.Support { + + /// Contains helper methods for observing property changed + public static class ObservableHelper { + +#if !NO_LINQ_EXPRESSIONS + /// Obtains the name of a property from a lambda expression + /// + /// Lambda expression for the property whose name will be returned + /// + /// The name of the property contained in the lamba expression + /// + /// + /// This method obtains the textual name of a property specified in a lambda + /// expression. By going through a lambda expression, the property will be + /// stated as actual code, allowing F2 refactoring to correctly update any + /// references to the property when it is renamed. + /// + /// + /// + /// string propertyName = ObservableHelper.GetPropertyName(() => SomeValue); + /// Assert.AreEqual("SomeValue", propertyName); + /// + /// + /// + public static string GetPropertyName(Expression> property) { + var lambda = (LambdaExpression)property; + + MemberExpression memberExpression; + { + var unaryExpression = lambda.Body as UnaryExpression; + if(unaryExpression != null) { + memberExpression = (MemberExpression)unaryExpression.Operand; + } else { + memberExpression = (MemberExpression)lambda.Body; + } + } + + return memberExpression.Member.Name; + } +#endif // !NO_LINQ_EXPRESSIONS + + } + +} // namespace Nuclex.Support diff --git a/Source/Parsing/CommandLine.Argument.cs b/Source/Parsing/CommandLine.Argument.cs index 96076c9..8feaba8 100644 --- a/Source/Parsing/CommandLine.Argument.cs +++ b/Source/Parsing/CommandLine.Argument.cs @@ -1,164 +1,163 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; - -namespace Nuclex.Support.Parsing { - - partial class CommandLine { - - /// Argument being specified on an application's command line - public class Argument { - - /// Initializes a new option with only a name - /// - /// String segment with the entire argument as it was given on the command line - /// - /// Absolute index the argument name starts at - /// Number of characters in the option name - /// The newly created option - internal static Argument OptionOnly( - StringSegment raw, - int nameStart, int nameLength - ) { - return new Argument(raw, nameStart, nameLength, -1, -1); - } - - /// Initializes a new argument with only a value - /// - /// String segment with the entire argument as it was given on the command line - /// - /// Absolute index the value starts at - /// Number of characters in the value - /// The newly created option - internal static Argument ValueOnly( - StringSegment raw, - int valueStart, int valueLength - ) { - return new Argument(raw, -1, -1, valueStart, valueLength); - } - - /// Creates a new option with a name and an assigned value - /// - /// String segment containing the entire option as it was given on the command line - /// - /// Absolute index the option name starts at - /// Number of characters in the option name - /// Absolute index the value starts at - /// Number of characters in the value - /// The newly created option - internal Argument( - StringSegment raw, - int nameStart, int nameLength, - int valueStart, int valueLength - ) { - this.raw = raw; - this.nameStart = nameStart; - this.nameLength = nameLength; - this.valueStart = valueStart; - this.valueLength = valueLength; - } - - /// Contains the raw string the command line argument was parsed from - public string Raw { - get { return this.raw.ToString(); } - } - - /// Characters used to initiate this option - public string Initiator { - get { - if(this.nameStart == -1) { - return null; - } else { - return this.raw.Text.Substring( - this.raw.Offset, this.nameStart - this.raw.Offset - ); - } - } - } - - /// Name of the command line option - public string Name { - get { - if(this.nameStart == -1) { - return null; - } else { - return this.raw.Text.Substring(this.nameStart, this.nameLength); - } - } - } - - /// Characters used to associate a value to this option - public string Associator { - get { - if(this.nameStart == -1) { - return null; - } else { - int associatorStart = this.nameStart + this.nameLength; - - if(this.valueStart == -1) { - int characterCount = (this.raw.Offset + this.raw.Count) - associatorStart; - if(characterCount == 0) { - return null; - } - } else if(this.valueStart == associatorStart) { - return null; - } - - return this.raw.Text.Substring(associatorStart, 1); - } - } - } - - /// Name of the command line option - public string Value { - get { - if(this.valueStart == -1) { - return null; - } else { - return this.raw.Text.Substring(this.valueStart, this.valueLength); - } - } - } - - /// The raw length of the command line argument - internal int RawLength { - get { return this.raw.Count; } - } - - /// - /// Contains the entire option as it was specified on the command line - /// - private StringSegment raw; - - /// Absolute index in the raw string the option name starts at - private int nameStart; - /// Number of characters in the option name - private int nameLength; - /// Absolute index in the raw string the value starts at - private int valueStart; - /// Number of characters in the value - private int valueLength; - - } - - } - -} // namespace Nuclex.Support.Parsing +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +namespace Nuclex.Support.Parsing { + + partial class CommandLine { + + /// Argument being specified on an application's command line + public class Argument { + + /// Initializes a new option with only a name + /// + /// String segment with the entire argument as it was given on the command line + /// + /// Absolute index the argument name starts at + /// Number of characters in the option name + /// The newly created option + internal static Argument OptionOnly( + StringSegment raw, + int nameStart, int nameLength + ) { + return new Argument(raw, nameStart, nameLength, -1, -1); + } + + /// Initializes a new argument with only a value + /// + /// String segment with the entire argument as it was given on the command line + /// + /// Absolute index the value starts at + /// Number of characters in the value + /// The newly created option + internal static Argument ValueOnly( + StringSegment raw, + int valueStart, int valueLength + ) { + return new Argument(raw, -1, -1, valueStart, valueLength); + } + + /// Creates a new option with a name and an assigned value + /// + /// String segment containing the entire option as it was given on the command line + /// + /// Absolute index the option name starts at + /// Number of characters in the option name + /// Absolute index the value starts at + /// Number of characters in the value + /// The newly created option + internal Argument( + StringSegment raw, + int nameStart, int nameLength, + int valueStart, int valueLength + ) { + this.raw = raw; + this.nameStart = nameStart; + this.nameLength = nameLength; + this.valueStart = valueStart; + this.valueLength = valueLength; + } + + /// Contains the raw string the command line argument was parsed from + public string Raw { + get { return this.raw.ToString(); } + } + + /// Characters used to initiate this option + public string Initiator { + get { + if(this.nameStart == -1) { + return null; + } else { + return this.raw.Text.Substring( + this.raw.Offset, this.nameStart - this.raw.Offset + ); + } + } + } + + /// Name of the command line option + public string Name { + get { + if(this.nameStart == -1) { + return null; + } else { + return this.raw.Text.Substring(this.nameStart, this.nameLength); + } + } + } + + /// Characters used to associate a value to this option + public string Associator { + get { + if(this.nameStart == -1) { + return null; + } else { + int associatorStart = this.nameStart + this.nameLength; + + if(this.valueStart == -1) { + int characterCount = (this.raw.Offset + this.raw.Count) - associatorStart; + if(characterCount == 0) { + return null; + } + } else if(this.valueStart == associatorStart) { + return null; + } + + return this.raw.Text.Substring(associatorStart, 1); + } + } + } + + /// Name of the command line option + public string Value { + get { + if(this.valueStart == -1) { + return null; + } else { + return this.raw.Text.Substring(this.valueStart, this.valueLength); + } + } + } + + /// The raw length of the command line argument + internal int RawLength { + get { return this.raw.Count; } + } + + /// + /// Contains the entire option as it was specified on the command line + /// + private StringSegment raw; + + /// Absolute index in the raw string the option name starts at + private int nameStart; + /// Number of characters in the option name + private int nameLength; + /// Absolute index in the raw string the value starts at + private int valueStart; + /// Number of characters in the value + private int valueLength; + + } + + } + +} // namespace Nuclex.Support.Parsing diff --git a/Source/Parsing/CommandLine.Formatter.cs b/Source/Parsing/CommandLine.Formatter.cs index 9451b0b..a5cfdab 100644 --- a/Source/Parsing/CommandLine.Formatter.cs +++ b/Source/Parsing/CommandLine.Formatter.cs @@ -1,62 +1,61 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Text; - -namespace Nuclex.Support.Parsing { - - partial class CommandLine { - - /// Formats a command line instance into a string - internal static class Formatter { - - /// - /// Formats all arguments in the provided command line instance into a string - /// - /// Command line instance that will be formatted - /// All arguments in the command line instance as a string - public static string FormatCommandLine(CommandLine commandLine) { - int totalLength = 0; - for(int index = 0; index < commandLine.arguments.Count; ++index) { - if(index != 0) { - ++totalLength; // For spacing between arguments - } - - totalLength += commandLine.arguments[index].RawLength; - } - - StringBuilder builder = new StringBuilder(totalLength); - for(int index = 0; index < commandLine.arguments.Count; ++index) { - if(index != 0) { - builder.Append(' '); - } - - builder.Append(commandLine.arguments[index].Raw); - } - - return builder.ToString(); - } - - } - - } - -} // namespace Nuclex.Support.Parsing +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Text; + +namespace Nuclex.Support.Parsing { + + partial class CommandLine { + + /// Formats a command line instance into a string + internal static class Formatter { + + /// + /// Formats all arguments in the provided command line instance into a string + /// + /// Command line instance that will be formatted + /// All arguments in the command line instance as a string + public static string FormatCommandLine(CommandLine commandLine) { + int totalLength = 0; + for(int index = 0; index < commandLine.arguments.Count; ++index) { + if(index != 0) { + ++totalLength; // For spacing between arguments + } + + totalLength += commandLine.arguments[index].RawLength; + } + + StringBuilder builder = new StringBuilder(totalLength); + for(int index = 0; index < commandLine.arguments.Count; ++index) { + if(index != 0) { + builder.Append(' '); + } + + builder.Append(commandLine.arguments[index].Raw); + } + + return builder.ToString(); + } + + } + + } + +} // namespace Nuclex.Support.Parsing diff --git a/Source/Parsing/CommandLine.Parser.cs b/Source/Parsing/CommandLine.Parser.cs index 5003a83..719356a 100644 --- a/Source/Parsing/CommandLine.Parser.cs +++ b/Source/Parsing/CommandLine.Parser.cs @@ -1,400 +1,399 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; - -namespace Nuclex.Support.Parsing { - - partial class CommandLine { - - /// Parses command line strings - private class Parser { - - /// Initializes a new command line parser - /// Whether the / character initiates an argument - private Parser(bool windowsMode) { - this.windowsMode = windowsMode; - this.arguments = new List(); - } - - /// Parses a string containing command line arguments - /// String that will be parsed - /// Whether the / character initiates an argument - /// The parsed command line arguments from the string - public static List Parse( - string commandLineString, bool windowsMode - ) { - Parser theParser = new Parser(windowsMode); - theParser.parseFullCommandLine(commandLineString); - return theParser.arguments; - } - - /// - /// Parses the provided string and adds the parameters found to - /// the command line representation - /// - /// - /// String containing the command line arguments that will be parsed - /// - private void parseFullCommandLine(string commandLineString) { - if(commandLineString == null) { - return; - } - - // Walk through the command line character by character and gather - // the parameters and values to build the command line representation from - for(int index = 0; index < commandLineString.Length; ) { - - // Look for the next non-whitespace character - index = StringHelper.IndexNotOfAny( - commandLineString, WhitespaceCharacters, index - ); - if(index == -1) { - break; - } - - // Parse the chunk of characters at this location and advance the index - // to the next location after the chunk of characters - parseChunk(commandLineString, ref index); - - } - } - - /// - /// Parses a chunk of characters and adds it as an option or a loose value to - /// the command line representation we're building - /// - /// - /// String containing the chunk of characters that will be parsed - /// - /// Index in the string at which to begin parsing - /// The number of characters that were consumed - private void parseChunk(string commandLineString, ref int index) { - int startIndex = index; - - char currentCharacter = commandLineString[index]; - switch(currentCharacter) { - - // Unix style argument using either '-' or "--" as its initiator - case '-': { - ++index; - - // Does the string end here? Stop parsing. - if(index >= commandLineString.Length) { - addValue(new StringSegment(commandLineString, startIndex, 1)); - break; - } - - // Does another '-' follow? Might be a unix style option or a loose "--" - if(commandLineString[index] == '-') { - ++index; - } - - parsePotentialOption(commandLineString, startIndex, ref index); - - break; - } - - // Windows style argument using '/' as its initiator - case '/': { - // The '/ character is only used to initiate argument on windows and can be - // toggled off. The application decides whether this is done depending on the - // operating system or whether uniform behavior across platforms is desired. - if(!this.windowsMode) { - goto default; - } - - ++index; - parsePotentialOption(commandLineString, startIndex, ref index); - break; - } - - // Quoted loose value - case '"': { - parseQuotedValue(commandLineString, ref index); - break; - } - - // Unquoted loose value - default: { - parseNakedValue(commandLineString, ref index); - break; - } - - } - } - - /// Parses a potential command line option - /// String containing the command line arguments - /// - /// Index of the option's initiator ('-' or '--' or '/') - /// - /// - /// Index at which the option name is supposed start (if it's an actual option) - /// - /// The number of characters consumed - private void parsePotentialOption( - string commandLineString, int initiatorStartIndex, ref int index - ) { - - // If the string ends here this can only be considered as a loose value - if(index == commandLineString.Length) { - addValue( - new StringSegment( - commandLineString, - initiatorStartIndex, - commandLineString.Length - initiatorStartIndex - ) - ); - return; - } - - int nameStartIndex = index; - - // Look for the first character that ends the option. If it is not an actual option, - // the very first character might be the end - if(commandLineString[index] != commandLineString[initiatorStartIndex]) { - index = commandLineString.IndexOfAny(NameEndingCharacters, nameStartIndex); - if(index == -1) { - index = commandLineString.Length; - } - } - - // If the first character of the supposed option is not valid for an option name, - // we have to consider this to be a loose value - if((index == nameStartIndex)/* && !isAssignmentCharacter(commandLineString[index])*/) { - index = commandLineString.IndexOfAny(WhitespaceCharacters, index); - if(index == -1) { - index = commandLineString.Length; - } - - addValue( - new StringSegment( - commandLineString, initiatorStartIndex, index - initiatorStartIndex - ) - ); - return; - } - - parsePotentialOptionAssignment( - commandLineString, initiatorStartIndex, nameStartIndex, ref index - ); - - } - - /// Parses the value assignment in a command line option - /// String containing the command line arguments - /// - /// Position of the character that started the option - /// - /// - /// Position of the first character in the option's name - /// - /// Index at which the option name ended - private void parsePotentialOptionAssignment( - string commandLineString, int initiatorStartIndex, int nameStartIndex, ref int index - ) { - int nameEndIndex = index; - int valueStartIndex; - int valueEndIndex; - - // See if this is an assignment character. If it is, the assigned value - // should follow to the right. - bool isAssignment = - (index < commandLineString.Length) && - isAssignmentCharacter(commandLineString[index]); - - // If it's an assignment, we can proceed parsing the assigned value - if(isAssignment) { - ++index; - parseOptionValue(commandLineString, initiatorStartIndex, nameStartIndex, ref index); - return; - } else { // No, it's an option name without an assignment - - bool isModifier = - (commandLineString[index - 1] == '+') || - (commandLineString[index - 1] == '-'); - - if(isModifier) { - valueStartIndex = index - 1; - valueEndIndex = index; - --nameEndIndex; - } else { - valueStartIndex = -1; - valueEndIndex = -1; - } - } - - int argumentLength = index - initiatorStartIndex; - this.arguments.Add( - new Argument( - new StringSegment(commandLineString, initiatorStartIndex, argumentLength), - nameStartIndex, nameEndIndex - nameStartIndex, - valueStartIndex, valueEndIndex - valueStartIndex - ) - ); - } - - /// Parses the value assignment in a command line option - /// String containing the command line arguments - /// - /// Position of the character that started the option - /// - /// - /// Position of the first character in the option's name - /// - /// Index at which the option name ended - private void parseOptionValue( - string commandLineString, int initiatorStartIndex, int nameStartIndex, ref int index - ) { - int nameEndIndex = index - 1; - int valueStartIndex, valueEndIndex; - - // Does the string end after the suspected assignment character? - bool argumentEndReached = (index == commandLineString.Length); - - if(argumentEndReached) { - valueStartIndex = -1; - valueEndIndex = -1; - } else { - char nextCharacter = commandLineString[index]; - - // Is this a quoted assignment - if(nextCharacter == '"') { - ++index; - valueStartIndex = index; - index = commandLineString.IndexOf('"', index); - if(index == -1) { - index = commandLineString.Length; - valueEndIndex = index; - } else { - valueEndIndex = index; - ++index; - } - } else { // Nope, assuming unquoted assignment or empty assignment - valueStartIndex = index; - index = commandLineString.IndexOfAny(WhitespaceCharacters, index); - if(index == -1) { - index = commandLineString.Length; - valueEndIndex = index; - } else { - if(index == valueStartIndex) { - valueStartIndex = -1; - valueEndIndex = -1; - } else { - valueEndIndex = index; - } - } - } - } - - int argumentLength = index - initiatorStartIndex; - this.arguments.Add( - new Argument( - new StringSegment(commandLineString, initiatorStartIndex, argumentLength), - nameStartIndex, nameEndIndex - nameStartIndex, - valueStartIndex, valueEndIndex - valueStartIndex - ) - ); - } - - /// Parses a quoted value from the input string - /// String the quoted value is parsed from - /// Index at which the quoted value begins - private void parseQuotedValue(string commandLineString, ref int index) { - int startIndex = index; - char quoteCharacter = commandLineString[index]; - int valueIndex = startIndex + 1; - - // Search for the closing quote - index = commandLineString.IndexOf(quoteCharacter, valueIndex); - if(index == -1) { - index = commandLineString.Length; // value ends at string end - this.arguments.Add( - Argument.ValueOnly( - new StringSegment(commandLineString, startIndex, index - startIndex), - valueIndex, index - valueIndex - ) - ); - } else { // A closing quote was found - this.arguments.Add( - Argument.ValueOnly( - new StringSegment(commandLineString, startIndex, index - startIndex + 1), - valueIndex, index - valueIndex - ) - ); - ++index; // Skip the closing quote - } - } - - /// Parses a plain, unquoted value from the input string - /// String containing the value to be parsed - /// Index at which the value begins - private void parseNakedValue(string commandLineString, ref int index) { - int startIndex = index; - - index = commandLineString.IndexOfAny(WhitespaceCharacters, index); - if(index == -1) { - index = commandLineString.Length; - } - - addValue(new StringSegment(commandLineString, startIndex, index - startIndex)); - } - - /// Adds a loose value to the command line - /// Value taht will be added - private void addValue(StringSegment value) { - this.arguments.Add( - Argument.ValueOnly(value, value.Offset, value.Count) - ); - } - - /// - /// Determines whether the specified character indicates an assignment - /// - /// - /// Character that will be checked for being an assignemnt - /// - /// - /// True if the specified character indicated an assignment, otherwise false - /// - private static bool isAssignmentCharacter(char character) { - return (character == ':') || (character == '='); - } - - /// Characters which end an option name when they are encountered - private static readonly char[] NameEndingCharacters = new char[] { - ' ', '\t', '=', ':', '"' - }; - - /// Characters the parser considers to be whitespace - private static readonly char[] WhitespaceCharacters = new char[] { ' ', '\t' }; - - /// Argument list being filled by the parser - private List arguments; - /// Whether the '/' character initiates an argument - private bool windowsMode; - - } - - } - -} // namespace Nuclex.Support.Parsing +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; + +namespace Nuclex.Support.Parsing { + + partial class CommandLine { + + /// Parses command line strings + private class Parser { + + /// Initializes a new command line parser + /// Whether the / character initiates an argument + private Parser(bool windowsMode) { + this.windowsMode = windowsMode; + this.arguments = new List(); + } + + /// Parses a string containing command line arguments + /// String that will be parsed + /// Whether the / character initiates an argument + /// The parsed command line arguments from the string + public static List Parse( + string commandLineString, bool windowsMode + ) { + Parser theParser = new Parser(windowsMode); + theParser.parseFullCommandLine(commandLineString); + return theParser.arguments; + } + + /// + /// Parses the provided string and adds the parameters found to + /// the command line representation + /// + /// + /// String containing the command line arguments that will be parsed + /// + private void parseFullCommandLine(string commandLineString) { + if(commandLineString == null) { + return; + } + + // Walk through the command line character by character and gather + // the parameters and values to build the command line representation from + for(int index = 0; index < commandLineString.Length; ) { + + // Look for the next non-whitespace character + index = StringHelper.IndexNotOfAny( + commandLineString, WhitespaceCharacters, index + ); + if(index == -1) { + break; + } + + // Parse the chunk of characters at this location and advance the index + // to the next location after the chunk of characters + parseChunk(commandLineString, ref index); + + } + } + + /// + /// Parses a chunk of characters and adds it as an option or a loose value to + /// the command line representation we're building + /// + /// + /// String containing the chunk of characters that will be parsed + /// + /// Index in the string at which to begin parsing + /// The number of characters that were consumed + private void parseChunk(string commandLineString, ref int index) { + int startIndex = index; + + char currentCharacter = commandLineString[index]; + switch(currentCharacter) { + + // Unix style argument using either '-' or "--" as its initiator + case '-': { + ++index; + + // Does the string end here? Stop parsing. + if(index >= commandLineString.Length) { + addValue(new StringSegment(commandLineString, startIndex, 1)); + break; + } + + // Does another '-' follow? Might be a unix style option or a loose "--" + if(commandLineString[index] == '-') { + ++index; + } + + parsePotentialOption(commandLineString, startIndex, ref index); + + break; + } + + // Windows style argument using '/' as its initiator + case '/': { + // The '/ character is only used to initiate argument on windows and can be + // toggled off. The application decides whether this is done depending on the + // operating system or whether uniform behavior across platforms is desired. + if(!this.windowsMode) { + goto default; + } + + ++index; + parsePotentialOption(commandLineString, startIndex, ref index); + break; + } + + // Quoted loose value + case '"': { + parseQuotedValue(commandLineString, ref index); + break; + } + + // Unquoted loose value + default: { + parseNakedValue(commandLineString, ref index); + break; + } + + } + } + + /// Parses a potential command line option + /// String containing the command line arguments + /// + /// Index of the option's initiator ('-' or '--' or '/') + /// + /// + /// Index at which the option name is supposed start (if it's an actual option) + /// + /// The number of characters consumed + private void parsePotentialOption( + string commandLineString, int initiatorStartIndex, ref int index + ) { + + // If the string ends here this can only be considered as a loose value + if(index == commandLineString.Length) { + addValue( + new StringSegment( + commandLineString, + initiatorStartIndex, + commandLineString.Length - initiatorStartIndex + ) + ); + return; + } + + int nameStartIndex = index; + + // Look for the first character that ends the option. If it is not an actual option, + // the very first character might be the end + if(commandLineString[index] != commandLineString[initiatorStartIndex]) { + index = commandLineString.IndexOfAny(NameEndingCharacters, nameStartIndex); + if(index == -1) { + index = commandLineString.Length; + } + } + + // If the first character of the supposed option is not valid for an option name, + // we have to consider this to be a loose value + if((index == nameStartIndex)/* && !isAssignmentCharacter(commandLineString[index])*/) { + index = commandLineString.IndexOfAny(WhitespaceCharacters, index); + if(index == -1) { + index = commandLineString.Length; + } + + addValue( + new StringSegment( + commandLineString, initiatorStartIndex, index - initiatorStartIndex + ) + ); + return; + } + + parsePotentialOptionAssignment( + commandLineString, initiatorStartIndex, nameStartIndex, ref index + ); + + } + + /// Parses the value assignment in a command line option + /// String containing the command line arguments + /// + /// Position of the character that started the option + /// + /// + /// Position of the first character in the option's name + /// + /// Index at which the option name ended + private void parsePotentialOptionAssignment( + string commandLineString, int initiatorStartIndex, int nameStartIndex, ref int index + ) { + int nameEndIndex = index; + int valueStartIndex; + int valueEndIndex; + + // See if this is an assignment character. If it is, the assigned value + // should follow to the right. + bool isAssignment = + (index < commandLineString.Length) && + isAssignmentCharacter(commandLineString[index]); + + // If it's an assignment, we can proceed parsing the assigned value + if(isAssignment) { + ++index; + parseOptionValue(commandLineString, initiatorStartIndex, nameStartIndex, ref index); + return; + } else { // No, it's an option name without an assignment + + bool isModifier = + (commandLineString[index - 1] == '+') || + (commandLineString[index - 1] == '-'); + + if(isModifier) { + valueStartIndex = index - 1; + valueEndIndex = index; + --nameEndIndex; + } else { + valueStartIndex = -1; + valueEndIndex = -1; + } + } + + int argumentLength = index - initiatorStartIndex; + this.arguments.Add( + new Argument( + new StringSegment(commandLineString, initiatorStartIndex, argumentLength), + nameStartIndex, nameEndIndex - nameStartIndex, + valueStartIndex, valueEndIndex - valueStartIndex + ) + ); + } + + /// Parses the value assignment in a command line option + /// String containing the command line arguments + /// + /// Position of the character that started the option + /// + /// + /// Position of the first character in the option's name + /// + /// Index at which the option name ended + private void parseOptionValue( + string commandLineString, int initiatorStartIndex, int nameStartIndex, ref int index + ) { + int nameEndIndex = index - 1; + int valueStartIndex, valueEndIndex; + + // Does the string end after the suspected assignment character? + bool argumentEndReached = (index == commandLineString.Length); + + if(argumentEndReached) { + valueStartIndex = -1; + valueEndIndex = -1; + } else { + char nextCharacter = commandLineString[index]; + + // Is this a quoted assignment + if(nextCharacter == '"') { + ++index; + valueStartIndex = index; + index = commandLineString.IndexOf('"', index); + if(index == -1) { + index = commandLineString.Length; + valueEndIndex = index; + } else { + valueEndIndex = index; + ++index; + } + } else { // Nope, assuming unquoted assignment or empty assignment + valueStartIndex = index; + index = commandLineString.IndexOfAny(WhitespaceCharacters, index); + if(index == -1) { + index = commandLineString.Length; + valueEndIndex = index; + } else { + if(index == valueStartIndex) { + valueStartIndex = -1; + valueEndIndex = -1; + } else { + valueEndIndex = index; + } + } + } + } + + int argumentLength = index - initiatorStartIndex; + this.arguments.Add( + new Argument( + new StringSegment(commandLineString, initiatorStartIndex, argumentLength), + nameStartIndex, nameEndIndex - nameStartIndex, + valueStartIndex, valueEndIndex - valueStartIndex + ) + ); + } + + /// Parses a quoted value from the input string + /// String the quoted value is parsed from + /// Index at which the quoted value begins + private void parseQuotedValue(string commandLineString, ref int index) { + int startIndex = index; + char quoteCharacter = commandLineString[index]; + int valueIndex = startIndex + 1; + + // Search for the closing quote + index = commandLineString.IndexOf(quoteCharacter, valueIndex); + if(index == -1) { + index = commandLineString.Length; // value ends at string end + this.arguments.Add( + Argument.ValueOnly( + new StringSegment(commandLineString, startIndex, index - startIndex), + valueIndex, index - valueIndex + ) + ); + } else { // A closing quote was found + this.arguments.Add( + Argument.ValueOnly( + new StringSegment(commandLineString, startIndex, index - startIndex + 1), + valueIndex, index - valueIndex + ) + ); + ++index; // Skip the closing quote + } + } + + /// Parses a plain, unquoted value from the input string + /// String containing the value to be parsed + /// Index at which the value begins + private void parseNakedValue(string commandLineString, ref int index) { + int startIndex = index; + + index = commandLineString.IndexOfAny(WhitespaceCharacters, index); + if(index == -1) { + index = commandLineString.Length; + } + + addValue(new StringSegment(commandLineString, startIndex, index - startIndex)); + } + + /// Adds a loose value to the command line + /// Value taht will be added + private void addValue(StringSegment value) { + this.arguments.Add( + Argument.ValueOnly(value, value.Offset, value.Count) + ); + } + + /// + /// Determines whether the specified character indicates an assignment + /// + /// + /// Character that will be checked for being an assignemnt + /// + /// + /// True if the specified character indicated an assignment, otherwise false + /// + private static bool isAssignmentCharacter(char character) { + return (character == ':') || (character == '='); + } + + /// Characters which end an option name when they are encountered + private static readonly char[] NameEndingCharacters = new char[] { + ' ', '\t', '=', ':', '"' + }; + + /// Characters the parser considers to be whitespace + private static readonly char[] WhitespaceCharacters = new char[] { ' ', '\t' }; + + /// Argument list being filled by the parser + private List arguments; + /// Whether the '/' character initiates an argument + private bool windowsMode; + + } + + } + +} // namespace Nuclex.Support.Parsing diff --git a/Source/Parsing/CommandLine.Test.cs b/Source/Parsing/CommandLine.Test.cs index 50de3cd..e9324ab 100644 --- a/Source/Parsing/CommandLine.Test.cs +++ b/Source/Parsing/CommandLine.Test.cs @@ -1,658 +1,657 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; - -#if UNITTEST - -using NUnit.Framework; - -namespace Nuclex.Support.Parsing { - - /// Ensures that the command line parser is working properly - [TestFixture] - internal class CommandLineTest { - - #region class ArgumentTest - - /// Unit test for the command line option class - [TestFixture] - public class ArgumentTest { - - /// - /// Verifies that the name of a command line argument without a value can - /// be extracted - /// - [Test] - public void TestNameExtraction() { - CommandLine.Argument argument = CommandLine.Argument.OptionOnly( - new StringSegment("--test"), 2, 4 - ); - - Assert.AreEqual("--test", argument.Raw); - Assert.AreEqual("--", argument.Initiator); - Assert.AreEqual("test", argument.Name); - Assert.IsNull(argument.Associator); - Assert.IsNull(argument.Value); - } - - /// - /// Verifies that the name of a command line argument without a value can be - /// extracted when the argument is contained in a substring of a larger string - /// - [Test] - public void TestNameExtractionFromSubstring() { - CommandLine.Argument argument = CommandLine.Argument.OptionOnly( - new StringSegment("||--test||", 2, 6), 4, 4 - ); - - Assert.AreEqual("--test", argument.Raw); - Assert.AreEqual("--", argument.Initiator); - Assert.AreEqual("test", argument.Name); - Assert.IsNull(argument.Associator); - Assert.IsNull(argument.Value); - } - - /// - /// Varifies that the name and value of a command line argument can be extracted - /// - [Test] - public void TestValueExtraction() { - CommandLine.Argument argument = new CommandLine.Argument( - new StringSegment("--test=123"), 2, 4, 7, 3 - ); - - Assert.AreEqual("--test=123", argument.Raw); - Assert.AreEqual("--", argument.Initiator); - Assert.AreEqual("test", argument.Name); - Assert.AreEqual("=", argument.Associator); - Assert.AreEqual("123", argument.Value); - } - - /// - /// Varifies that the name and value of a command line argument can be extracted - /// when the argument is contained in a substring of a larger string - /// - [Test] - public void TestValueExtractionFromSubstring() { - CommandLine.Argument argument = new CommandLine.Argument( - new StringSegment("||--test=123||", 2, 10), 4, 4, 9, 3 - ); - - Assert.AreEqual("--test=123", argument.Raw); - Assert.AreEqual("--", argument.Initiator); - Assert.AreEqual("test", argument.Name); - Assert.AreEqual("=", argument.Associator); - Assert.AreEqual("123", argument.Value); - } - - /// - /// Varifies that the name and value of a command line argument can be extracted - /// when the option is assigned a quoted value - /// - [Test] - public void TestQuotedValueExtraction() { - CommandLine.Argument argument = new CommandLine.Argument( - new StringSegment("--test=\"123\"", 0, 12), 2, 4, 8, 3 - ); - - Assert.AreEqual("--test=\"123\"", argument.Raw); - Assert.AreEqual("--", argument.Initiator); - Assert.AreEqual("test", argument.Name); - Assert.AreEqual("=", argument.Associator); - Assert.AreEqual("123", argument.Value); - } - - /// - /// Varifies that the associator of a command line argument with an open ended - /// value assignment can be retrieved - /// - [Test] - public void TestValuelessAssociatorRetrieval() { - CommandLine.Argument argument = CommandLine.Argument.OptionOnly( - new StringSegment("--test="), 2, 4 - ); - - Assert.AreEqual("--test=", argument.Raw); - Assert.AreEqual("--", argument.Initiator); - Assert.AreEqual("test", argument.Name); - Assert.AreEqual("=", argument.Associator); - Assert.IsNull(argument.Value); - } - - /// - /// Varifies that the associator of a command line option with an open ended value - /// assignment can be retrieved when the option is contained in a substring of - /// a larger string - /// - [Test] - public void TestValuelessAssociatorRetrievalFromSubstring() { - CommandLine.Argument option = CommandLine.Argument.OptionOnly( - new StringSegment("||--test=||", 2, 7), 4, 4 - ); - - Assert.AreEqual("--test=", option.Raw); - Assert.AreEqual("--", option.Initiator); - Assert.AreEqual("test", option.Name); - Assert.AreEqual("=", option.Associator); - Assert.IsNull(option.Value); - } - - /// - /// Varifies that a command line argument without an option name can be retrieved - /// - [Test] - public void TestNamelessValueRetrieval() { - CommandLine.Argument argument = CommandLine.Argument.ValueOnly( - new StringSegment("\"hello world\""), 1, 11 - ); - - Assert.AreEqual("\"hello world\"", argument.Raw); - Assert.IsNull(argument.Initiator); - Assert.IsNull(argument.Name); - Assert.IsNull(argument.Associator); - Assert.AreEqual("hello world", argument.Value); - } - - /// - /// Varifies that a command line argument without an option name can be retrieved - /// that is contained in a substring of larger string - /// - [Test] - public void TestNamelessValueRetrievalFromSubstring() { - CommandLine.Argument argument = CommandLine.Argument.ValueOnly( - new StringSegment("||\"hello world\"||", 2, 13), 3, 11 - ); - - Assert.AreEqual("\"hello world\"", argument.Raw); - Assert.IsNull(argument.Initiator); - Assert.IsNull(argument.Name); - Assert.IsNull(argument.Associator); - Assert.AreEqual("hello world", argument.Value); - } - - } - - #endregion // class ArgumentTest - - /// Verifies that the default constructor is working - [Test] - public void TestDefaultConstructor() { - CommandLine commandLine = new CommandLine(); - - Assert.AreEqual(0, commandLine.Arguments.Count); - } - - /// - /// Validates that the parser can handle an argument initiator with an - /// assignment that is missing a name - /// - [Test] - public void TestParseAmbiguousNameResolution() { - CommandLine commandLine = CommandLine.Parse("--:test"); - - // Without a name, this is not a valid command line option, so it will - // be parsed as a loose value instead. - Assert.AreEqual(1, commandLine.Arguments.Count); - Assert.AreEqual("--:test", commandLine.Arguments[0].Raw); - Assert.IsNull(commandLine.Arguments[0].Initiator); - Assert.IsNull(commandLine.Arguments[0].Name); - Assert.IsNull(commandLine.Arguments[0].Associator); - Assert.AreEqual("--:test", commandLine.Arguments[0].Value); - } - - /// - /// Verifies that a lone short argument initiator without anything behind - /// can be parsed - /// - [Test] - public void TestParseShortArgumentInitiatorOnly() { - CommandLine commandLine = CommandLine.Parse("-"); - - Assert.AreEqual(1, commandLine.Arguments.Count); - Assert.AreEqual("-", commandLine.Arguments[0].Raw); - Assert.IsNull(commandLine.Arguments[0].Initiator); - Assert.IsNull(commandLine.Arguments[0].Name); - Assert.IsNull(commandLine.Arguments[0].Associator); - Assert.AreEqual("-", commandLine.Arguments[0].Value); - } - - /// - /// Verifies that a lone long argument initiator without anything behind - /// can be parsed - /// - [Test] - public void TestParseLongArgumentInitiatorOnly() { - CommandLine commandLine = CommandLine.Parse("--"); - - Assert.AreEqual(1, commandLine.Arguments.Count); - Assert.AreEqual("--", commandLine.Arguments[0].Raw); - Assert.IsNull(commandLine.Arguments[0].Initiator); - Assert.IsNull(commandLine.Arguments[0].Name); - Assert.IsNull(commandLine.Arguments[0].Associator); - Assert.AreEqual("--", commandLine.Arguments[0].Value); - } - - /// - /// Validates that the parser can handle multiple lone argument initators without - /// a following argument - /// - [Test] - public void TestParseArgumentInitiatorAtEnd() { - CommandLine commandLine = CommandLine.Parse("-hello:-world -"); - - Assert.AreEqual(2, commandLine.Arguments.Count); - - Assert.AreEqual("-hello:-world", commandLine.Arguments[0].Raw); - Assert.AreEqual("-", commandLine.Arguments[0].Initiator); - Assert.AreEqual("hello", commandLine.Arguments[0].Name); - Assert.AreEqual(":", commandLine.Arguments[0].Associator); - Assert.AreEqual("-world", commandLine.Arguments[0].Value); - - Assert.AreEqual("-", commandLine.Arguments[1].Raw); - Assert.IsNull(commandLine.Arguments[1].Initiator); - Assert.IsNull(commandLine.Arguments[1].Name); - Assert.IsNull(commandLine.Arguments[1].Associator); - Assert.AreEqual("-", commandLine.Arguments[1].Value); - } - - /// Validates that quoted arguments can be parsed - [Test] - public void TestParseQuotedValue() { - CommandLine commandLine = CommandLine.Parse("hello -world --this -is=\"a test\""); - - Assert.AreEqual(4, commandLine.Arguments.Count); - - Assert.AreEqual("hello", commandLine.Arguments[0].Raw); - Assert.IsNull(commandLine.Arguments[0].Initiator); - Assert.IsNull(commandLine.Arguments[0].Name); - Assert.IsNull(commandLine.Arguments[0].Associator); - Assert.AreEqual("hello", commandLine.Arguments[0].Value); - - Assert.AreEqual("-world", commandLine.Arguments[1].Raw); - Assert.AreEqual("-", commandLine.Arguments[1].Initiator); - Assert.AreEqual("world", commandLine.Arguments[1].Name); - Assert.IsNull(commandLine.Arguments[1].Associator); - Assert.IsNull(commandLine.Arguments[1].Value); - - Assert.AreEqual("--this", commandLine.Arguments[2].Raw); - Assert.AreEqual("--", commandLine.Arguments[2].Initiator); - Assert.AreEqual("this", commandLine.Arguments[2].Name); - Assert.IsNull(commandLine.Arguments[2].Associator); - Assert.IsNull(commandLine.Arguments[2].Value); - - Assert.AreEqual("-is=\"a test\"", commandLine.Arguments[3].Raw); - Assert.AreEqual("-", commandLine.Arguments[3].Initiator); - Assert.AreEqual("is", commandLine.Arguments[3].Name); - Assert.AreEqual("=", commandLine.Arguments[3].Associator); - Assert.AreEqual("a test", commandLine.Arguments[3].Value); - } - - /// Validates that null can be parsed - [Test] - public void TestParseNull() { - CommandLine commandLine = CommandLine.Parse((string)null); - - Assert.AreEqual(0, commandLine.Arguments.Count); - } - - /// Validates that a single argument without quotes can be parsed - [Test] - public void TestParseSingleNakedValue() { - CommandLine commandLine = CommandLine.Parse("hello"); - - Assert.AreEqual(1, commandLine.Arguments.Count); - Assert.AreEqual("hello", commandLine.Arguments[0].Raw); - Assert.IsNull(commandLine.Arguments[0].Initiator); - Assert.IsNull(commandLine.Arguments[0].Name); - Assert.IsNull(commandLine.Arguments[0].Associator); - Assert.AreEqual("hello", commandLine.Arguments[0].Value); - } - - /// - /// Validates that the parser can handle a quoted argument that's missing - /// the closing quote - /// - [Test] - public void TestParseQuotedArgumentWithoutClosingQuote() { - CommandLine commandLine = CommandLine.Parse("\"Quoted argument"); - - Assert.AreEqual(1, commandLine.Arguments.Count); - Assert.AreEqual("\"Quoted argument", commandLine.Arguments[0].Raw); - Assert.IsNull(commandLine.Arguments[0].Initiator); - Assert.IsNull(commandLine.Arguments[0].Name); - Assert.IsNull(commandLine.Arguments[0].Associator); - Assert.AreEqual("Quoted argument", commandLine.Arguments[0].Value); - } - - /// - /// Validates that the parser correctly handles a quoted value assignment that's - /// missing the closing quote - /// - [Test] - public void TestParseQuotedValueWithoutClosingQuote() { - CommandLine commandLine = CommandLine.Parse("--test=\"Quoted argument"); - - Assert.AreEqual(1, commandLine.Arguments.Count); - Assert.AreEqual("--test=\"Quoted argument", commandLine.Arguments[0].Raw); - Assert.AreEqual("--", commandLine.Arguments[0].Initiator); - Assert.AreEqual("test", commandLine.Arguments[0].Name); - Assert.AreEqual("=", commandLine.Arguments[0].Associator); - Assert.AreEqual("Quoted argument", commandLine.Arguments[0].Value); - } - - /// - /// Validates that the parser can handle an command line consisting of only spaces - /// - [Test] - public void TestParseSpacesOnly() { - CommandLine commandLine = CommandLine.Parse(" \t "); - - Assert.AreEqual(0, commandLine.Arguments.Count); - } - - /// - /// Validates that the parser can handle a quoted option - /// - [Test] - public void TestParseQuotedOption() { - CommandLine commandLine = CommandLine.Parse("--\"hello\""); - - // Quoted options are not supported, so this becomes a loose value - Assert.AreEqual(1, commandLine.Arguments.Count); - Assert.AreEqual("--\"hello\"", commandLine.Arguments[0].Raw); - Assert.IsNull(commandLine.Arguments[0].Initiator); - Assert.IsNull(commandLine.Arguments[0].Name); - Assert.IsNull(commandLine.Arguments[0].Associator); - Assert.AreEqual("--\"hello\"", commandLine.Arguments[0].Value); - } - - /// - /// Validates that the parser can handle multiple lone argument initators without - /// a following argument - /// - [Test] - public void TestParseMultipleLoneArgumentInitiators() { - CommandLine commandLine = CommandLine.Parse("--- --"); - - Assert.AreEqual(2, commandLine.Arguments.Count); - - Assert.AreEqual("---", commandLine.Arguments[0].Raw); - Assert.IsNull(commandLine.Arguments[0].Initiator); - Assert.IsNull(commandLine.Arguments[0].Name); - Assert.IsNull(commandLine.Arguments[0].Associator); - Assert.AreEqual("---", commandLine.Arguments[0].Value); - - Assert.AreEqual("--", commandLine.Arguments[1].Raw); - Assert.IsNull(commandLine.Arguments[1].Initiator); - Assert.IsNull(commandLine.Arguments[1].Name); - Assert.IsNull(commandLine.Arguments[1].Associator); - Assert.AreEqual("--", commandLine.Arguments[1].Value); - } - - /// - /// Verifies that the parser correctly handles options with embedded option initiators - /// - [Test] - public void TestParseOptionWithEmbeddedInitiator() { - CommandLine commandLine = CommandLine.Parse("-hello/world=123 -test-case"); - - Assert.AreEqual(2, commandLine.Arguments.Count); - - Assert.AreEqual("-hello/world=123", commandLine.Arguments[0].Raw); - Assert.AreEqual("-", commandLine.Arguments[0].Initiator); - Assert.AreEqual("hello/world", commandLine.Arguments[0].Name); - Assert.AreEqual("=", commandLine.Arguments[0].Associator); - Assert.AreEqual("123", commandLine.Arguments[0].Value); - - Assert.AreEqual("-test-case", commandLine.Arguments[1].Raw); - Assert.AreEqual("-", commandLine.Arguments[1].Initiator); - Assert.AreEqual("test-case", commandLine.Arguments[1].Name); - Assert.IsNull(commandLine.Arguments[1].Associator); - Assert.IsNull(commandLine.Arguments[1].Value); - } - - /// - /// Validates that arguments and values without spaces inbetween can be parsed - /// - [Test] - public void TestParseOptionAndValueWithoutSpaces() { - CommandLine commandLine = CommandLine.Parse("\"value\"-option\"value\""); - - Assert.AreEqual(3, commandLine.Arguments.Count); - - Assert.AreEqual("\"value\"", commandLine.Arguments[0].Raw); - Assert.IsNull(commandLine.Arguments[0].Initiator); - Assert.IsNull(commandLine.Arguments[0].Name); - Assert.IsNull(commandLine.Arguments[0].Associator); - Assert.AreEqual("value", commandLine.Arguments[0].Value); - - Assert.AreEqual("-option", commandLine.Arguments[1].Raw); - Assert.AreEqual("-", commandLine.Arguments[1].Initiator); - Assert.AreEqual("option", commandLine.Arguments[1].Name); - Assert.IsNull(commandLine.Arguments[1].Associator); - Assert.IsNull(commandLine.Arguments[1].Value); - - Assert.AreEqual("\"value\"", commandLine.Arguments[2].Raw); - Assert.IsNull(commandLine.Arguments[2].Initiator); - Assert.IsNull(commandLine.Arguments[2].Name); - Assert.IsNull(commandLine.Arguments[2].Associator); - Assert.AreEqual("value", commandLine.Arguments[2].Value); - } - - /// - /// Validates that options with modifiers at the end of the command line - /// are parsed successfully - /// - [Test] - public void TestParseOptionWithModifierAtEnd() { - CommandLine commandLine = CommandLine.Parse("--test-value- -test+"); - - Assert.AreEqual(2, commandLine.Arguments.Count); - - Assert.AreEqual("--test-value-", commandLine.Arguments[0].Raw); - Assert.AreEqual("--", commandLine.Arguments[0].Initiator); - Assert.AreEqual("test-value", commandLine.Arguments[0].Name); - Assert.IsNull(commandLine.Arguments[0].Associator); - Assert.AreEqual("-", commandLine.Arguments[0].Value); - - Assert.AreEqual("-test+", commandLine.Arguments[1].Raw); - Assert.AreEqual("-", commandLine.Arguments[1].Initiator); - Assert.AreEqual("test", commandLine.Arguments[1].Name); - Assert.IsNull(commandLine.Arguments[1].Associator); - Assert.AreEqual("+", commandLine.Arguments[1].Value); - } - - /// - /// Validates that options with values assigned to them are parsed successfully - /// - [Test] - public void TestParseOptionWithAssignment() { - CommandLine commandLine = CommandLine.Parse("-hello: -world=321"); - - Assert.AreEqual(2, commandLine.Arguments.Count); - - Assert.AreEqual("-hello:", commandLine.Arguments[0].Raw); - Assert.AreEqual("-", commandLine.Arguments[0].Initiator); - Assert.AreEqual("hello", commandLine.Arguments[0].Name); - Assert.AreEqual(":", commandLine.Arguments[0].Associator); - Assert.IsNull(commandLine.Arguments[0].Value); - - Assert.AreEqual("-world=321", commandLine.Arguments[1].Raw); - Assert.AreEqual("-", commandLine.Arguments[1].Initiator); - Assert.AreEqual("world", commandLine.Arguments[1].Name); - Assert.AreEqual("=", commandLine.Arguments[1].Associator); - Assert.AreEqual("321", commandLine.Arguments[1].Value); - } - - /// - /// Validates that options with an empty value at the end of the command line - /// string are parsed successfully - /// - [Test] - public void TestParseOptionAtEndOfString() { - CommandLine commandLine = CommandLine.Parse("--test:"); - - Assert.AreEqual(1, commandLine.Arguments.Count); - Assert.AreEqual("--test:", commandLine.Arguments[0].Raw); - Assert.AreEqual("--", commandLine.Arguments[0].Initiator); - Assert.AreEqual("test", commandLine.Arguments[0].Name); - Assert.AreEqual(":", commandLine.Arguments[0].Associator); - Assert.IsNull(commandLine.Arguments[0].Value); - } - - /// - /// Verifies that the parser can recognize windows command line options if - /// configured to windows mode - /// - [Test] - public void TestWindowsOptionInitiator() { - CommandLine commandLine = CommandLine.Parse("/hello //world", true); - - Assert.AreEqual(2, commandLine.Arguments.Count); - - Assert.AreEqual("/hello", commandLine.Arguments[0].Raw); - Assert.AreEqual("/", commandLine.Arguments[0].Initiator); - Assert.AreEqual("hello", commandLine.Arguments[0].Name); - Assert.IsNull(commandLine.Arguments[0].Associator); - Assert.IsNull(commandLine.Arguments[0].Value); - - Assert.AreEqual("//world", commandLine.Arguments[1].Raw); - Assert.IsNull(commandLine.Arguments[1].Initiator); - Assert.IsNull(commandLine.Arguments[1].Name); - Assert.IsNull(commandLine.Arguments[1].Associator); - Assert.AreEqual("//world", commandLine.Arguments[1].Value); - } - - /// - /// Verifies that the parser ignores windows command line options if - /// configured to non-windows mode - /// - [Test] - public void TestNonWindowsOptionValues() { - CommandLine commandLine = CommandLine.Parse("/hello //world", false); - - Assert.AreEqual(2, commandLine.Arguments.Count); - - Assert.AreEqual("/hello", commandLine.Arguments[0].Raw); - Assert.IsNull(commandLine.Arguments[0].Initiator); - Assert.IsNull(commandLine.Arguments[0].Name); - Assert.IsNull(commandLine.Arguments[0].Associator); - Assert.AreEqual("/hello", commandLine.Arguments[0].Value); - - Assert.AreEqual("//world", commandLine.Arguments[1].Raw); - Assert.IsNull(commandLine.Arguments[1].Initiator); - Assert.IsNull(commandLine.Arguments[1].Name); - Assert.IsNull(commandLine.Arguments[1].Associator); - Assert.AreEqual("//world", commandLine.Arguments[1].Value); - } - - /// - /// Tests whether the existence of named arguments can be checked - /// - [Test] - public void HasArgumentWorksForWindowsStyleArguments() { - CommandLine test = CommandLine.Parse("/first:x /second:y /second:z third", true); - - Assert.IsTrue(test.HasArgument("first")); - Assert.IsTrue(test.HasArgument("second")); - Assert.IsFalse(test.HasArgument("third")); - Assert.IsFalse(test.HasArgument("fourth")); - } - - /// - /// Tests whether the existence of named arguments can be checked - /// - [Test] - public void HasArgumentWorksForUnixStyleArguments() { - CommandLine test = CommandLine.Parse("--first=x --second=y --second=z third", false); - - Assert.IsTrue(test.HasArgument("first")); - Assert.IsTrue(test.HasArgument("second")); - Assert.IsFalse(test.HasArgument("third")); - Assert.IsFalse(test.HasArgument("fourth")); - } - - /// - /// Tests whether a command line can be built with the command line class - /// - [Test] - public void TestCommandLineFormatting() { - CommandLine commandLine = new CommandLine(true); - - commandLine.AddValue("single"); - commandLine.AddValue("with space"); - commandLine.AddOption("option"); - commandLine.AddOption("@@", "extravagant-option"); - commandLine.AddAssignment("name", "value"); - commandLine.AddAssignment("name", "value with spaces"); - commandLine.AddAssignment("@@", "name", "value"); - commandLine.AddAssignment("@@", "name", "value with spaces"); - - Assert.AreEqual(8, commandLine.Arguments.Count); - Assert.AreEqual("single", commandLine.Arguments[0].Value); - Assert.AreEqual("with space", commandLine.Arguments[1].Value); - Assert.AreEqual("option", commandLine.Arguments[2].Name); - Assert.AreEqual("@@", commandLine.Arguments[3].Initiator); - Assert.AreEqual("extravagant-option", commandLine.Arguments[3].Name); - Assert.AreEqual("name", commandLine.Arguments[4].Name); - Assert.AreEqual("value", commandLine.Arguments[4].Value); - Assert.AreEqual("name", commandLine.Arguments[5].Name); - Assert.AreEqual("value with spaces", commandLine.Arguments[5].Value); - Assert.AreEqual("@@", commandLine.Arguments[6].Initiator); - Assert.AreEqual("name", commandLine.Arguments[6].Name); - Assert.AreEqual("value", commandLine.Arguments[6].Value); - Assert.AreEqual("name", commandLine.Arguments[7].Name); - Assert.AreEqual("@@", commandLine.Arguments[7].Initiator); - Assert.AreEqual("value with spaces", commandLine.Arguments[7].Value); - - string commandLineString = commandLine.ToString(); - Assert.AreEqual( - "single \"with space\" " + - "-option @@extravagant-option " + - "-name=value -name=\"value with spaces\" " + - "@@name=value @@name=\"value with spaces\"", - commandLineString - ); - - } - - /// - /// Tests whether a command line can be built that contains empty arguments - /// - [Test] - public void TestNullArgumentFormatting() { - CommandLine commandLine = new CommandLine(false); - - commandLine.AddValue(string.Empty); - commandLine.AddValue("hello"); - commandLine.AddValue(null); - commandLine.AddValue("-test"); - - Assert.AreEqual(4, commandLine.Arguments.Count); - string commandLineString = commandLine.ToString(); - Assert.AreEqual("\"\" hello \"\" \"-test\"", commandLineString); - } - - } - -} // namespace Nuclex.Support.Parsing - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +#if UNITTEST + +using NUnit.Framework; + +namespace Nuclex.Support.Parsing { + + /// Ensures that the command line parser is working properly + [TestFixture] + internal class CommandLineTest { + + #region class ArgumentTest + + /// Unit test for the command line option class + [TestFixture] + public class ArgumentTest { + + /// + /// Verifies that the name of a command line argument without a value can + /// be extracted + /// + [Test] + public void TestNameExtraction() { + CommandLine.Argument argument = CommandLine.Argument.OptionOnly( + new StringSegment("--test"), 2, 4 + ); + + Assert.AreEqual("--test", argument.Raw); + Assert.AreEqual("--", argument.Initiator); + Assert.AreEqual("test", argument.Name); + Assert.IsNull(argument.Associator); + Assert.IsNull(argument.Value); + } + + /// + /// Verifies that the name of a command line argument without a value can be + /// extracted when the argument is contained in a substring of a larger string + /// + [Test] + public void TestNameExtractionFromSubstring() { + CommandLine.Argument argument = CommandLine.Argument.OptionOnly( + new StringSegment("||--test||", 2, 6), 4, 4 + ); + + Assert.AreEqual("--test", argument.Raw); + Assert.AreEqual("--", argument.Initiator); + Assert.AreEqual("test", argument.Name); + Assert.IsNull(argument.Associator); + Assert.IsNull(argument.Value); + } + + /// + /// Varifies that the name and value of a command line argument can be extracted + /// + [Test] + public void TestValueExtraction() { + CommandLine.Argument argument = new CommandLine.Argument( + new StringSegment("--test=123"), 2, 4, 7, 3 + ); + + Assert.AreEqual("--test=123", argument.Raw); + Assert.AreEqual("--", argument.Initiator); + Assert.AreEqual("test", argument.Name); + Assert.AreEqual("=", argument.Associator); + Assert.AreEqual("123", argument.Value); + } + + /// + /// Varifies that the name and value of a command line argument can be extracted + /// when the argument is contained in a substring of a larger string + /// + [Test] + public void TestValueExtractionFromSubstring() { + CommandLine.Argument argument = new CommandLine.Argument( + new StringSegment("||--test=123||", 2, 10), 4, 4, 9, 3 + ); + + Assert.AreEqual("--test=123", argument.Raw); + Assert.AreEqual("--", argument.Initiator); + Assert.AreEqual("test", argument.Name); + Assert.AreEqual("=", argument.Associator); + Assert.AreEqual("123", argument.Value); + } + + /// + /// Varifies that the name and value of a command line argument can be extracted + /// when the option is assigned a quoted value + /// + [Test] + public void TestQuotedValueExtraction() { + CommandLine.Argument argument = new CommandLine.Argument( + new StringSegment("--test=\"123\"", 0, 12), 2, 4, 8, 3 + ); + + Assert.AreEqual("--test=\"123\"", argument.Raw); + Assert.AreEqual("--", argument.Initiator); + Assert.AreEqual("test", argument.Name); + Assert.AreEqual("=", argument.Associator); + Assert.AreEqual("123", argument.Value); + } + + /// + /// Varifies that the associator of a command line argument with an open ended + /// value assignment can be retrieved + /// + [Test] + public void TestValuelessAssociatorRetrieval() { + CommandLine.Argument argument = CommandLine.Argument.OptionOnly( + new StringSegment("--test="), 2, 4 + ); + + Assert.AreEqual("--test=", argument.Raw); + Assert.AreEqual("--", argument.Initiator); + Assert.AreEqual("test", argument.Name); + Assert.AreEqual("=", argument.Associator); + Assert.IsNull(argument.Value); + } + + /// + /// Varifies that the associator of a command line option with an open ended value + /// assignment can be retrieved when the option is contained in a substring of + /// a larger string + /// + [Test] + public void TestValuelessAssociatorRetrievalFromSubstring() { + CommandLine.Argument option = CommandLine.Argument.OptionOnly( + new StringSegment("||--test=||", 2, 7), 4, 4 + ); + + Assert.AreEqual("--test=", option.Raw); + Assert.AreEqual("--", option.Initiator); + Assert.AreEqual("test", option.Name); + Assert.AreEqual("=", option.Associator); + Assert.IsNull(option.Value); + } + + /// + /// Varifies that a command line argument without an option name can be retrieved + /// + [Test] + public void TestNamelessValueRetrieval() { + CommandLine.Argument argument = CommandLine.Argument.ValueOnly( + new StringSegment("\"hello world\""), 1, 11 + ); + + Assert.AreEqual("\"hello world\"", argument.Raw); + Assert.IsNull(argument.Initiator); + Assert.IsNull(argument.Name); + Assert.IsNull(argument.Associator); + Assert.AreEqual("hello world", argument.Value); + } + + /// + /// Varifies that a command line argument without an option name can be retrieved + /// that is contained in a substring of larger string + /// + [Test] + public void TestNamelessValueRetrievalFromSubstring() { + CommandLine.Argument argument = CommandLine.Argument.ValueOnly( + new StringSegment("||\"hello world\"||", 2, 13), 3, 11 + ); + + Assert.AreEqual("\"hello world\"", argument.Raw); + Assert.IsNull(argument.Initiator); + Assert.IsNull(argument.Name); + Assert.IsNull(argument.Associator); + Assert.AreEqual("hello world", argument.Value); + } + + } + + #endregion // class ArgumentTest + + /// Verifies that the default constructor is working + [Test] + public void TestDefaultConstructor() { + CommandLine commandLine = new CommandLine(); + + Assert.AreEqual(0, commandLine.Arguments.Count); + } + + /// + /// Validates that the parser can handle an argument initiator with an + /// assignment that is missing a name + /// + [Test] + public void TestParseAmbiguousNameResolution() { + CommandLine commandLine = CommandLine.Parse("--:test"); + + // Without a name, this is not a valid command line option, so it will + // be parsed as a loose value instead. + Assert.AreEqual(1, commandLine.Arguments.Count); + Assert.AreEqual("--:test", commandLine.Arguments[0].Raw); + Assert.IsNull(commandLine.Arguments[0].Initiator); + Assert.IsNull(commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.AreEqual("--:test", commandLine.Arguments[0].Value); + } + + /// + /// Verifies that a lone short argument initiator without anything behind + /// can be parsed + /// + [Test] + public void TestParseShortArgumentInitiatorOnly() { + CommandLine commandLine = CommandLine.Parse("-"); + + Assert.AreEqual(1, commandLine.Arguments.Count); + Assert.AreEqual("-", commandLine.Arguments[0].Raw); + Assert.IsNull(commandLine.Arguments[0].Initiator); + Assert.IsNull(commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.AreEqual("-", commandLine.Arguments[0].Value); + } + + /// + /// Verifies that a lone long argument initiator without anything behind + /// can be parsed + /// + [Test] + public void TestParseLongArgumentInitiatorOnly() { + CommandLine commandLine = CommandLine.Parse("--"); + + Assert.AreEqual(1, commandLine.Arguments.Count); + Assert.AreEqual("--", commandLine.Arguments[0].Raw); + Assert.IsNull(commandLine.Arguments[0].Initiator); + Assert.IsNull(commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.AreEqual("--", commandLine.Arguments[0].Value); + } + + /// + /// Validates that the parser can handle multiple lone argument initators without + /// a following argument + /// + [Test] + public void TestParseArgumentInitiatorAtEnd() { + CommandLine commandLine = CommandLine.Parse("-hello:-world -"); + + Assert.AreEqual(2, commandLine.Arguments.Count); + + Assert.AreEqual("-hello:-world", commandLine.Arguments[0].Raw); + Assert.AreEqual("-", commandLine.Arguments[0].Initiator); + Assert.AreEqual("hello", commandLine.Arguments[0].Name); + Assert.AreEqual(":", commandLine.Arguments[0].Associator); + Assert.AreEqual("-world", commandLine.Arguments[0].Value); + + Assert.AreEqual("-", commandLine.Arguments[1].Raw); + Assert.IsNull(commandLine.Arguments[1].Initiator); + Assert.IsNull(commandLine.Arguments[1].Name); + Assert.IsNull(commandLine.Arguments[1].Associator); + Assert.AreEqual("-", commandLine.Arguments[1].Value); + } + + /// Validates that quoted arguments can be parsed + [Test] + public void TestParseQuotedValue() { + CommandLine commandLine = CommandLine.Parse("hello -world --this -is=\"a test\""); + + Assert.AreEqual(4, commandLine.Arguments.Count); + + Assert.AreEqual("hello", commandLine.Arguments[0].Raw); + Assert.IsNull(commandLine.Arguments[0].Initiator); + Assert.IsNull(commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.AreEqual("hello", commandLine.Arguments[0].Value); + + Assert.AreEqual("-world", commandLine.Arguments[1].Raw); + Assert.AreEqual("-", commandLine.Arguments[1].Initiator); + Assert.AreEqual("world", commandLine.Arguments[1].Name); + Assert.IsNull(commandLine.Arguments[1].Associator); + Assert.IsNull(commandLine.Arguments[1].Value); + + Assert.AreEqual("--this", commandLine.Arguments[2].Raw); + Assert.AreEqual("--", commandLine.Arguments[2].Initiator); + Assert.AreEqual("this", commandLine.Arguments[2].Name); + Assert.IsNull(commandLine.Arguments[2].Associator); + Assert.IsNull(commandLine.Arguments[2].Value); + + Assert.AreEqual("-is=\"a test\"", commandLine.Arguments[3].Raw); + Assert.AreEqual("-", commandLine.Arguments[3].Initiator); + Assert.AreEqual("is", commandLine.Arguments[3].Name); + Assert.AreEqual("=", commandLine.Arguments[3].Associator); + Assert.AreEqual("a test", commandLine.Arguments[3].Value); + } + + /// Validates that null can be parsed + [Test] + public void TestParseNull() { + CommandLine commandLine = CommandLine.Parse((string)null); + + Assert.AreEqual(0, commandLine.Arguments.Count); + } + + /// Validates that a single argument without quotes can be parsed + [Test] + public void TestParseSingleNakedValue() { + CommandLine commandLine = CommandLine.Parse("hello"); + + Assert.AreEqual(1, commandLine.Arguments.Count); + Assert.AreEqual("hello", commandLine.Arguments[0].Raw); + Assert.IsNull(commandLine.Arguments[0].Initiator); + Assert.IsNull(commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.AreEqual("hello", commandLine.Arguments[0].Value); + } + + /// + /// Validates that the parser can handle a quoted argument that's missing + /// the closing quote + /// + [Test] + public void TestParseQuotedArgumentWithoutClosingQuote() { + CommandLine commandLine = CommandLine.Parse("\"Quoted argument"); + + Assert.AreEqual(1, commandLine.Arguments.Count); + Assert.AreEqual("\"Quoted argument", commandLine.Arguments[0].Raw); + Assert.IsNull(commandLine.Arguments[0].Initiator); + Assert.IsNull(commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.AreEqual("Quoted argument", commandLine.Arguments[0].Value); + } + + /// + /// Validates that the parser correctly handles a quoted value assignment that's + /// missing the closing quote + /// + [Test] + public void TestParseQuotedValueWithoutClosingQuote() { + CommandLine commandLine = CommandLine.Parse("--test=\"Quoted argument"); + + Assert.AreEqual(1, commandLine.Arguments.Count); + Assert.AreEqual("--test=\"Quoted argument", commandLine.Arguments[0].Raw); + Assert.AreEqual("--", commandLine.Arguments[0].Initiator); + Assert.AreEqual("test", commandLine.Arguments[0].Name); + Assert.AreEqual("=", commandLine.Arguments[0].Associator); + Assert.AreEqual("Quoted argument", commandLine.Arguments[0].Value); + } + + /// + /// Validates that the parser can handle an command line consisting of only spaces + /// + [Test] + public void TestParseSpacesOnly() { + CommandLine commandLine = CommandLine.Parse(" \t "); + + Assert.AreEqual(0, commandLine.Arguments.Count); + } + + /// + /// Validates that the parser can handle a quoted option + /// + [Test] + public void TestParseQuotedOption() { + CommandLine commandLine = CommandLine.Parse("--\"hello\""); + + // Quoted options are not supported, so this becomes a loose value + Assert.AreEqual(1, commandLine.Arguments.Count); + Assert.AreEqual("--\"hello\"", commandLine.Arguments[0].Raw); + Assert.IsNull(commandLine.Arguments[0].Initiator); + Assert.IsNull(commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.AreEqual("--\"hello\"", commandLine.Arguments[0].Value); + } + + /// + /// Validates that the parser can handle multiple lone argument initators without + /// a following argument + /// + [Test] + public void TestParseMultipleLoneArgumentInitiators() { + CommandLine commandLine = CommandLine.Parse("--- --"); + + Assert.AreEqual(2, commandLine.Arguments.Count); + + Assert.AreEqual("---", commandLine.Arguments[0].Raw); + Assert.IsNull(commandLine.Arguments[0].Initiator); + Assert.IsNull(commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.AreEqual("---", commandLine.Arguments[0].Value); + + Assert.AreEqual("--", commandLine.Arguments[1].Raw); + Assert.IsNull(commandLine.Arguments[1].Initiator); + Assert.IsNull(commandLine.Arguments[1].Name); + Assert.IsNull(commandLine.Arguments[1].Associator); + Assert.AreEqual("--", commandLine.Arguments[1].Value); + } + + /// + /// Verifies that the parser correctly handles options with embedded option initiators + /// + [Test] + public void TestParseOptionWithEmbeddedInitiator() { + CommandLine commandLine = CommandLine.Parse("-hello/world=123 -test-case"); + + Assert.AreEqual(2, commandLine.Arguments.Count); + + Assert.AreEqual("-hello/world=123", commandLine.Arguments[0].Raw); + Assert.AreEqual("-", commandLine.Arguments[0].Initiator); + Assert.AreEqual("hello/world", commandLine.Arguments[0].Name); + Assert.AreEqual("=", commandLine.Arguments[0].Associator); + Assert.AreEqual("123", commandLine.Arguments[0].Value); + + Assert.AreEqual("-test-case", commandLine.Arguments[1].Raw); + Assert.AreEqual("-", commandLine.Arguments[1].Initiator); + Assert.AreEqual("test-case", commandLine.Arguments[1].Name); + Assert.IsNull(commandLine.Arguments[1].Associator); + Assert.IsNull(commandLine.Arguments[1].Value); + } + + /// + /// Validates that arguments and values without spaces inbetween can be parsed + /// + [Test] + public void TestParseOptionAndValueWithoutSpaces() { + CommandLine commandLine = CommandLine.Parse("\"value\"-option\"value\""); + + Assert.AreEqual(3, commandLine.Arguments.Count); + + Assert.AreEqual("\"value\"", commandLine.Arguments[0].Raw); + Assert.IsNull(commandLine.Arguments[0].Initiator); + Assert.IsNull(commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.AreEqual("value", commandLine.Arguments[0].Value); + + Assert.AreEqual("-option", commandLine.Arguments[1].Raw); + Assert.AreEqual("-", commandLine.Arguments[1].Initiator); + Assert.AreEqual("option", commandLine.Arguments[1].Name); + Assert.IsNull(commandLine.Arguments[1].Associator); + Assert.IsNull(commandLine.Arguments[1].Value); + + Assert.AreEqual("\"value\"", commandLine.Arguments[2].Raw); + Assert.IsNull(commandLine.Arguments[2].Initiator); + Assert.IsNull(commandLine.Arguments[2].Name); + Assert.IsNull(commandLine.Arguments[2].Associator); + Assert.AreEqual("value", commandLine.Arguments[2].Value); + } + + /// + /// Validates that options with modifiers at the end of the command line + /// are parsed successfully + /// + [Test] + public void TestParseOptionWithModifierAtEnd() { + CommandLine commandLine = CommandLine.Parse("--test-value- -test+"); + + Assert.AreEqual(2, commandLine.Arguments.Count); + + Assert.AreEqual("--test-value-", commandLine.Arguments[0].Raw); + Assert.AreEqual("--", commandLine.Arguments[0].Initiator); + Assert.AreEqual("test-value", commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.AreEqual("-", commandLine.Arguments[0].Value); + + Assert.AreEqual("-test+", commandLine.Arguments[1].Raw); + Assert.AreEqual("-", commandLine.Arguments[1].Initiator); + Assert.AreEqual("test", commandLine.Arguments[1].Name); + Assert.IsNull(commandLine.Arguments[1].Associator); + Assert.AreEqual("+", commandLine.Arguments[1].Value); + } + + /// + /// Validates that options with values assigned to them are parsed successfully + /// + [Test] + public void TestParseOptionWithAssignment() { + CommandLine commandLine = CommandLine.Parse("-hello: -world=321"); + + Assert.AreEqual(2, commandLine.Arguments.Count); + + Assert.AreEqual("-hello:", commandLine.Arguments[0].Raw); + Assert.AreEqual("-", commandLine.Arguments[0].Initiator); + Assert.AreEqual("hello", commandLine.Arguments[0].Name); + Assert.AreEqual(":", commandLine.Arguments[0].Associator); + Assert.IsNull(commandLine.Arguments[0].Value); + + Assert.AreEqual("-world=321", commandLine.Arguments[1].Raw); + Assert.AreEqual("-", commandLine.Arguments[1].Initiator); + Assert.AreEqual("world", commandLine.Arguments[1].Name); + Assert.AreEqual("=", commandLine.Arguments[1].Associator); + Assert.AreEqual("321", commandLine.Arguments[1].Value); + } + + /// + /// Validates that options with an empty value at the end of the command line + /// string are parsed successfully + /// + [Test] + public void TestParseOptionAtEndOfString() { + CommandLine commandLine = CommandLine.Parse("--test:"); + + Assert.AreEqual(1, commandLine.Arguments.Count); + Assert.AreEqual("--test:", commandLine.Arguments[0].Raw); + Assert.AreEqual("--", commandLine.Arguments[0].Initiator); + Assert.AreEqual("test", commandLine.Arguments[0].Name); + Assert.AreEqual(":", commandLine.Arguments[0].Associator); + Assert.IsNull(commandLine.Arguments[0].Value); + } + + /// + /// Verifies that the parser can recognize windows command line options if + /// configured to windows mode + /// + [Test] + public void TestWindowsOptionInitiator() { + CommandLine commandLine = CommandLine.Parse("/hello //world", true); + + Assert.AreEqual(2, commandLine.Arguments.Count); + + Assert.AreEqual("/hello", commandLine.Arguments[0].Raw); + Assert.AreEqual("/", commandLine.Arguments[0].Initiator); + Assert.AreEqual("hello", commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.IsNull(commandLine.Arguments[0].Value); + + Assert.AreEqual("//world", commandLine.Arguments[1].Raw); + Assert.IsNull(commandLine.Arguments[1].Initiator); + Assert.IsNull(commandLine.Arguments[1].Name); + Assert.IsNull(commandLine.Arguments[1].Associator); + Assert.AreEqual("//world", commandLine.Arguments[1].Value); + } + + /// + /// Verifies that the parser ignores windows command line options if + /// configured to non-windows mode + /// + [Test] + public void TestNonWindowsOptionValues() { + CommandLine commandLine = CommandLine.Parse("/hello //world", false); + + Assert.AreEqual(2, commandLine.Arguments.Count); + + Assert.AreEqual("/hello", commandLine.Arguments[0].Raw); + Assert.IsNull(commandLine.Arguments[0].Initiator); + Assert.IsNull(commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.AreEqual("/hello", commandLine.Arguments[0].Value); + + Assert.AreEqual("//world", commandLine.Arguments[1].Raw); + Assert.IsNull(commandLine.Arguments[1].Initiator); + Assert.IsNull(commandLine.Arguments[1].Name); + Assert.IsNull(commandLine.Arguments[1].Associator); + Assert.AreEqual("//world", commandLine.Arguments[1].Value); + } + + /// + /// Tests whether the existence of named arguments can be checked + /// + [Test] + public void HasArgumentWorksForWindowsStyleArguments() { + CommandLine test = CommandLine.Parse("/first:x /second:y /second:z third", true); + + Assert.IsTrue(test.HasArgument("first")); + Assert.IsTrue(test.HasArgument("second")); + Assert.IsFalse(test.HasArgument("third")); + Assert.IsFalse(test.HasArgument("fourth")); + } + + /// + /// Tests whether the existence of named arguments can be checked + /// + [Test] + public void HasArgumentWorksForUnixStyleArguments() { + CommandLine test = CommandLine.Parse("--first=x --second=y --second=z third", false); + + Assert.IsTrue(test.HasArgument("first")); + Assert.IsTrue(test.HasArgument("second")); + Assert.IsFalse(test.HasArgument("third")); + Assert.IsFalse(test.HasArgument("fourth")); + } + + /// + /// Tests whether a command line can be built with the command line class + /// + [Test] + public void TestCommandLineFormatting() { + CommandLine commandLine = new CommandLine(true); + + commandLine.AddValue("single"); + commandLine.AddValue("with space"); + commandLine.AddOption("option"); + commandLine.AddOption("@@", "extravagant-option"); + commandLine.AddAssignment("name", "value"); + commandLine.AddAssignment("name", "value with spaces"); + commandLine.AddAssignment("@@", "name", "value"); + commandLine.AddAssignment("@@", "name", "value with spaces"); + + Assert.AreEqual(8, commandLine.Arguments.Count); + Assert.AreEqual("single", commandLine.Arguments[0].Value); + Assert.AreEqual("with space", commandLine.Arguments[1].Value); + Assert.AreEqual("option", commandLine.Arguments[2].Name); + Assert.AreEqual("@@", commandLine.Arguments[3].Initiator); + Assert.AreEqual("extravagant-option", commandLine.Arguments[3].Name); + Assert.AreEqual("name", commandLine.Arguments[4].Name); + Assert.AreEqual("value", commandLine.Arguments[4].Value); + Assert.AreEqual("name", commandLine.Arguments[5].Name); + Assert.AreEqual("value with spaces", commandLine.Arguments[5].Value); + Assert.AreEqual("@@", commandLine.Arguments[6].Initiator); + Assert.AreEqual("name", commandLine.Arguments[6].Name); + Assert.AreEqual("value", commandLine.Arguments[6].Value); + Assert.AreEqual("name", commandLine.Arguments[7].Name); + Assert.AreEqual("@@", commandLine.Arguments[7].Initiator); + Assert.AreEqual("value with spaces", commandLine.Arguments[7].Value); + + string commandLineString = commandLine.ToString(); + Assert.AreEqual( + "single \"with space\" " + + "-option @@extravagant-option " + + "-name=value -name=\"value with spaces\" " + + "@@name=value @@name=\"value with spaces\"", + commandLineString + ); + + } + + /// + /// Tests whether a command line can be built that contains empty arguments + /// + [Test] + public void TestNullArgumentFormatting() { + CommandLine commandLine = new CommandLine(false); + + commandLine.AddValue(string.Empty); + commandLine.AddValue("hello"); + commandLine.AddValue(null); + commandLine.AddValue("-test"); + + Assert.AreEqual(4, commandLine.Arguments.Count); + string commandLineString = commandLine.ToString(); + Assert.AreEqual("\"\" hello \"\" \"-test\"", commandLineString); + } + + } + +} // namespace Nuclex.Support.Parsing + +#endif // UNITTEST diff --git a/Source/Parsing/CommandLine.cs b/Source/Parsing/CommandLine.cs index 80db373..15c7408 100644 --- a/Source/Parsing/CommandLine.cs +++ b/Source/Parsing/CommandLine.cs @@ -1,305 +1,304 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Nuclex.Support.Parsing { - - /// Parses and stores an application's command line parameters - /// - /// - /// At the time of the creation of this component, there are already several command - /// line parsing libraries out there. Most of them, however, do way too much at once - /// or at the very least rely on huge, untested clutters of classes and methods to - /// arrive at their results. - /// - /// - /// This command line parser does nothing more than represent the command line to - /// the application through a convenient interface. It parses a command line and - /// extracts the arguments, but doesn't interpret them and or check them for validity. - /// - /// - /// This design promotes simplicity and makes is an ideal building block to create - /// actual command line interpreters that connect the parameters to program - /// instructions and or fill structures in code. - /// - /// - /// Terminology - /// - /// - /// Command line - /// - /// The entire command line either as a string or as - /// an already parsed data structure - /// - /// - /// - /// Argument - /// - /// Either an option or a loose value (see below) being specified on - /// the command line - /// - /// - /// - /// Option - /// - /// Can be specified on the command line and typically alters the behavior - /// of the application or changes a setting. For example, '--normalize' or - /// '/safemode'. - /// - /// - /// - /// Value - /// - /// Can either sit loosely in the command line (eg. 'update' or 'textfile.txt') - /// or as assignment to an option (eg. '--width=1280' or '/overwrite:always') - /// - /// - /// - /// - /// - public partial class CommandLine { - - /// - /// Whether the command line should use Windows mode by default - /// - public static readonly bool WindowsModeDefault = - (Path.DirectorySeparatorChar == '\\'); - - /// Initializes a new command line - public CommandLine() : - this(new List(), WindowsModeDefault) { } - - /// Initializes a new command line - /// Whether the / character initiates an argument - public CommandLine(bool windowsMode) : - this(new List(), windowsMode) { } - - /// Initializes a new command line - /// List containing the parsed arguments - private CommandLine(IList argumentList) : - this(argumentList, WindowsModeDefault) { } - - /// Initializes a new command line - /// List containing the parsed arguments - /// Whether the / character initiates an argument - private CommandLine(IList argumentList, bool windowsMode) { - this.arguments = argumentList; - this.windowsMode = windowsMode; - } - - /// Parses the command line arguments from the provided string - /// String containing the command line arguments - /// The parsed command line - /// - /// You should always pass Environment.CommandLine to this method to avoid - /// some problems with the built-in command line tokenizer in .NET - /// (which splits '--test"hello world"/v' into '--testhello world/v') - /// - public static CommandLine Parse(string commandLineString) { - bool windowsMode = (Path.DirectorySeparatorChar != '/'); - return Parse(commandLineString, windowsMode); - } - - /// Parses the command line arguments from the provided string - /// String containing the command line arguments - /// Whether the / character initiates an argument - /// The parsed command line - /// - /// You should always pass Environment.CommandLine to this methods to avoid - /// some problems with the built-in command line tokenizer in .NET - /// (which splits '--test"hello world"/v' into '--testhello world/v') - /// - public static CommandLine Parse(string commandLineString, bool windowsMode) { - return new CommandLine( - Parser.Parse(commandLineString, windowsMode) - ); - } - - /// Returns whether an argument with the specified name exists - /// Name of the argument whose existence will be checked - /// True if an argument with the specified name exists - public bool HasArgument(string name) { - return (indexOfArgument(name) != -1); - } - - /// Adds a value to the command line - /// Value that will be added - public void AddValue(string value) { - int valueLength = (value != null) ? value.Length : 0; - - if(requiresQuotes(value)) { - StringBuilder builder = new StringBuilder(valueLength + 2); - builder.Append('"'); - builder.Append(value); - builder.Append('"'); - - this.arguments.Add( - Argument.ValueOnly( - new StringSegment(builder.ToString(), 0, valueLength + 2), - 1, - valueLength - ) - ); - } else { - this.arguments.Add( - Argument.ValueOnly(new StringSegment(value), 0, valueLength) - ); - } - } - - /// Adds an option to the command line - /// Name of the option that will be added - public void AddOption(string name) { - AddOption("-", name); - } - - /// Adds an option to the command line - /// Initiator that will be used to start the option - /// Name of the option that will be added - public void AddOption(string initiator, string name) { - StringBuilder builder = new StringBuilder(initiator.Length + name.Length); - builder.Append(initiator); - builder.Append(name); - - this.arguments.Add( - Argument.OptionOnly( - new StringSegment(builder.ToString()), - initiator.Length, - name.Length - ) - ); - } - - /// Adds an option with an assignment to the command line - /// Name of the option that will be added - /// Value that will be assigned to the option - public void AddAssignment(string name, string value) { - AddAssignment("-", name, value); - } - - /// Adds an option with an assignment to the command line - /// Initiator that will be used to start the option - /// Name of the option that will be added - /// Value that will be assigned to the option - public void AddAssignment(string initiator, string name, string value) { - bool valueContainsSpaces = containsWhitespace(value); - StringBuilder builder = new StringBuilder( - initiator.Length + name.Length + 1 + value.Length + (valueContainsSpaces ? 2 : 0) - ); - builder.Append(initiator); - builder.Append(name); - builder.Append('='); - if(valueContainsSpaces) { - builder.Append('"'); - builder.Append(value); - builder.Append('"'); - } else { - builder.Append(value); - } - - this.arguments.Add( - new Argument( - new StringSegment(builder.ToString()), - initiator.Length, - name.Length, - initiator.Length + name.Length + 1 + (valueContainsSpaces ? 1 : 0), - value.Length - ) - ); - } - - /// Returns a string that contains the entire command line - /// The entire command line as a single string - public override string ToString() { - return Formatter.FormatCommandLine(this); - } - - /// Retrieves the index of the argument with the specified name - /// Name of the argument whose index will be returned - /// - /// The index of the indicated argument of -1 if no argument with that name exists - /// - private int indexOfArgument(string name) { - for(int index = 0; index < this.arguments.Count; ++index) { - if(this.arguments[index].Name == name) { - return index; - } - } - - return -1; - } - - /// Options that were specified on the command line - public IList Arguments { - get { return this.arguments; } - } - - /// - /// Determines whether the string requires quotes to survive the command line - /// - /// Value that will be checked for requiring quotes - /// True if the value requires quotes to survive the command line - private bool requiresQuotes(string value) { - - // If the value is empty, it needs quotes to become visible as an argument - // (versus being intepreted as spacing between other arguments) - if(string.IsNullOrEmpty(value)) { - return true; - } - - // Any whitespace characters force us to use quotes, so does a minus sign - // at the beginning of the value (otherwise, it would become an option argument) - bool requiresQuotes = - containsWhitespace(value) || - (value[0] == '-'); - - // On windows, option arguments can also be starten with the forward slash - // character, so we require quotes as well if the value starts with one - if(this.windowsMode) { - requiresQuotes |= (value[0] == '/'); - } - - return requiresQuotes; - - } - - /// - /// Determines whether the string contains any whitespace characters - /// - /// String that will be scanned for whitespace characters - /// True if the provided string contains whitespace characters - private static bool containsWhitespace(string value) { - return - (value.IndexOf(' ') != -1) || - (value.IndexOf('\t') != -1); - } - - /// Options that were specified on the command line - private IList arguments; - /// Whether the / character initiates an argument - private bool windowsMode; - - } - -} // namespace Nuclex.Support.Parsing +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Nuclex.Support.Parsing { + + /// Parses and stores an application's command line parameters + /// + /// + /// At the time of the creation of this component, there are already several command + /// line parsing libraries out there. Most of them, however, do way too much at once + /// or at the very least rely on huge, untested clutters of classes and methods to + /// arrive at their results. + /// + /// + /// This command line parser does nothing more than represent the command line to + /// the application through a convenient interface. It parses a command line and + /// extracts the arguments, but doesn't interpret them and or check them for validity. + /// + /// + /// This design promotes simplicity and makes is an ideal building block to create + /// actual command line interpreters that connect the parameters to program + /// instructions and or fill structures in code. + /// + /// + /// Terminology + /// + /// + /// Command line + /// + /// The entire command line either as a string or as + /// an already parsed data structure + /// + /// + /// + /// Argument + /// + /// Either an option or a loose value (see below) being specified on + /// the command line + /// + /// + /// + /// Option + /// + /// Can be specified on the command line and typically alters the behavior + /// of the application or changes a setting. For example, '--normalize' or + /// '/safemode'. + /// + /// + /// + /// Value + /// + /// Can either sit loosely in the command line (eg. 'update' or 'textfile.txt') + /// or as assignment to an option (eg. '--width=1280' or '/overwrite:always') + /// + /// + /// + /// + /// + public partial class CommandLine { + + /// + /// Whether the command line should use Windows mode by default + /// + public static readonly bool WindowsModeDefault = + (Path.DirectorySeparatorChar == '\\'); + + /// Initializes a new command line + public CommandLine() : + this(new List(), WindowsModeDefault) { } + + /// Initializes a new command line + /// Whether the / character initiates an argument + public CommandLine(bool windowsMode) : + this(new List(), windowsMode) { } + + /// Initializes a new command line + /// List containing the parsed arguments + private CommandLine(IList argumentList) : + this(argumentList, WindowsModeDefault) { } + + /// Initializes a new command line + /// List containing the parsed arguments + /// Whether the / character initiates an argument + private CommandLine(IList argumentList, bool windowsMode) { + this.arguments = argumentList; + this.windowsMode = windowsMode; + } + + /// Parses the command line arguments from the provided string + /// String containing the command line arguments + /// The parsed command line + /// + /// You should always pass Environment.CommandLine to this method to avoid + /// some problems with the built-in command line tokenizer in .NET + /// (which splits '--test"hello world"/v' into '--testhello world/v') + /// + public static CommandLine Parse(string commandLineString) { + bool windowsMode = (Path.DirectorySeparatorChar != '/'); + return Parse(commandLineString, windowsMode); + } + + /// Parses the command line arguments from the provided string + /// String containing the command line arguments + /// Whether the / character initiates an argument + /// The parsed command line + /// + /// You should always pass Environment.CommandLine to this methods to avoid + /// some problems with the built-in command line tokenizer in .NET + /// (which splits '--test"hello world"/v' into '--testhello world/v') + /// + public static CommandLine Parse(string commandLineString, bool windowsMode) { + return new CommandLine( + Parser.Parse(commandLineString, windowsMode) + ); + } + + /// Returns whether an argument with the specified name exists + /// Name of the argument whose existence will be checked + /// True if an argument with the specified name exists + public bool HasArgument(string name) { + return (indexOfArgument(name) != -1); + } + + /// Adds a value to the command line + /// Value that will be added + public void AddValue(string value) { + int valueLength = (value != null) ? value.Length : 0; + + if(requiresQuotes(value)) { + StringBuilder builder = new StringBuilder(valueLength + 2); + builder.Append('"'); + builder.Append(value); + builder.Append('"'); + + this.arguments.Add( + Argument.ValueOnly( + new StringSegment(builder.ToString(), 0, valueLength + 2), + 1, + valueLength + ) + ); + } else { + this.arguments.Add( + Argument.ValueOnly(new StringSegment(value), 0, valueLength) + ); + } + } + + /// Adds an option to the command line + /// Name of the option that will be added + public void AddOption(string name) { + AddOption("-", name); + } + + /// Adds an option to the command line + /// Initiator that will be used to start the option + /// Name of the option that will be added + public void AddOption(string initiator, string name) { + StringBuilder builder = new StringBuilder(initiator.Length + name.Length); + builder.Append(initiator); + builder.Append(name); + + this.arguments.Add( + Argument.OptionOnly( + new StringSegment(builder.ToString()), + initiator.Length, + name.Length + ) + ); + } + + /// Adds an option with an assignment to the command line + /// Name of the option that will be added + /// Value that will be assigned to the option + public void AddAssignment(string name, string value) { + AddAssignment("-", name, value); + } + + /// Adds an option with an assignment to the command line + /// Initiator that will be used to start the option + /// Name of the option that will be added + /// Value that will be assigned to the option + public void AddAssignment(string initiator, string name, string value) { + bool valueContainsSpaces = containsWhitespace(value); + StringBuilder builder = new StringBuilder( + initiator.Length + name.Length + 1 + value.Length + (valueContainsSpaces ? 2 : 0) + ); + builder.Append(initiator); + builder.Append(name); + builder.Append('='); + if(valueContainsSpaces) { + builder.Append('"'); + builder.Append(value); + builder.Append('"'); + } else { + builder.Append(value); + } + + this.arguments.Add( + new Argument( + new StringSegment(builder.ToString()), + initiator.Length, + name.Length, + initiator.Length + name.Length + 1 + (valueContainsSpaces ? 1 : 0), + value.Length + ) + ); + } + + /// Returns a string that contains the entire command line + /// The entire command line as a single string + public override string ToString() { + return Formatter.FormatCommandLine(this); + } + + /// Retrieves the index of the argument with the specified name + /// Name of the argument whose index will be returned + /// + /// The index of the indicated argument of -1 if no argument with that name exists + /// + private int indexOfArgument(string name) { + for(int index = 0; index < this.arguments.Count; ++index) { + if(this.arguments[index].Name == name) { + return index; + } + } + + return -1; + } + + /// Options that were specified on the command line + public IList Arguments { + get { return this.arguments; } + } + + /// + /// Determines whether the string requires quotes to survive the command line + /// + /// Value that will be checked for requiring quotes + /// True if the value requires quotes to survive the command line + private bool requiresQuotes(string value) { + + // If the value is empty, it needs quotes to become visible as an argument + // (versus being intepreted as spacing between other arguments) + if(string.IsNullOrEmpty(value)) { + return true; + } + + // Any whitespace characters force us to use quotes, so does a minus sign + // at the beginning of the value (otherwise, it would become an option argument) + bool requiresQuotes = + containsWhitespace(value) || + (value[0] == '-'); + + // On windows, option arguments can also be starten with the forward slash + // character, so we require quotes as well if the value starts with one + if(this.windowsMode) { + requiresQuotes |= (value[0] == '/'); + } + + return requiresQuotes; + + } + + /// + /// Determines whether the string contains any whitespace characters + /// + /// String that will be scanned for whitespace characters + /// True if the provided string contains whitespace characters + private static bool containsWhitespace(string value) { + return + (value.IndexOf(' ') != -1) || + (value.IndexOf('\t') != -1); + } + + /// Options that were specified on the command line + private IList arguments; + /// Whether the / character initiates an argument + private bool windowsMode; + + } + +} // namespace Nuclex.Support.Parsing diff --git a/Source/Parsing/ParserHelper.Test.cs b/Source/Parsing/ParserHelper.Test.cs index 2adf32b..63cf662 100644 --- a/Source/Parsing/ParserHelper.Test.cs +++ b/Source/Parsing/ParserHelper.Test.cs @@ -1,233 +1,232 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.Collections.Generic; -using System.Text; - -using NUnit.Framework; - -namespace Nuclex.Support.Parsing { - - /// Verifies that the parser helper methods are correct - [TestFixture] - internal class ParserHelperTest { - - /// Ensures that the SkipSpaces() method can handle null strings - [Test] - public void CanSkipSpacesInNullString() { - int index = 0; - Assert.DoesNotThrow( - delegate() { ParserHelper.SkipSpaces((string)null, ref index); } - ); - Assert.AreEqual(0, index); - } - - /// Ensures that the SkipSpaces() method can handle empty strings - [Test] - public void CanSkipSpacesInEmptyString() { - int index = 0; - Assert.DoesNotThrow( - delegate() { ParserHelper.SkipSpaces(string.Empty, ref index); } - ); - Assert.AreEqual(0, index); - } - - /// Ensures that the SkipSpaces() method can skip spaces - [Test] - public void SpacesCanBeSkipped() { - int index = 7; - ParserHelper.SkipSpaces(" Test Test ", ref index); - Assert.AreEqual(10, index); - } - - /// Ensures that the SkipNonSpaces() method can handle null strings - [Test] - public void CanSkipNonSpacesInNullString() { - int index = 0; - Assert.DoesNotThrow( - delegate() { ParserHelper.SkipNonSpaces((string)null, ref index); } - ); - Assert.AreEqual(0, index); - } - - /// Ensures that the SkipNonSpaces() method can handle empty strings - [Test] - public void CanSkipNonSpacesInEmptyString() { - int index = 0; - Assert.DoesNotThrow( - delegate() { ParserHelper.SkipNonSpaces(string.Empty, ref index); } - ); - Assert.AreEqual(0, index); - } - - /// Ensures that the SkipNonSpaces() method can skip non-space characters - [Test] - public void NonSpacesCanBeSkipped() { - int index = 7; - ParserHelper.SkipNonSpaces("Test Test Test", ref index); - Assert.AreEqual(11, index); - } - - /// Ensures that the SkipNumbers() method can handle null strings - [Test] - public void CanSkipNumbersInNullString() { - int index = 0; - Assert.DoesNotThrow( - delegate() { ParserHelper.SkipNumericals((string)null, ref index); } - ); - Assert.AreEqual(0, index); - } - - /// Ensures that the SkipNumbers() method can handle empty strings - [Test] - public void CanSkipNumbersInEmptyString() { - int index = 0; - Assert.DoesNotThrow( - delegate() { ParserHelper.SkipNumericals(string.Empty, ref index); } - ); - Assert.AreEqual(0, index); - } - - /// Ensures that the SkipNumbers() method can skip numbers - [Test] - public void NumbersCanBeSkipped() { - int index = 6; - ParserHelper.SkipNumericals("123abc456def789", ref index); - Assert.AreEqual(9, index); - } - - /// Ensures that the SkipIntegers() method can handle null strings - [Test] - public void CanSkipIntegersInNullString() { - int index = 0; - Assert.IsFalse(ParserHelper.SkipInteger((string)null, ref index)); - Assert.AreEqual(0, index); - } - - /// Ensures that the SkipNumbers() method can handle empty strings - [Test] - public void CanSkipIntegersInEmptyString() { - int index = 0; - Assert.IsFalse(ParserHelper.SkipInteger(string.Empty, ref index)); - Assert.AreEqual(0, index); - } - - /// Verifies that a prefix alone can not be skipped as an integer - [Test] - public void PrefixAloneIsNotAnInteger() { - int index = 0; - Assert.IsFalse(ParserHelper.SkipInteger("+Test", ref index)); - Assert.AreEqual(0, index); - Assert.IsFalse(ParserHelper.SkipInteger("-", ref index)); - Assert.AreEqual(0, index); - } - - /// Verifies that a prefixed integer can be skipped - [Test] - public void PrefixedIntegersCanBeSkipped() { - int index = 0; - Assert.IsTrue(ParserHelper.SkipInteger("+123", ref index)); - Assert.AreEqual(4, index); - } - - /// Verifies that an integer without a prefix can be skipped - [Test] - public void PlainIntegersCanBeSkipped() { - int index = 0; - Assert.IsTrue(ParserHelper.SkipInteger("12345", ref index)); - Assert.AreEqual(5, index); - } - - /// - /// Verifies that trying to skip text as if it was an integer skips nothing - /// - [Test] - public void SkippingTextAsIntegerReturnsFalse() { - int index = 0; - Assert.IsFalse(ParserHelper.SkipInteger("hello", ref index)); - Assert.AreEqual(0, index); - } - - /// Ensures that the SkipIntegers() method can handle null strings - [Test] - public void CanSkipStringInNullString() { - int index = 0; - Assert.IsFalse(ParserHelper.SkipString((string)null, ref index)); - Assert.AreEqual(0, index); - } - - /// Ensures that the SkipNumbers() method can handle empty strings - [Test] - public void CanSkipStringInEmptyString() { - int index = 0; - Assert.IsFalse(ParserHelper.SkipString(string.Empty, ref index)); - Assert.AreEqual(0, index); - } - - /// Verifies that a string consisting of a single word can be skipped - [Test] - public void SingleWordStringsCanBeSkipped() { - int index = 0; - Assert.IsTrue(ParserHelper.SkipString("hello", ref index)); - Assert.AreEqual(5, index); - } - - /// - /// Verifies that a space character is not skipped over when skipping a string - /// - [Test] - public void SpaceTerminatesUnquotedStrings() { - int index = 0; - Assert.IsTrue(ParserHelper.SkipString("hello world", ref index)); - Assert.AreEqual(5, index); - } - - /// Verifies that a string in quotes continues until the closing quote - [Test] - public void QuotedStringsCanBeSkipped() { - int index = 0; - Assert.IsTrue(ParserHelper.SkipString("\"This is a test\"", ref index)); - Assert.AreEqual(16, index); - } - - /// Verifies that a string in quotes continues until the closing quote - [Test] - public void QuotedStringsStopAtClosingQuote() { - int index = 0; - Assert.IsTrue(ParserHelper.SkipString("\"This is a test\" but this not.", ref index)); - Assert.AreEqual(16, index); - } - - /// Verifies that a string in quotes continues until the closing quote - [Test] - public void QuotedStringRequiresClosingQuote() { - int index = 0; - Assert.IsFalse(ParserHelper.SkipString("\"This is missing the closing quote", ref index)); - Assert.AreEqual(0, index); - } - - } - -} // namespace Nuclex.Support.Parsing - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.Collections.Generic; +using System.Text; + +using NUnit.Framework; + +namespace Nuclex.Support.Parsing { + + /// Verifies that the parser helper methods are correct + [TestFixture] + internal class ParserHelperTest { + + /// Ensures that the SkipSpaces() method can handle null strings + [Test] + public void CanSkipSpacesInNullString() { + int index = 0; + Assert.DoesNotThrow( + delegate() { ParserHelper.SkipSpaces((string)null, ref index); } + ); + Assert.AreEqual(0, index); + } + + /// Ensures that the SkipSpaces() method can handle empty strings + [Test] + public void CanSkipSpacesInEmptyString() { + int index = 0; + Assert.DoesNotThrow( + delegate() { ParserHelper.SkipSpaces(string.Empty, ref index); } + ); + Assert.AreEqual(0, index); + } + + /// Ensures that the SkipSpaces() method can skip spaces + [Test] + public void SpacesCanBeSkipped() { + int index = 7; + ParserHelper.SkipSpaces(" Test Test ", ref index); + Assert.AreEqual(10, index); + } + + /// Ensures that the SkipNonSpaces() method can handle null strings + [Test] + public void CanSkipNonSpacesInNullString() { + int index = 0; + Assert.DoesNotThrow( + delegate() { ParserHelper.SkipNonSpaces((string)null, ref index); } + ); + Assert.AreEqual(0, index); + } + + /// Ensures that the SkipNonSpaces() method can handle empty strings + [Test] + public void CanSkipNonSpacesInEmptyString() { + int index = 0; + Assert.DoesNotThrow( + delegate() { ParserHelper.SkipNonSpaces(string.Empty, ref index); } + ); + Assert.AreEqual(0, index); + } + + /// Ensures that the SkipNonSpaces() method can skip non-space characters + [Test] + public void NonSpacesCanBeSkipped() { + int index = 7; + ParserHelper.SkipNonSpaces("Test Test Test", ref index); + Assert.AreEqual(11, index); + } + + /// Ensures that the SkipNumbers() method can handle null strings + [Test] + public void CanSkipNumbersInNullString() { + int index = 0; + Assert.DoesNotThrow( + delegate() { ParserHelper.SkipNumericals((string)null, ref index); } + ); + Assert.AreEqual(0, index); + } + + /// Ensures that the SkipNumbers() method can handle empty strings + [Test] + public void CanSkipNumbersInEmptyString() { + int index = 0; + Assert.DoesNotThrow( + delegate() { ParserHelper.SkipNumericals(string.Empty, ref index); } + ); + Assert.AreEqual(0, index); + } + + /// Ensures that the SkipNumbers() method can skip numbers + [Test] + public void NumbersCanBeSkipped() { + int index = 6; + ParserHelper.SkipNumericals("123abc456def789", ref index); + Assert.AreEqual(9, index); + } + + /// Ensures that the SkipIntegers() method can handle null strings + [Test] + public void CanSkipIntegersInNullString() { + int index = 0; + Assert.IsFalse(ParserHelper.SkipInteger((string)null, ref index)); + Assert.AreEqual(0, index); + } + + /// Ensures that the SkipNumbers() method can handle empty strings + [Test] + public void CanSkipIntegersInEmptyString() { + int index = 0; + Assert.IsFalse(ParserHelper.SkipInteger(string.Empty, ref index)); + Assert.AreEqual(0, index); + } + + /// Verifies that a prefix alone can not be skipped as an integer + [Test] + public void PrefixAloneIsNotAnInteger() { + int index = 0; + Assert.IsFalse(ParserHelper.SkipInteger("+Test", ref index)); + Assert.AreEqual(0, index); + Assert.IsFalse(ParserHelper.SkipInteger("-", ref index)); + Assert.AreEqual(0, index); + } + + /// Verifies that a prefixed integer can be skipped + [Test] + public void PrefixedIntegersCanBeSkipped() { + int index = 0; + Assert.IsTrue(ParserHelper.SkipInteger("+123", ref index)); + Assert.AreEqual(4, index); + } + + /// Verifies that an integer without a prefix can be skipped + [Test] + public void PlainIntegersCanBeSkipped() { + int index = 0; + Assert.IsTrue(ParserHelper.SkipInteger("12345", ref index)); + Assert.AreEqual(5, index); + } + + /// + /// Verifies that trying to skip text as if it was an integer skips nothing + /// + [Test] + public void SkippingTextAsIntegerReturnsFalse() { + int index = 0; + Assert.IsFalse(ParserHelper.SkipInteger("hello", ref index)); + Assert.AreEqual(0, index); + } + + /// Ensures that the SkipIntegers() method can handle null strings + [Test] + public void CanSkipStringInNullString() { + int index = 0; + Assert.IsFalse(ParserHelper.SkipString((string)null, ref index)); + Assert.AreEqual(0, index); + } + + /// Ensures that the SkipNumbers() method can handle empty strings + [Test] + public void CanSkipStringInEmptyString() { + int index = 0; + Assert.IsFalse(ParserHelper.SkipString(string.Empty, ref index)); + Assert.AreEqual(0, index); + } + + /// Verifies that a string consisting of a single word can be skipped + [Test] + public void SingleWordStringsCanBeSkipped() { + int index = 0; + Assert.IsTrue(ParserHelper.SkipString("hello", ref index)); + Assert.AreEqual(5, index); + } + + /// + /// Verifies that a space character is not skipped over when skipping a string + /// + [Test] + public void SpaceTerminatesUnquotedStrings() { + int index = 0; + Assert.IsTrue(ParserHelper.SkipString("hello world", ref index)); + Assert.AreEqual(5, index); + } + + /// Verifies that a string in quotes continues until the closing quote + [Test] + public void QuotedStringsCanBeSkipped() { + int index = 0; + Assert.IsTrue(ParserHelper.SkipString("\"This is a test\"", ref index)); + Assert.AreEqual(16, index); + } + + /// Verifies that a string in quotes continues until the closing quote + [Test] + public void QuotedStringsStopAtClosingQuote() { + int index = 0; + Assert.IsTrue(ParserHelper.SkipString("\"This is a test\" but this not.", ref index)); + Assert.AreEqual(16, index); + } + + /// Verifies that a string in quotes continues until the closing quote + [Test] + public void QuotedStringRequiresClosingQuote() { + int index = 0; + Assert.IsFalse(ParserHelper.SkipString("\"This is missing the closing quote", ref index)); + Assert.AreEqual(0, index); + } + + } + +} // namespace Nuclex.Support.Parsing + +#endif // UNITTEST diff --git a/Source/Parsing/ParserHelper.cs b/Source/Parsing/ParserHelper.cs index 22802d1..7cb3a14 100644 --- a/Source/Parsing/ParserHelper.cs +++ b/Source/Parsing/ParserHelper.cs @@ -1,181 +1,180 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; - -namespace Nuclex.Support.Parsing { - - /// Provides helper methods for parsers - public class ParserHelper { - - /// Advances the index past any whitespace in the string - /// String which is being indexed - /// Index that will be advanced - public static void SkipSpaces(string text, ref int index) { - if(text == null) { - return; - } - - int length = text.Length; - while(index < length) { - if(!char.IsWhiteSpace(text, index)) { - break; - } - - ++index; - } - } - - /// Advances the index to the next whitespace in the string - /// String which is being indexed - /// Index that will be advanced - public static void SkipNonSpaces(string text, ref int index) { - if(text == null) { - return; - } - - int length = text.Length; - while(index < length) { - if(char.IsWhiteSpace(text, index)) { - break; - } - - ++index; - } - } - - /// Advances the index to the next character that isn't numeric - /// String which is being indexed - /// Index that will be advanced - /// - /// This skips only numeric characters, but not complete numbers -- if the number - /// begins with a minus or plus sign, for example, this function will not skip it. - /// - public static void SkipNumericals(string text, ref int index) { - if(text == null) { - return; - } - - int length = text.Length; - while(index < length) { - if(!char.IsNumber(text, index)) { - break; - } - - ++index; - } - } - - /// Skips an integer in the provided string - /// String in which an integer will be skipped - /// Index at which the integer begins - /// True if an integer was found and skipped, otherwise false - public static bool SkipInteger(string text, ref int index) { - if(text == null) { - return false; - } - - int length = text.Length; - if(index >= length) { - return false; - } - - // If the number begins with a minus or plus sign, skip over the sign - int nextIndex; - if((text[index] == '-') || (text[index] == '+')) { - nextIndex = index + 1; - - SkipNumericals(text, ref nextIndex); - if(nextIndex == (index + 1)) { - return false; - } - } else { - nextIndex = index; - - SkipNumericals(text, ref nextIndex); - if(nextIndex == index) { - return false; - } - } - - index = nextIndex; - return true; - } - - /// Skips a string appearing in the input text - /// Text in which a string will be skipped - /// Index at which the string begins - /// True if a string was found and skipped, otherwise false - public static bool SkipString(string text, ref int index) { - if(text == null) { - return false; - } - - int length = text.Length; - if(index >= length) { - return false; - } - - // If the string begins with an opening quote, look for the closing quote - if(text[index] == '"') { - - int endIndex = text.IndexOf('"', index + 1); - if(endIndex == -1) { - return false; - } - - index = endIndex + 1; - return true; - - } else { // Normal strings end with the first whitespace - - int startIndex = index; - SkipNonSpaces(text, ref index); - - return (index != startIndex); - - } - } - - /// Skips a floating point value appearing in the input text - /// Text in which a floating point value will be skipped - /// Index at which the floating point value begins - /// True if the floating point value was skipped, otherwise false - public static bool SkipFloat(string text, ref int index) { - if(SkipInteger(text, ref index)) { - if(index < text.Length) { - if(text[index] == '.') { - ++index; - SkipNumericals(text, ref index); - } - if((text[index] == 'e') || (text[index] == 'E')) { - throw new NotImplementedException("Exponential format not supported yet"); - } - } - - return true; - } - - return false; - } - - } - -} // namespace Nuclex.Support.Parsing +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +namespace Nuclex.Support.Parsing { + + /// Provides helper methods for parsers + public class ParserHelper { + + /// Advances the index past any whitespace in the string + /// String which is being indexed + /// Index that will be advanced + public static void SkipSpaces(string text, ref int index) { + if(text == null) { + return; + } + + int length = text.Length; + while(index < length) { + if(!char.IsWhiteSpace(text, index)) { + break; + } + + ++index; + } + } + + /// Advances the index to the next whitespace in the string + /// String which is being indexed + /// Index that will be advanced + public static void SkipNonSpaces(string text, ref int index) { + if(text == null) { + return; + } + + int length = text.Length; + while(index < length) { + if(char.IsWhiteSpace(text, index)) { + break; + } + + ++index; + } + } + + /// Advances the index to the next character that isn't numeric + /// String which is being indexed + /// Index that will be advanced + /// + /// This skips only numeric characters, but not complete numbers -- if the number + /// begins with a minus or plus sign, for example, this function will not skip it. + /// + public static void SkipNumericals(string text, ref int index) { + if(text == null) { + return; + } + + int length = text.Length; + while(index < length) { + if(!char.IsNumber(text, index)) { + break; + } + + ++index; + } + } + + /// Skips an integer in the provided string + /// String in which an integer will be skipped + /// Index at which the integer begins + /// True if an integer was found and skipped, otherwise false + public static bool SkipInteger(string text, ref int index) { + if(text == null) { + return false; + } + + int length = text.Length; + if(index >= length) { + return false; + } + + // If the number begins with a minus or plus sign, skip over the sign + int nextIndex; + if((text[index] == '-') || (text[index] == '+')) { + nextIndex = index + 1; + + SkipNumericals(text, ref nextIndex); + if(nextIndex == (index + 1)) { + return false; + } + } else { + nextIndex = index; + + SkipNumericals(text, ref nextIndex); + if(nextIndex == index) { + return false; + } + } + + index = nextIndex; + return true; + } + + /// Skips a string appearing in the input text + /// Text in which a string will be skipped + /// Index at which the string begins + /// True if a string was found and skipped, otherwise false + public static bool SkipString(string text, ref int index) { + if(text == null) { + return false; + } + + int length = text.Length; + if(index >= length) { + return false; + } + + // If the string begins with an opening quote, look for the closing quote + if(text[index] == '"') { + + int endIndex = text.IndexOf('"', index + 1); + if(endIndex == -1) { + return false; + } + + index = endIndex + 1; + return true; + + } else { // Normal strings end with the first whitespace + + int startIndex = index; + SkipNonSpaces(text, ref index); + + return (index != startIndex); + + } + } + + /// Skips a floating point value appearing in the input text + /// Text in which a floating point value will be skipped + /// Index at which the floating point value begins + /// True if the floating point value was skipped, otherwise false + public static bool SkipFloat(string text, ref int index) { + if(SkipInteger(text, ref index)) { + if(index < text.Length) { + if(text[index] == '.') { + ++index; + SkipNumericals(text, ref index); + } + if((text[index] == 'e') || (text[index] == 'E')) { + throw new NotImplementedException("Exponential format not supported yet"); + } + } + + return true; + } + + return false; + } + + } + +} // namespace Nuclex.Support.Parsing diff --git a/Source/PathHelper.Test.cs b/Source/PathHelper.Test.cs index 113a3b0..686c994 100644 --- a/Source/PathHelper.Test.cs +++ b/Source/PathHelper.Test.cs @@ -1,247 +1,246 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.IO; - -#if UNITTEST - -using NUnit.Framework; - -namespace Nuclex.Support { - - /// Unit Test for the path helper class - [TestFixture] - internal class PathHelperTest { - - /// - /// Tests whether the relative path creator keeps the absolute path if - /// the location being passed is not relative to the base path. - /// - [Test] - public void TestRelativeWindowsPathOfNonRelativePath() { - Assert.That( - PathHelper.MakeRelative( - platformify("C:/Folder1/Folder2"), - platformify("D:/Folder1/Folder2") - ), - Is.EqualTo(platformify("D:/Folder1/Folder2")) - ); - Assert.That( - PathHelper.MakeRelative( - platformify("C:/Folder1/Folder2/"), - platformify("D:/Folder1/Folder2/") - ), - Is.EqualTo(platformify("D:/Folder1/Folder2/")) - ); - - } - - /// - /// Tests whether the relative path creator correctly builds the relative - /// path to the parent folder of the base path for windows paths. - /// - [Test] - public void TestRelativeWindowsPathToParentFolder() { - Assert.That( - PathHelper.MakeRelative( - platformify("C:/Folder1/Folder2"), - platformify("C:/Folder1") - ), - Is.EqualTo(platformify("..")) - ); - Assert.That( - PathHelper.MakeRelative( - platformify("C:/Folder1/Folder2/"), - platformify("C:/Folder1/") - ), - Is.EqualTo(platformify("../")) - ); - } - - /// - /// Tests whether the relative path creator correctly builds the relative path to - /// the parent folder of the base path for windows paths with more than one level. - /// - [Test] - public void TestRelativeWindowsPathToParentFolderTwoLevels() { - Assert.That( - PathHelper.MakeRelative( - platformify("C:/Folder1/Folder2/Folder3"), - platformify("C:/Folder1") - ), - Is.EqualTo(platformify("../..")) - ); - Assert.That( - PathHelper.MakeRelative( - platformify("C:/Folder1/Folder2/Folder3/"), - platformify("C:/Folder1/") - ), - Is.EqualTo(platformify("../../")) - ); - } - - - /// - /// Tests whether the relative path creator correctly builds the relative - /// path to the parent folder of the base path for unix paths. - /// - [Test] - public void TestRelativeUnixPathToParentFolder() { - Assert.That( - PathHelper.MakeRelative( - platformify("/Folder1/Folder2"), - platformify("/Folder1") - ), - Is.EqualTo(platformify("..")) - ); - Assert.That( - PathHelper.MakeRelative( - platformify("/Folder1/Folder2/"), - platformify("/Folder1/") - ), - Is.EqualTo(platformify("../")) - ); - } - - /// - /// Tests whether the relative path creator correctly builds the relative path to - /// the parent folder of the base path for unix paths with more than one level. - /// - [Test] - public void TestRelativeUnixPathToParentFolderTwoLevels() { - Assert.That( - PathHelper.MakeRelative( - platformify("/Folder1/Folder2/Folder3"), - platformify("/Folder1") - ), - Is.EqualTo(platformify("../..")) - ); - Assert.That( - PathHelper.MakeRelative( - platformify("/Folder1/Folder2/Folder3/"), - platformify("/Folder1/") - ), - Is.EqualTo(platformify("../../")) - ); - } - - /// - /// Tests whether the relative path creator correctly builds the relative - /// path to a nested folder in the base path for windows paths. - /// - [Test] - public void TestRelativeWindowsPathToNestedFolder() { - Assert.That( - PathHelper.MakeRelative( - platformify("C:/Folder1"), - platformify("C:/Folder1/Folder2") - ), - Is.EqualTo(platformify("Folder2")) - ); - Assert.That( - PathHelper.MakeRelative( - platformify("C:/Folder1/"), - platformify("C:/Folder1/Folder2/") - ), - Is.EqualTo(platformify("Folder2/")) - ); - } - - /// - /// Tests whether the relative path creator correctly builds the relative - /// path to a nested folder in the base path for unix paths. - /// - [Test] - public void TestRelativeUnixPathToNestedFolder() { - Assert.That( - PathHelper.MakeRelative( - platformify("/Folder1"), - platformify("/Folder1/Folder2") - ), - Is.EqualTo(platformify("Folder2")) - ); - Assert.That( - PathHelper.MakeRelative( - platformify("/Folder1/"), - platformify("/Folder1/Folder2/") - ), - Is.EqualTo(platformify("Folder2/")) - ); - } - - /// - /// Tests whether the relative path creator correctly builds the relative - /// path to another folder on the same level as base path for windows paths. - /// - [Test] - public void TestRelativeWindowsPathToSiblingFolder() { - Assert.That( - PathHelper.MakeRelative( - platformify("C:/Folder1/Folder2/"), - platformify("C:/Folder1/Folder2345") - ), - Is.EqualTo(platformify("../Folder2345")) - ); - Assert.That( - PathHelper.MakeRelative( - platformify("C:/Folder1/Folder2345/"), - platformify("C:/Folder1/Folder2") - ), - Is.EqualTo(platformify("../Folder2")) - ); - } - - /// - /// Tests whether the relative path creator correctly builds the relative - /// path to another folder on the same level as base path for unix paths. - /// - [Test] - public void TestRelativeUnixPathToSiblingFolder() { - Assert.That( - PathHelper.MakeRelative( - platformify("/Folder1/Folder2/"), - platformify("/Folder1/Folder2345") - ), - Is.EqualTo(platformify("../Folder2345")) - ); - Assert.That( - PathHelper.MakeRelative( - platformify("/Folder1/Folder2345/"), - platformify("/Folder1/Folder2") - ), - Is.EqualTo(platformify("../Folder2")) - ); - } - - /// - /// Converts unix-style directory separators into the format used by the current platform - /// - /// Path to converts into the platform-dependent format - /// Platform-specific version of the provided unix-style path - private string platformify(string path) { - return path.Replace('/', Path.DirectorySeparatorChar); - } - - } - -} // namespace Nuclex.Support - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.IO; + +#if UNITTEST + +using NUnit.Framework; + +namespace Nuclex.Support { + + /// Unit Test for the path helper class + [TestFixture] + internal class PathHelperTest { + + /// + /// Tests whether the relative path creator keeps the absolute path if + /// the location being passed is not relative to the base path. + /// + [Test] + public void TestRelativeWindowsPathOfNonRelativePath() { + Assert.That( + PathHelper.MakeRelative( + platformify("C:/Folder1/Folder2"), + platformify("D:/Folder1/Folder2") + ), + Is.EqualTo(platformify("D:/Folder1/Folder2")) + ); + Assert.That( + PathHelper.MakeRelative( + platformify("C:/Folder1/Folder2/"), + platformify("D:/Folder1/Folder2/") + ), + Is.EqualTo(platformify("D:/Folder1/Folder2/")) + ); + + } + + /// + /// Tests whether the relative path creator correctly builds the relative + /// path to the parent folder of the base path for windows paths. + /// + [Test] + public void TestRelativeWindowsPathToParentFolder() { + Assert.That( + PathHelper.MakeRelative( + platformify("C:/Folder1/Folder2"), + platformify("C:/Folder1") + ), + Is.EqualTo(platformify("..")) + ); + Assert.That( + PathHelper.MakeRelative( + platformify("C:/Folder1/Folder2/"), + platformify("C:/Folder1/") + ), + Is.EqualTo(platformify("../")) + ); + } + + /// + /// Tests whether the relative path creator correctly builds the relative path to + /// the parent folder of the base path for windows paths with more than one level. + /// + [Test] + public void TestRelativeWindowsPathToParentFolderTwoLevels() { + Assert.That( + PathHelper.MakeRelative( + platformify("C:/Folder1/Folder2/Folder3"), + platformify("C:/Folder1") + ), + Is.EqualTo(platformify("../..")) + ); + Assert.That( + PathHelper.MakeRelative( + platformify("C:/Folder1/Folder2/Folder3/"), + platformify("C:/Folder1/") + ), + Is.EqualTo(platformify("../../")) + ); + } + + + /// + /// Tests whether the relative path creator correctly builds the relative + /// path to the parent folder of the base path for unix paths. + /// + [Test] + public void TestRelativeUnixPathToParentFolder() { + Assert.That( + PathHelper.MakeRelative( + platformify("/Folder1/Folder2"), + platformify("/Folder1") + ), + Is.EqualTo(platformify("..")) + ); + Assert.That( + PathHelper.MakeRelative( + platformify("/Folder1/Folder2/"), + platformify("/Folder1/") + ), + Is.EqualTo(platformify("../")) + ); + } + + /// + /// Tests whether the relative path creator correctly builds the relative path to + /// the parent folder of the base path for unix paths with more than one level. + /// + [Test] + public void TestRelativeUnixPathToParentFolderTwoLevels() { + Assert.That( + PathHelper.MakeRelative( + platformify("/Folder1/Folder2/Folder3"), + platformify("/Folder1") + ), + Is.EqualTo(platformify("../..")) + ); + Assert.That( + PathHelper.MakeRelative( + platformify("/Folder1/Folder2/Folder3/"), + platformify("/Folder1/") + ), + Is.EqualTo(platformify("../../")) + ); + } + + /// + /// Tests whether the relative path creator correctly builds the relative + /// path to a nested folder in the base path for windows paths. + /// + [Test] + public void TestRelativeWindowsPathToNestedFolder() { + Assert.That( + PathHelper.MakeRelative( + platformify("C:/Folder1"), + platformify("C:/Folder1/Folder2") + ), + Is.EqualTo(platformify("Folder2")) + ); + Assert.That( + PathHelper.MakeRelative( + platformify("C:/Folder1/"), + platformify("C:/Folder1/Folder2/") + ), + Is.EqualTo(platformify("Folder2/")) + ); + } + + /// + /// Tests whether the relative path creator correctly builds the relative + /// path to a nested folder in the base path for unix paths. + /// + [Test] + public void TestRelativeUnixPathToNestedFolder() { + Assert.That( + PathHelper.MakeRelative( + platformify("/Folder1"), + platformify("/Folder1/Folder2") + ), + Is.EqualTo(platformify("Folder2")) + ); + Assert.That( + PathHelper.MakeRelative( + platformify("/Folder1/"), + platformify("/Folder1/Folder2/") + ), + Is.EqualTo(platformify("Folder2/")) + ); + } + + /// + /// Tests whether the relative path creator correctly builds the relative + /// path to another folder on the same level as base path for windows paths. + /// + [Test] + public void TestRelativeWindowsPathToSiblingFolder() { + Assert.That( + PathHelper.MakeRelative( + platformify("C:/Folder1/Folder2/"), + platformify("C:/Folder1/Folder2345") + ), + Is.EqualTo(platformify("../Folder2345")) + ); + Assert.That( + PathHelper.MakeRelative( + platformify("C:/Folder1/Folder2345/"), + platformify("C:/Folder1/Folder2") + ), + Is.EqualTo(platformify("../Folder2")) + ); + } + + /// + /// Tests whether the relative path creator correctly builds the relative + /// path to another folder on the same level as base path for unix paths. + /// + [Test] + public void TestRelativeUnixPathToSiblingFolder() { + Assert.That( + PathHelper.MakeRelative( + platformify("/Folder1/Folder2/"), + platformify("/Folder1/Folder2345") + ), + Is.EqualTo(platformify("../Folder2345")) + ); + Assert.That( + PathHelper.MakeRelative( + platformify("/Folder1/Folder2345/"), + platformify("/Folder1/Folder2") + ), + Is.EqualTo(platformify("../Folder2")) + ); + } + + /// + /// Converts unix-style directory separators into the format used by the current platform + /// + /// Path to converts into the platform-dependent format + /// Platform-specific version of the provided unix-style path + private string platformify(string path) { + return path.Replace('/', Path.DirectorySeparatorChar); + } + + } + +} // namespace Nuclex.Support + +#endif // UNITTEST diff --git a/Source/PathHelper.cs b/Source/PathHelper.cs index 71f48fa..eaba98b 100644 --- a/Source/PathHelper.cs +++ b/Source/PathHelper.cs @@ -1,95 +1,94 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Text; -using System.IO; - -namespace Nuclex.Support { - - /// Utility class for path operations - public static class PathHelper { - - /// Converts an absolute path into a relative one - /// Base directory the new path should be relative to - /// Absolute path that will be made relative - /// - /// A path relative to the indicated base directory that matches the - /// absolute path given. - /// - public static string MakeRelative(string basePath, string absolutePath) { - string[] baseDirectories = basePath.Split(Path.DirectorySeparatorChar); - string[] absoluteDirectories = absolutePath.Split(Path.DirectorySeparatorChar); - - // Find the common root path of both paths so we know from which point on - // the two paths will differ - int lastCommonRoot = -1; - int commonLength = Math.Min(baseDirectories.Length, absoluteDirectories.Length); - for(int index = 0; index < commonLength; ++index) { - if(absoluteDirectories[index] == baseDirectories[index]) { - lastCommonRoot = index; - } else { - break; - } - } - - // If the paths don't share a common root, we have to use an absolute path. - // Should the absolutePath parameter actually be a relative path, this will - // also trigger the return of the absolutePath as-is. - if(lastCommonRoot == -1) { - return absolutePath; - } - - // Calculate the required length for the StringBuilder in order to be slightly - // more friendly in terms of memory usage. - int requiredLength = (baseDirectories.Length - (lastCommonRoot + 1)) * 3; - for(int index = lastCommonRoot + 1; index < absoluteDirectories.Length; ++index) { - requiredLength += absoluteDirectories[index].Length + 1; - } - - StringBuilder relativePath = new StringBuilder(requiredLength); - - // Go to the common path by adding .. until we're where we want to be - for(int index = lastCommonRoot + 1; index < baseDirectories.Length; ++index) { - if(baseDirectories[index].Length > 0) { - if(relativePath.Length > 0) { // We don't want the path to start with a slash - relativePath.Append(Path.DirectorySeparatorChar); - } - - relativePath.Append(".."); - } - } - - // Now that we're in the common root folder, enter the folders that - // the absolute target path has in addition to the root folder. - for(int index = lastCommonRoot + 1; index < absoluteDirectories.Length; index++) { - if(relativePath.Length > 0) { // We don't want the path to start with a slash - relativePath.Append(Path.DirectorySeparatorChar); - } - - relativePath.Append(absoluteDirectories[index]); - } - - return relativePath.ToString(); - } - - } - -} // namespace Nuclex.Support +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Text; +using System.IO; + +namespace Nuclex.Support { + + /// Utility class for path operations + public static class PathHelper { + + /// Converts an absolute path into a relative one + /// Base directory the new path should be relative to + /// Absolute path that will be made relative + /// + /// A path relative to the indicated base directory that matches the + /// absolute path given. + /// + public static string MakeRelative(string basePath, string absolutePath) { + string[] baseDirectories = basePath.Split(Path.DirectorySeparatorChar); + string[] absoluteDirectories = absolutePath.Split(Path.DirectorySeparatorChar); + + // Find the common root path of both paths so we know from which point on + // the two paths will differ + int lastCommonRoot = -1; + int commonLength = Math.Min(baseDirectories.Length, absoluteDirectories.Length); + for(int index = 0; index < commonLength; ++index) { + if(absoluteDirectories[index] == baseDirectories[index]) { + lastCommonRoot = index; + } else { + break; + } + } + + // If the paths don't share a common root, we have to use an absolute path. + // Should the absolutePath parameter actually be a relative path, this will + // also trigger the return of the absolutePath as-is. + if(lastCommonRoot == -1) { + return absolutePath; + } + + // Calculate the required length for the StringBuilder in order to be slightly + // more friendly in terms of memory usage. + int requiredLength = (baseDirectories.Length - (lastCommonRoot + 1)) * 3; + for(int index = lastCommonRoot + 1; index < absoluteDirectories.Length; ++index) { + requiredLength += absoluteDirectories[index].Length + 1; + } + + StringBuilder relativePath = new StringBuilder(requiredLength); + + // Go to the common path by adding .. until we're where we want to be + for(int index = lastCommonRoot + 1; index < baseDirectories.Length; ++index) { + if(baseDirectories[index].Length > 0) { + if(relativePath.Length > 0) { // We don't want the path to start with a slash + relativePath.Append(Path.DirectorySeparatorChar); + } + + relativePath.Append(".."); + } + } + + // Now that we're in the common root folder, enter the folders that + // the absolute target path has in addition to the root folder. + for(int index = lastCommonRoot + 1; index < absoluteDirectories.Length; index++) { + if(relativePath.Length > 0) { // We don't want the path to start with a slash + relativePath.Append(Path.DirectorySeparatorChar); + } + + relativePath.Append(absoluteDirectories[index]); + } + + return relativePath.ToString(); + } + + } + +} // namespace Nuclex.Support diff --git a/Source/PropertyChangedEventArgsHelper.Test.cs b/Source/PropertyChangedEventArgsHelper.Test.cs index a429ba6..b8cd871 100644 --- a/Source/PropertyChangedEventArgsHelper.Test.cs +++ b/Source/PropertyChangedEventArgsHelper.Test.cs @@ -1,125 +1,124 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.ComponentModel; - -using NUnit.Framework; - -namespace Nuclex.Support { - - /// Unit tests for the property change event argument helper - [TestFixture] - internal class PropertyChangedEventArgsHelperTest { - - #region class TestViewModel - - /// Example class on which unit test generates change notifications - public class TestViewModel { - - /// Example property that will be reported to have changed - public int SomeProperty { get; set; } - - } - - #endregion // class TestViewModel - - /// - /// Verifies that a property change notification matching the property - /// passed to the AreAffecting() method is recognized - /// - [Test] - public void MatchingPropertyChangeNotificationIsRecognized() { - var arguments = new PropertyChangedEventArgs("SomeProperty"); - #pragma warning disable 0618 - Assert.IsTrue(arguments.AreAffecting(() => ViewModel.SomeProperty)); - #pragma warning restore 0618 - Assert.IsTrue(arguments.AreAffecting("SomeProperty")); - } - - /// - /// Ensures that a mismatching property change notification will - /// not report the property as being affected. - /// - [Test] - public void MismatchingPropertyIsReportedAsUnaffected() { - var arguments = new PropertyChangedEventArgs("AnotherProperty"); - #pragma warning disable 0618 - Assert.IsFalse(arguments.AreAffecting(() => ViewModel.SomeProperty)); - #pragma warning restore 0618 - Assert.IsFalse(arguments.AreAffecting("SomeProperty")); - } - - /// - /// Verifies that any specific property is reported as being affected - /// when the property change notification is a null wildcard - /// - [Test] - public void SpecificPropertyIsAffectedByNullWildcard() { - var nullArguments = new PropertyChangedEventArgs(null); - #pragma warning disable 0618 - Assert.IsTrue(nullArguments.AreAffecting(() => ViewModel.SomeProperty)); - #pragma warning restore 0618 - Assert.IsTrue(nullArguments.AreAffecting("SomeProperty")); - } - - /// - /// Verifies that any specific property is reported as being affected - /// when the property change notification is an empty wildcard - /// - [Test] - public void SpecificPropertyIsAffectedByEmptyWildcard() { - var emptyArguments = new PropertyChangedEventArgs(string.Empty); - #pragma warning disable 0618 - Assert.IsTrue(emptyArguments.AreAffecting(() => ViewModel.SomeProperty)); - #pragma warning disable 0618 - Assert.IsTrue(emptyArguments.AreAffecting("SomeProperty")); - } - - /// - /// Tests whether the helper can recognize a wildcard property change - /// notification using null as the wildcard. - /// - [Test] - public void NullWildcardIsRecognized() { - var nullArguments = new PropertyChangedEventArgs(null); - Assert.IsTrue(nullArguments.AffectAllProperties()); - } - - /// - /// Tests whether the helper can recognize a wildcard property change - /// notification using an empty string as the wildcard. - /// - [Test] - public void EmptyWildcardIsRecognized() { - var emptyArguments = new PropertyChangedEventArgs(string.Empty); - Assert.IsTrue(emptyArguments.AffectAllProperties()); - } - - /// Helper used to construct lambda expressions - protected static TestViewModel ViewModel { get; set; } - - } - -} // namespace Nuclex.Support - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.ComponentModel; + +using NUnit.Framework; + +namespace Nuclex.Support { + + /// Unit tests for the property change event argument helper + [TestFixture] + internal class PropertyChangedEventArgsHelperTest { + + #region class TestViewModel + + /// Example class on which unit test generates change notifications + public class TestViewModel { + + /// Example property that will be reported to have changed + public int SomeProperty { get; set; } + + } + + #endregion // class TestViewModel + + /// + /// Verifies that a property change notification matching the property + /// passed to the AreAffecting() method is recognized + /// + [Test] + public void MatchingPropertyChangeNotificationIsRecognized() { + var arguments = new PropertyChangedEventArgs("SomeProperty"); + #pragma warning disable 0618 + Assert.IsTrue(arguments.AreAffecting(() => ViewModel.SomeProperty)); + #pragma warning restore 0618 + Assert.IsTrue(arguments.AreAffecting("SomeProperty")); + } + + /// + /// Ensures that a mismatching property change notification will + /// not report the property as being affected. + /// + [Test] + public void MismatchingPropertyIsReportedAsUnaffected() { + var arguments = new PropertyChangedEventArgs("AnotherProperty"); + #pragma warning disable 0618 + Assert.IsFalse(arguments.AreAffecting(() => ViewModel.SomeProperty)); + #pragma warning restore 0618 + Assert.IsFalse(arguments.AreAffecting("SomeProperty")); + } + + /// + /// Verifies that any specific property is reported as being affected + /// when the property change notification is a null wildcard + /// + [Test] + public void SpecificPropertyIsAffectedByNullWildcard() { + var nullArguments = new PropertyChangedEventArgs(null); + #pragma warning disable 0618 + Assert.IsTrue(nullArguments.AreAffecting(() => ViewModel.SomeProperty)); + #pragma warning restore 0618 + Assert.IsTrue(nullArguments.AreAffecting("SomeProperty")); + } + + /// + /// Verifies that any specific property is reported as being affected + /// when the property change notification is an empty wildcard + /// + [Test] + public void SpecificPropertyIsAffectedByEmptyWildcard() { + var emptyArguments = new PropertyChangedEventArgs(string.Empty); + #pragma warning disable 0618 + Assert.IsTrue(emptyArguments.AreAffecting(() => ViewModel.SomeProperty)); + #pragma warning disable 0618 + Assert.IsTrue(emptyArguments.AreAffecting("SomeProperty")); + } + + /// + /// Tests whether the helper can recognize a wildcard property change + /// notification using null as the wildcard. + /// + [Test] + public void NullWildcardIsRecognized() { + var nullArguments = new PropertyChangedEventArgs(null); + Assert.IsTrue(nullArguments.AffectAllProperties()); + } + + /// + /// Tests whether the helper can recognize a wildcard property change + /// notification using an empty string as the wildcard. + /// + [Test] + public void EmptyWildcardIsRecognized() { + var emptyArguments = new PropertyChangedEventArgs(string.Empty); + Assert.IsTrue(emptyArguments.AffectAllProperties()); + } + + /// Helper used to construct lambda expressions + protected static TestViewModel ViewModel { get; set; } + + } + +} // namespace Nuclex.Support + +#endif // UNITTEST diff --git a/Source/PropertyChangedEventArgsHelper.cs b/Source/PropertyChangedEventArgsHelper.cs index d7c0812..fb10227 100644 --- a/Source/PropertyChangedEventArgsHelper.cs +++ b/Source/PropertyChangedEventArgsHelper.cs @@ -1,255 +1,254 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -#if NO_CONCURRENT_COLLECTIONS -using System.Collections.Generic; -#else -using System.Collections.Concurrent; -#endif -using System.ComponentModel; -#if !NO_LINQ_EXPRESSIONS -using System.Linq.Expressions; -#endif - -namespace Nuclex.Support { - - /// Contains helper methods for property change notifications - public static class PropertyChangedEventArgsHelper { - - /// - /// A property change event argument container that indicates that all - /// properties have changed their value. - /// - public static readonly PropertyChangedEventArgs Wildcard = - new PropertyChangedEventArgs(null); - - /// Initializes a new property changed argument helper - static PropertyChangedEventArgsHelper() { -#if NO_CONCURRENT_COLLECTIONS - cache = new Dictionary(); -#else - cache = new ConcurrentDictionary(); -#endif - } - -#if !NO_LINQ_EXPRESSIONS - /// - /// Provides a property change argument container for the specified property - /// - /// - /// Property for which an event argument container will be provided - /// - /// The event argument container for a property of the specified name - /// - /// - /// This method transparently caches instances of the argument containers - /// to avoid feeding the garbage collector. A typical application only has - /// in the order of tens to hundreds of different properties for which changes - /// will be reported, making a cache to avoid garbage collections viable. - /// - /// - /// - /// PropertyChangedEventArgs arguments = - /// PropertyChangedEventArgsHelper.GetArgumentsFor(() => SomeProperty); - /// - /// - /// - [Obsolete("Prefer the C# 'nameof()' operator to using a Linq expression")] - public static PropertyChangedEventArgs GetArgumentsFor( - Expression> property - ) { - return GetArgumentsFor(ObservableHelper.GetPropertyName(property)); - } -#endif - - /// - /// Provides a property change argument container for the specified property - /// - /// - /// Property for which an event argument container will be provided - /// - /// The event argument container for a property of the specified name - /// - /// - /// This method transparently caches instances of the argument containers - /// to avoid feeding the garbage collector. A typical application only has - /// in the order of tens to hundreds of different properties for which changes - /// will be reported, making a cache to avoid garbage collections viable. - /// - /// - /// - /// PropertyChangedEventArgs arguments = - /// PropertyChangedEventArgsHelper.GetArgumentsFor("SomeProperty"); - /// - /// - /// - public static PropertyChangedEventArgs GetArgumentsFor(string propertyName) { - if(string.IsNullOrEmpty(propertyName)) { - return Wildcard; - } - -#if NO_CONCURRENT_COLLECTIONS - lock(cache) { - // Try to reuse the change notification if an instance already exists - PropertyChangedEventArgs arguments; - if(!cache.TryGetValue(propertyName, out arguments)) { - arguments = new PropertyChangedEventArgs(propertyName); - cache.Add(propertyName, arguments); - } - - return arguments; - } -#else - // If an instance for this property already exists, just return it - PropertyChangedEventArgs arguments; - if(cache.TryGetValue(propertyName, out arguments)) { - return arguments; - } - - // No instance existed (at least a short moment ago), so create a new one - return cache.GetOrAdd(propertyName, new PropertyChangedEventArgs(propertyName)); -#endif - } - -#if !NO_LINQ_EXPRESSIONS - /// - /// Determines whether the property change affects the specified property - /// - /// - /// Type of the property that will be tested for being affected - /// - /// - /// Property change that has been reported by the observed object - /// - /// Property that will be tested for being affected - /// Whether the specified property is affected by the property change - /// - /// - /// By using this method, you can shorten the code needed to test whether - /// a property change notification affects a specific property. You also - /// avoid hardcoding the property name, which would have the adverse effect - /// of not updating the textual property names during F2 refactoring. - /// - /// - /// - /// private void propertyChanged(object sender, PropertyChangedEventArgs arguments) { - /// if(arguments.AreAffecting(() => ViewModel.DisplayedValue)) { - /// updateDisplayedValueFromViewModel(); - /// } // Do not use else if here or wildcards will not work - /// if(arguments.AreAffecting(() => ViewModel.OtherValue)) { - /// updateOtherValueFromViewModel(); - /// } - /// } - /// - /// - /// - [Obsolete("Prefer the C# 'nameof()' operator to using a Linq expression")] - public static bool AreAffecting( - this PropertyChangedEventArgs arguments, Expression> property - ) { - if(arguments.AffectAllProperties()) { - return true; - } - - string propertyName = ObservableHelper.GetPropertyName(property); - return (arguments.PropertyName == propertyName); - } -#endif - - /// - /// Determines whether the property change affects the specified property - /// - /// - /// Property change that has been reported by the observed object - /// - /// Property that will be tested for being affected - /// Whether the specified property is affected by the property change - /// - /// - /// By using this method, you can shorten the code needed to test whether - /// a property change notification affects a specific property. - /// - /// - /// - /// private void propertyChanged(object sender, PropertyChangedEventArgs arguments) { - /// if(arguments.AreAffecting("DisplayedValue")) { - /// updateDisplayedValueFromViewModel(); - /// } // Do not use else if here or wildcards will not work - /// if(arguments.AreAffecting("OtherValue")) { - /// updateOtherValueFromViewModel(); - /// } - /// } - /// - /// - /// - public static bool AreAffecting( - this PropertyChangedEventArgs arguments, string propertyName - ) { - if(arguments.AffectAllProperties()) { - return true; - } - - return (arguments.PropertyName == propertyName); - } - - /// Determines whether a property change notification is a wildcard - /// - /// Property change notification that will be checked on being a wildcard - /// - /// - /// Whether the property change is a wildcard, indicating that all properties - /// have changed. - /// - /// - /// - /// As stated on MSDN: "The PropertyChanged event can indicate all properties - /// on the object have changed by using either Nothing or String.Empty as - /// the property name in the PropertyChangedEventArgs." - /// - /// - /// This method offers an expressive way of checking for that eventuality. - /// - /// - /// - /// private void propertyChanged(object sender, PropertyChangedEventArgs arguments) { - /// if(arguments.AffectAllProperties()) { - /// // Do something - /// } - /// } - /// - /// - /// - public static bool AffectAllProperties(this PropertyChangedEventArgs arguments) { - return string.IsNullOrEmpty(arguments.PropertyName); - } - - /// - /// Caches PropertyChangedEventArgs instances to avoid feeding the garbage collector - /// -#if NO_CONCURRENT_COLLECTIONS - private static readonly Dictionary cache; -#else - private static readonly ConcurrentDictionary cache; -#endif - - } - -} // namespace Nuclex.Support +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +#if NO_CONCURRENT_COLLECTIONS +using System.Collections.Generic; +#else +using System.Collections.Concurrent; +#endif +using System.ComponentModel; +#if !NO_LINQ_EXPRESSIONS +using System.Linq.Expressions; +#endif + +namespace Nuclex.Support { + + /// Contains helper methods for property change notifications + public static class PropertyChangedEventArgsHelper { + + /// + /// A property change event argument container that indicates that all + /// properties have changed their value. + /// + public static readonly PropertyChangedEventArgs Wildcard = + new PropertyChangedEventArgs(null); + + /// Initializes a new property changed argument helper + static PropertyChangedEventArgsHelper() { +#if NO_CONCURRENT_COLLECTIONS + cache = new Dictionary(); +#else + cache = new ConcurrentDictionary(); +#endif + } + +#if !NO_LINQ_EXPRESSIONS + /// + /// Provides a property change argument container for the specified property + /// + /// + /// Property for which an event argument container will be provided + /// + /// The event argument container for a property of the specified name + /// + /// + /// This method transparently caches instances of the argument containers + /// to avoid feeding the garbage collector. A typical application only has + /// in the order of tens to hundreds of different properties for which changes + /// will be reported, making a cache to avoid garbage collections viable. + /// + /// + /// + /// PropertyChangedEventArgs arguments = + /// PropertyChangedEventArgsHelper.GetArgumentsFor(() => SomeProperty); + /// + /// + /// + [Obsolete("Prefer the C# 'nameof()' operator to using a Linq expression")] + public static PropertyChangedEventArgs GetArgumentsFor( + Expression> property + ) { + return GetArgumentsFor(ObservableHelper.GetPropertyName(property)); + } +#endif + + /// + /// Provides a property change argument container for the specified property + /// + /// + /// Property for which an event argument container will be provided + /// + /// The event argument container for a property of the specified name + /// + /// + /// This method transparently caches instances of the argument containers + /// to avoid feeding the garbage collector. A typical application only has + /// in the order of tens to hundreds of different properties for which changes + /// will be reported, making a cache to avoid garbage collections viable. + /// + /// + /// + /// PropertyChangedEventArgs arguments = + /// PropertyChangedEventArgsHelper.GetArgumentsFor("SomeProperty"); + /// + /// + /// + public static PropertyChangedEventArgs GetArgumentsFor(string propertyName) { + if(string.IsNullOrEmpty(propertyName)) { + return Wildcard; + } + +#if NO_CONCURRENT_COLLECTIONS + lock(cache) { + // Try to reuse the change notification if an instance already exists + PropertyChangedEventArgs arguments; + if(!cache.TryGetValue(propertyName, out arguments)) { + arguments = new PropertyChangedEventArgs(propertyName); + cache.Add(propertyName, arguments); + } + + return arguments; + } +#else + // If an instance for this property already exists, just return it + PropertyChangedEventArgs arguments; + if(cache.TryGetValue(propertyName, out arguments)) { + return arguments; + } + + // No instance existed (at least a short moment ago), so create a new one + return cache.GetOrAdd(propertyName, new PropertyChangedEventArgs(propertyName)); +#endif + } + +#if !NO_LINQ_EXPRESSIONS + /// + /// Determines whether the property change affects the specified property + /// + /// + /// Type of the property that will be tested for being affected + /// + /// + /// Property change that has been reported by the observed object + /// + /// Property that will be tested for being affected + /// Whether the specified property is affected by the property change + /// + /// + /// By using this method, you can shorten the code needed to test whether + /// a property change notification affects a specific property. You also + /// avoid hardcoding the property name, which would have the adverse effect + /// of not updating the textual property names during F2 refactoring. + /// + /// + /// + /// private void propertyChanged(object sender, PropertyChangedEventArgs arguments) { + /// if(arguments.AreAffecting(() => ViewModel.DisplayedValue)) { + /// updateDisplayedValueFromViewModel(); + /// } // Do not use else if here or wildcards will not work + /// if(arguments.AreAffecting(() => ViewModel.OtherValue)) { + /// updateOtherValueFromViewModel(); + /// } + /// } + /// + /// + /// + [Obsolete("Prefer the C# 'nameof()' operator to using a Linq expression")] + public static bool AreAffecting( + this PropertyChangedEventArgs arguments, Expression> property + ) { + if(arguments.AffectAllProperties()) { + return true; + } + + string propertyName = ObservableHelper.GetPropertyName(property); + return (arguments.PropertyName == propertyName); + } +#endif + + /// + /// Determines whether the property change affects the specified property + /// + /// + /// Property change that has been reported by the observed object + /// + /// Property that will be tested for being affected + /// Whether the specified property is affected by the property change + /// + /// + /// By using this method, you can shorten the code needed to test whether + /// a property change notification affects a specific property. + /// + /// + /// + /// private void propertyChanged(object sender, PropertyChangedEventArgs arguments) { + /// if(arguments.AreAffecting("DisplayedValue")) { + /// updateDisplayedValueFromViewModel(); + /// } // Do not use else if here or wildcards will not work + /// if(arguments.AreAffecting("OtherValue")) { + /// updateOtherValueFromViewModel(); + /// } + /// } + /// + /// + /// + public static bool AreAffecting( + this PropertyChangedEventArgs arguments, string propertyName + ) { + if(arguments.AffectAllProperties()) { + return true; + } + + return (arguments.PropertyName == propertyName); + } + + /// Determines whether a property change notification is a wildcard + /// + /// Property change notification that will be checked on being a wildcard + /// + /// + /// Whether the property change is a wildcard, indicating that all properties + /// have changed. + /// + /// + /// + /// As stated on MSDN: "The PropertyChanged event can indicate all properties + /// on the object have changed by using either Nothing or String.Empty as + /// the property name in the PropertyChangedEventArgs." + /// + /// + /// This method offers an expressive way of checking for that eventuality. + /// + /// + /// + /// private void propertyChanged(object sender, PropertyChangedEventArgs arguments) { + /// if(arguments.AffectAllProperties()) { + /// // Do something + /// } + /// } + /// + /// + /// + public static bool AffectAllProperties(this PropertyChangedEventArgs arguments) { + return string.IsNullOrEmpty(arguments.PropertyName); + } + + /// + /// Caches PropertyChangedEventArgs instances to avoid feeding the garbage collector + /// +#if NO_CONCURRENT_COLLECTIONS + private static readonly Dictionary cache; +#else + private static readonly ConcurrentDictionary cache; +#endif + + } + +} // namespace Nuclex.Support diff --git a/Source/Settings/ConfigurationFileStore.Parsing.cs b/Source/Settings/ConfigurationFileStore.Parsing.cs index ec8e68f..63fc0e5 100644 --- a/Source/Settings/ConfigurationFileStore.Parsing.cs +++ b/Source/Settings/ConfigurationFileStore.Parsing.cs @@ -1,319 +1,318 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; - -using Nuclex.Support.Parsing; - -namespace Nuclex.Support.Settings { - - partial class ConfigurationFileStore { - - #region class ParserState - - /// Remembers the target store and current category of the parser - private class ParserState { - - /// Store to which the parsed categories and options will be added - public ConfigurationFileStore Store; - - /// Current category options belong to - public Category Category; - - } - - #endregion // class ParserState - - /// Parses a configuration file from the specified text reader - /// Reader the configuration file will be parsed from - /// The configuration file parsed from the specified reader - public static ConfigurationFileStore Parse(TextReader reader) { - var store = new ConfigurationFileStore(); - var state = new ParserState() { - Store = store, - Category = store.rootCategory - }; - - for(; ; ) { - string line = reader.ReadLine(); - if(line == null) { - break; - } - - parseLine(state, line); - } - - return store; - } - - /// Incrementally parses a line read from a configuration file - /// Current parser state - /// Line that has been read - private static void parseLine(ParserState state, string line) { - - // If the line is empty, ignore it - int length = line.Length; - if(length == 0) { - return; - } - - // Skip all spaces at the beginning of the line - int firstCharacterIndex = 0; - ParserHelper.SkipSpaces(line, ref firstCharacterIndex); - - // If the line contained nothing but spaces, ignore it - if(firstCharacterIndex == length) { - return; - } - - // If the line is a comment, ignore it - if((line[firstCharacterIndex] == '#') || (line[firstCharacterIndex] == ';')) { - return; - } - - // Now the line is either a category definition or some attempt to set an option - if(line[firstCharacterIndex] == '[') { - parseCategory(state, line, firstCharacterIndex); - } else { - parseOption(state, line, firstCharacterIndex); - } - state.Category.Lines.Add(line); - - } - - /// Parses a category definition encountered on a line - /// Current parser state - /// Line containing the category definition - /// Index of the definition's first character - private static void parseCategory( - ParserState state, string line, int firstCharacterIndex - ) { - Debug.Assert(line[firstCharacterIndex] == '['); - - int nameStartIndex = firstCharacterIndex + 1; - ParserHelper.SkipSpaces(line, ref nameStartIndex); - - int lastCharacterIndex = line.Length - 1; - if(nameStartIndex >= lastCharacterIndex) { - return; // No space left for closing brace - } - - int nameEndIndex = line.IndexOf(']', nameStartIndex); - if(nameEndIndex == -1) { - return; // No closing brace in line - } - - // Skip any whitespaces between the last character and the closing brace - do { - --nameEndIndex; - } while(char.IsWhiteSpace(line, nameEndIndex)); - - // Now we know that the line holds a category definition and where exactly in - // the line the category name is located. Create the category. - state.Category = new Category() { - CategoryName = new StringSegment( - line, nameStartIndex, nameEndIndex - nameStartIndex + 1 - ), - OptionLookup = new Dictionary(), - Lines = new List() - }; - state.Store.categoryLookup.Add(state.Category.CategoryName.ToString(), state.Category); - } - - /// Parses an option definition encountered on a line - /// Current parser state - /// Line containing the option definition - /// Index of the definition's first character - private static void parseOption( - ParserState state, string line, int firstCharacterIndex - ) { - int assignmentIndex = line.IndexOf('=', firstCharacterIndex + 1); - if(assignmentIndex == -1) { - return; // No assignment took place - } - - // Cut off any whitespaces between the option name and the assignment - int nameEndIndex = assignmentIndex; - do { - --nameEndIndex; - } while(char.IsWhiteSpace(line, nameEndIndex)); - - // We have enough information to know that this is an assignment of some kind - Option option = new Option() { - LineIndex = state.Category.Lines.Count, - OptionName = new StringSegment( - line, firstCharacterIndex, nameEndIndex - firstCharacterIndex + 1 - ) - }; - - parseOptionValue(option, line, assignmentIndex + 1); - - // We've got the option assignment, either with an empty or proper value - state.Store.options.Add(option); - state.Category.OptionLookup.Add(option.OptionName.ToString(), option); - } - - /// Parses the value assigned to an option - /// Option to which a value is being assigned - /// Line containing the option assignment - /// Index one after the assignment character - private static void parseOptionValue(Option option, string line, int assignmentEndIndex) { - int firstCharacterIndex = assignmentEndIndex; - ParserHelper.SkipSpaces(line, ref firstCharacterIndex); - - // Just for beauty, when the option value is empty but padded with spaces, - // leave one space between the equals sign and the value. - if(firstCharacterIndex > assignmentEndIndex) { - ++assignmentEndIndex; - } - - // If the line consists of only whitespace, create an empty value - if(firstCharacterIndex == line.Length) { - option.OptionValue = new StringSegment(line, assignmentEndIndex, 0); - return; - } - - char firstCharacter = line[firstCharacterIndex]; - - // Values can be quoted to allow for comments characters appearing in them - int lastCharacterIndex; - if(firstCharacter == '"') { - lastCharacterIndex = line.LastIndexOf('"'); - } else { - lastCharacterIndex = firstCharacterIndex; - } - - int commentStartIndex = line.IndexOf(';', lastCharacterIndex); - if(commentStartIndex == -1) { - commentStartIndex = line.IndexOf('#', lastCharacterIndex); - } - if(commentStartIndex == -1) { - lastCharacterIndex = line.Length - 1; - } else { - lastCharacterIndex = commentStartIndex - 1; - } - - while(lastCharacterIndex > firstCharacterIndex) { - if(char.IsWhiteSpace(line, lastCharacterIndex)) { - --lastCharacterIndex; - } else { - break; - } - } - - option.OptionValue = new StringSegment( - line, firstCharacterIndex, lastCharacterIndex - firstCharacterIndex + 1 - ); - } - - /// Determines the best matching type for an option value - /// Value for which the best matching type will be found - /// The best matching type for the specified option value - private static Type getBestMatchingType(ref StringSegment value) { - if(value.Count == 0) { - return typeof(string); - } - - // If there are at least two characters, it may be an integer with - // a sign in front of it - if(value.Count >= 2) { - int index = value.Offset; - if(ParserHelper.SkipInteger(value.Text, ref index)) { - if(index >= value.Offset + value.Count) { - return typeof(int); - } - if(value.Text[index] == '.') { - return typeof(float); - } - } - } else { // If it's just a single character, it may be a number - if(char.IsNumber(value.Text, value.Offset)) { - return typeof(int); - } - } - - // If it parses as a boolean literal, then it must be a boolean - if(parseBooleanLiteral(ref value) != null) { - return typeof(bool); - } - - return typeof(string); - } - - /// Tries to parse a boolean literal - /// Value that will be parsed as a boolean literal - /// - /// True or false if the value was a boolean literal, null if it wasn't - /// - private static bool? parseBooleanLiteral(ref StringSegment value) { - switch(value.Count) { - - // If the string spells 'no', it is considered a boolean - case 2: { - bool isSpellingNo = - ((value.Text[value.Offset + 0] == 'n') || (value.Text[value.Offset + 0] == 'N')) && - ((value.Text[value.Offset + 1] == 'o') || (value.Text[value.Offset + 1] == 'O')); - return isSpellingNo ? new Nullable(false) : null; - } - - // If the string spells 'yes', it is considered a boolean - case 3: { - bool isSpellingYes = - ((value.Text[value.Offset + 0] == 'y') || (value.Text[value.Offset + 0] == 'Y')) && - ((value.Text[value.Offset + 1] == 'e') || (value.Text[value.Offset + 1] == 'E')) && - ((value.Text[value.Offset + 2] == 's') || (value.Text[value.Offset + 2] == 'S')); - return isSpellingYes ? new Nullable(true) : null; - } - - // If the string spells 'true', it is considered a boolean - case 4: { - bool isSpellingTrue = - ((value.Text[value.Offset + 0] == 't') || (value.Text[value.Offset + 0] == 'T')) && - ((value.Text[value.Offset + 1] == 'r') || (value.Text[value.Offset + 1] == 'R')) && - ((value.Text[value.Offset + 2] == 'u') || (value.Text[value.Offset + 2] == 'U')) && - ((value.Text[value.Offset + 3] == 'e') || (value.Text[value.Offset + 3] == 'E')); - return isSpellingTrue ? new Nullable(true) : null; - } - - // If the string spells 'false', it is considered a boolean - case 5: { - bool isSpellingFalse = - ((value.Text[value.Offset + 0] == 'f') || (value.Text[value.Offset + 0] == 'F')) && - ((value.Text[value.Offset + 1] == 'a') || (value.Text[value.Offset + 1] == 'A')) && - ((value.Text[value.Offset + 2] == 'l') || (value.Text[value.Offset + 2] == 'L')) && - ((value.Text[value.Offset + 3] == 's') || (value.Text[value.Offset + 3] == 'S')) && - ((value.Text[value.Offset + 4] == 'e') || (value.Text[value.Offset + 4] == 'E')); - return isSpellingFalse ? new Nullable(false) : null; - } - - // Anything else is not considered a boolean - default: { - return null; - } - - } - } - - } - -} // namespace Nuclex.Support.Configuration +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; + +using Nuclex.Support.Parsing; + +namespace Nuclex.Support.Settings { + + partial class ConfigurationFileStore { + + #region class ParserState + + /// Remembers the target store and current category of the parser + private class ParserState { + + /// Store to which the parsed categories and options will be added + public ConfigurationFileStore Store; + + /// Current category options belong to + public Category Category; + + } + + #endregion // class ParserState + + /// Parses a configuration file from the specified text reader + /// Reader the configuration file will be parsed from + /// The configuration file parsed from the specified reader + public static ConfigurationFileStore Parse(TextReader reader) { + var store = new ConfigurationFileStore(); + var state = new ParserState() { + Store = store, + Category = store.rootCategory + }; + + for(; ; ) { + string line = reader.ReadLine(); + if(line == null) { + break; + } + + parseLine(state, line); + } + + return store; + } + + /// Incrementally parses a line read from a configuration file + /// Current parser state + /// Line that has been read + private static void parseLine(ParserState state, string line) { + + // If the line is empty, ignore it + int length = line.Length; + if(length == 0) { + return; + } + + // Skip all spaces at the beginning of the line + int firstCharacterIndex = 0; + ParserHelper.SkipSpaces(line, ref firstCharacterIndex); + + // If the line contained nothing but spaces, ignore it + if(firstCharacterIndex == length) { + return; + } + + // If the line is a comment, ignore it + if((line[firstCharacterIndex] == '#') || (line[firstCharacterIndex] == ';')) { + return; + } + + // Now the line is either a category definition or some attempt to set an option + if(line[firstCharacterIndex] == '[') { + parseCategory(state, line, firstCharacterIndex); + } else { + parseOption(state, line, firstCharacterIndex); + } + state.Category.Lines.Add(line); + + } + + /// Parses a category definition encountered on a line + /// Current parser state + /// Line containing the category definition + /// Index of the definition's first character + private static void parseCategory( + ParserState state, string line, int firstCharacterIndex + ) { + Debug.Assert(line[firstCharacterIndex] == '['); + + int nameStartIndex = firstCharacterIndex + 1; + ParserHelper.SkipSpaces(line, ref nameStartIndex); + + int lastCharacterIndex = line.Length - 1; + if(nameStartIndex >= lastCharacterIndex) { + return; // No space left for closing brace + } + + int nameEndIndex = line.IndexOf(']', nameStartIndex); + if(nameEndIndex == -1) { + return; // No closing brace in line + } + + // Skip any whitespaces between the last character and the closing brace + do { + --nameEndIndex; + } while(char.IsWhiteSpace(line, nameEndIndex)); + + // Now we know that the line holds a category definition and where exactly in + // the line the category name is located. Create the category. + state.Category = new Category() { + CategoryName = new StringSegment( + line, nameStartIndex, nameEndIndex - nameStartIndex + 1 + ), + OptionLookup = new Dictionary(), + Lines = new List() + }; + state.Store.categoryLookup.Add(state.Category.CategoryName.ToString(), state.Category); + } + + /// Parses an option definition encountered on a line + /// Current parser state + /// Line containing the option definition + /// Index of the definition's first character + private static void parseOption( + ParserState state, string line, int firstCharacterIndex + ) { + int assignmentIndex = line.IndexOf('=', firstCharacterIndex + 1); + if(assignmentIndex == -1) { + return; // No assignment took place + } + + // Cut off any whitespaces between the option name and the assignment + int nameEndIndex = assignmentIndex; + do { + --nameEndIndex; + } while(char.IsWhiteSpace(line, nameEndIndex)); + + // We have enough information to know that this is an assignment of some kind + Option option = new Option() { + LineIndex = state.Category.Lines.Count, + OptionName = new StringSegment( + line, firstCharacterIndex, nameEndIndex - firstCharacterIndex + 1 + ) + }; + + parseOptionValue(option, line, assignmentIndex + 1); + + // We've got the option assignment, either with an empty or proper value + state.Store.options.Add(option); + state.Category.OptionLookup.Add(option.OptionName.ToString(), option); + } + + /// Parses the value assigned to an option + /// Option to which a value is being assigned + /// Line containing the option assignment + /// Index one after the assignment character + private static void parseOptionValue(Option option, string line, int assignmentEndIndex) { + int firstCharacterIndex = assignmentEndIndex; + ParserHelper.SkipSpaces(line, ref firstCharacterIndex); + + // Just for beauty, when the option value is empty but padded with spaces, + // leave one space between the equals sign and the value. + if(firstCharacterIndex > assignmentEndIndex) { + ++assignmentEndIndex; + } + + // If the line consists of only whitespace, create an empty value + if(firstCharacterIndex == line.Length) { + option.OptionValue = new StringSegment(line, assignmentEndIndex, 0); + return; + } + + char firstCharacter = line[firstCharacterIndex]; + + // Values can be quoted to allow for comments characters appearing in them + int lastCharacterIndex; + if(firstCharacter == '"') { + lastCharacterIndex = line.LastIndexOf('"'); + } else { + lastCharacterIndex = firstCharacterIndex; + } + + int commentStartIndex = line.IndexOf(';', lastCharacterIndex); + if(commentStartIndex == -1) { + commentStartIndex = line.IndexOf('#', lastCharacterIndex); + } + if(commentStartIndex == -1) { + lastCharacterIndex = line.Length - 1; + } else { + lastCharacterIndex = commentStartIndex - 1; + } + + while(lastCharacterIndex > firstCharacterIndex) { + if(char.IsWhiteSpace(line, lastCharacterIndex)) { + --lastCharacterIndex; + } else { + break; + } + } + + option.OptionValue = new StringSegment( + line, firstCharacterIndex, lastCharacterIndex - firstCharacterIndex + 1 + ); + } + + /// Determines the best matching type for an option value + /// Value for which the best matching type will be found + /// The best matching type for the specified option value + private static Type getBestMatchingType(ref StringSegment value) { + if(value.Count == 0) { + return typeof(string); + } + + // If there are at least two characters, it may be an integer with + // a sign in front of it + if(value.Count >= 2) { + int index = value.Offset; + if(ParserHelper.SkipInteger(value.Text, ref index)) { + if(index >= value.Offset + value.Count) { + return typeof(int); + } + if(value.Text[index] == '.') { + return typeof(float); + } + } + } else { // If it's just a single character, it may be a number + if(char.IsNumber(value.Text, value.Offset)) { + return typeof(int); + } + } + + // If it parses as a boolean literal, then it must be a boolean + if(parseBooleanLiteral(ref value) != null) { + return typeof(bool); + } + + return typeof(string); + } + + /// Tries to parse a boolean literal + /// Value that will be parsed as a boolean literal + /// + /// True or false if the value was a boolean literal, null if it wasn't + /// + private static bool? parseBooleanLiteral(ref StringSegment value) { + switch(value.Count) { + + // If the string spells 'no', it is considered a boolean + case 2: { + bool isSpellingNo = + ((value.Text[value.Offset + 0] == 'n') || (value.Text[value.Offset + 0] == 'N')) && + ((value.Text[value.Offset + 1] == 'o') || (value.Text[value.Offset + 1] == 'O')); + return isSpellingNo ? new Nullable(false) : null; + } + + // If the string spells 'yes', it is considered a boolean + case 3: { + bool isSpellingYes = + ((value.Text[value.Offset + 0] == 'y') || (value.Text[value.Offset + 0] == 'Y')) && + ((value.Text[value.Offset + 1] == 'e') || (value.Text[value.Offset + 1] == 'E')) && + ((value.Text[value.Offset + 2] == 's') || (value.Text[value.Offset + 2] == 'S')); + return isSpellingYes ? new Nullable(true) : null; + } + + // If the string spells 'true', it is considered a boolean + case 4: { + bool isSpellingTrue = + ((value.Text[value.Offset + 0] == 't') || (value.Text[value.Offset + 0] == 'T')) && + ((value.Text[value.Offset + 1] == 'r') || (value.Text[value.Offset + 1] == 'R')) && + ((value.Text[value.Offset + 2] == 'u') || (value.Text[value.Offset + 2] == 'U')) && + ((value.Text[value.Offset + 3] == 'e') || (value.Text[value.Offset + 3] == 'E')); + return isSpellingTrue ? new Nullable(true) : null; + } + + // If the string spells 'false', it is considered a boolean + case 5: { + bool isSpellingFalse = + ((value.Text[value.Offset + 0] == 'f') || (value.Text[value.Offset + 0] == 'F')) && + ((value.Text[value.Offset + 1] == 'a') || (value.Text[value.Offset + 1] == 'A')) && + ((value.Text[value.Offset + 2] == 'l') || (value.Text[value.Offset + 2] == 'L')) && + ((value.Text[value.Offset + 3] == 's') || (value.Text[value.Offset + 3] == 'S')) && + ((value.Text[value.Offset + 4] == 'e') || (value.Text[value.Offset + 4] == 'E')); + return isSpellingFalse ? new Nullable(false) : null; + } + + // Anything else is not considered a boolean + default: { + return null; + } + + } + } + + } + +} // namespace Nuclex.Support.Configuration diff --git a/Source/Settings/ConfigurationFileStore.Test.cs b/Source/Settings/ConfigurationFileStore.Test.cs index 6b85b3f..0d83b72 100644 --- a/Source/Settings/ConfigurationFileStore.Test.cs +++ b/Source/Settings/ConfigurationFileStore.Test.cs @@ -1,467 +1,466 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -using NUnit.Framework; - -namespace Nuclex.Support.Settings { - - /// Unit tests for the configuration file store - [TestFixture] - internal class ConfigurationFileStoreTest { - - /// - /// Verifies that loading an empty file doesn't lead to an exception - /// - [Test] - public void CanParseEmptyFile() { - Assert.That(() => load(string.Empty), Throws.Nothing); - } - - /// - /// Verifies that categories can be parsed from a configuration file - /// - [Test] - public void CanParseCategories() { - string[] categoryNames = new string[] { "Category1", "Category 2" }; - string fileContents = - "[" + categoryNames[0] + "]\r\n" + - " [ " + categoryNames[1] + " ] \r\n"; - ConfigurationFileStore configurationFile = load(fileContents); - - Assert.That(configurationFile.EnumerateCategories(), Is.EquivalentTo(categoryNames)); - } - - /// - /// Verifies that malformed categories can be handled by the parser - /// - [Test] - public void MalformedCategoriesAreIgnored() { - string fileContents = - "[ Not a category\r\n" + - " ["; - ConfigurationFileStore configurationFile = load(fileContents); - - Assert.That(configurationFile.EnumerateCategories(), Is.Empty); - } - - /// - /// Verifies that empty lines in the configuration file have no meaning - /// - [Test] - public void EmptyLinesAreSkipped() { - string fileContents = - "\r\n" + - " "; - ConfigurationFileStore configurationFile = load(fileContents); - Assert.That(configurationFile.EnumerateCategories(), Is.Empty); - } - - /// - /// Verifies that category definitions after a comment sign are ignored - /// - [Test] - public void CommentedOutCategoriesAreIgnored() { - string fileContents = - "#[NotACategory]\r\n" + - "; [ Also Not A Category ]\r\n"; - ConfigurationFileStore configurationFile = load(fileContents); - Assert.That(configurationFile.EnumerateCategories(), Is.Empty); - } - - /// - /// Verifies that assignments without an option name are ignored by the parser - /// - [Test] - public void NamelessAssignmentsAreIgnored() { - string fileContents = - "=\r\n" + - " = \r\n" + - " = hello"; - ConfigurationFileStore configurationFile = load(fileContents); - Assert.That(configurationFile.EnumerateCategories(), Is.Empty); - Assert.That(configurationFile.EnumerateOptions(), Is.Empty); - } - - /// - /// Verifies that assignments without an option name are ignored by the parser - /// - [Test] - public void OptionsCanHaveEmptyValues() { - string fileContents = - "a =\r\n" + - "b = \r\n" + - "c = ; hello"; - ConfigurationFileStore configurationFile = load(fileContents); - Assert.That(configurationFile.EnumerateCategories(), Is.Empty); - - var options = new List(configurationFile.EnumerateOptions()); - Assert.That(options.Count, Is.EqualTo(3)); - - for(int index = 0; index < options.Count; ++index) { - Assert.That( - configurationFile.Get(null, options[index].Name), Is.Null.Or.Empty - ); - } - } - - /// - /// Verifies that values assigned to options can contain space charcters - /// - [Test] - public void OptionValuesCanContainSpaces() { - string fileContents = - "test = hello world"; - ConfigurationFileStore configurationFile = load(fileContents); - - Assert.That(configurationFile.Get(null, "test"), Is.EqualTo("hello world")); - } - - /// - /// Verifies that values enclosed in quotes can embed comment characters - /// - [Test] - public void OptionValuesWithQuotesCanEmbedComments() { - string fileContents = - "test = \"This ; is # not a comment\" # but this is"; - ConfigurationFileStore configurationFile = load(fileContents); - - Assert.That( - configurationFile.Get(null, "test"), - Is.EqualTo("\"This ; is # not a comment\"") - ); - } - - /// - /// Verifies that values can end on a quote without causing trouble - /// - [Test] - public void CommentsCanEndWithAQuote() { - string fileContents = - "test = \"This value ends with a quote\""; - ConfigurationFileStore configurationFile = load(fileContents); - - Assert.That( - configurationFile.Get(null, "test"), - Is.EqualTo("\"This value ends with a quote\"") - ); - } - - /// - /// Verifies that values can forget the closing quote without causing trouble - /// - [Test] - public void ClosingQuoteCanBeOmmitted() { - string fileContents = - "test = \"No closing quote"; - ConfigurationFileStore configurationFile = load(fileContents); - - Assert.That( - configurationFile.Get(null, "test"), - Is.EqualTo("\"No closing quote") - ); - } - - /// - /// Verifies that text placed after the closing quote will also be part of - /// an option's value - /// - [Test] - public void TextAfterClosingQuoteBecomesPartOfValue() { - string fileContents = - "test = \"Begins here\" end ends here"; - ConfigurationFileStore configurationFile = load(fileContents); - - Assert.That( - configurationFile.Get(null, "test"), - Is.EqualTo("\"Begins here\" end ends here") - ); - } - - /// - /// Verifies that text placed after the closing quote will also be part of - /// an option's value - /// - [Test] - public void OptionValuesCanBeChanged() { - string fileContents = "test = 123 ; comment"; - ConfigurationFileStore configurationFile = load(fileContents); - - configurationFile.Set(null, "test", "hello world"); - - Assert.That( - save(configurationFile), - Contains.Substring("hello world").And.ContainsSubstring("comment") - ); - } - - /// - /// Verifies that options can be added to the configuration file - /// - [Test] - public void OptionsCanBeAdded() { - var configurationFile = new ConfigurationFileStore(); - - configurationFile.Set(null, "test", "123"); - Assert.That(configurationFile.Get(null, "test"), Is.EqualTo("123")); - } - - /// - /// Verifies that options can be added to the configuration file - /// - [Test] - public void CategoriesCanBeAdded() { - var configurationFile = new ConfigurationFileStore(); - - configurationFile.Set("general", "sol", "42"); - - Assert.That( - configurationFile.EnumerateCategories(), Is.EquivalentTo(new string[] { "general" }) - ); - Assert.That(save(configurationFile), Contains.Substring("[general]")); - } - - /// - /// Verifies that accessing an option that doesn't exist throws an exception - /// - [Test] - public void AccessingNonExistingOptionThrowsException() { - var configurationFile = new ConfigurationFileStore(); - - Assert.That( - () => configurationFile.Get(null, "doesn't exist"), - Throws.Exception.AssignableTo() - ); - } - - /// - /// Verifies that accessing a category that doesn't exist throws an exception - /// - [Test] - public void AccessingNonExistingCategoryThrowsException() { - var configurationFile = new ConfigurationFileStore(); - configurationFile.Set(null, "test", "123"); - - Assert.That( - () => configurationFile.Get("doesn't exist", "test"), - Throws.Exception.AssignableTo() - ); - } - - /// - /// Verifies that it's possible to enumerate a category that doesn't exist - /// - [Test] - public void NonExistingCategoryCanBeEnumerated() { - var configurationFile = new ConfigurationFileStore(); - - Assert.That(configurationFile.EnumerateOptions("doesn't exist"), Is.Empty); - } - - /// - /// Verifies that it's possible to create an option without a value - /// - [Test] - public void ValuelessOptionsCanBeCreated() { - var configurationFile = new ConfigurationFileStore(); - - configurationFile.Set(null, "test", null); - Assert.That(configurationFile.Get(null, "test"), Is.Null.Or.Empty); - } - - /// - /// Verifies that it's possible to assign an empty value to an option - /// - [Test] - public void OptionValueCanBeCleared() { - string fileContents = "test = 123 ; comment"; - ConfigurationFileStore configurationFile = load(fileContents); - - configurationFile.Set(null, "test", null); - Assert.That(configurationFile.Get(null, "test"), Is.Null.Or.Empty); - } - - /// - /// Verifies that it's possible to remove options from the configuration file - /// - [Test] - public void OptionsCanBeRemoved() { - var configurationFile = new ConfigurationFileStore(); - configurationFile.Set(null, "test", null); - - Assert.That(configurationFile.Remove(null, "test"), Is.True); - - string value; - Assert.That(configurationFile.TryGet(null, "test", out value), Is.False); - } - - /// - /// Verifies that options are removed from the configuration file correctly - /// - [Test] - public void RemovingOptionShiftsFollowingOptionsUp() { - string fileContents = - "first = 1\r\n" + - "second = 2"; - ConfigurationFileStore configurationFile = load(fileContents); - - Assert.That(configurationFile.Remove(null, "first"), Is.True); - configurationFile.Set(null, "second", "yay! first!"); - - Assert.That(save(configurationFile), Has.No.ContainsSubstring("1")); - Assert.That(save(configurationFile), Contains.Substring("second")); - Assert.That(save(configurationFile), Contains.Substring("yay! first!")); - } - - /// - /// Verifies that it's not an error to remove an option from a non-existing category - /// - [Test] - public void CanRemoveOptionFromNonExistingCategory() { - var configurationFile = new ConfigurationFileStore(); - Assert.That(configurationFile.Remove("nothing", "first"), Is.False); - } - - /// - /// Verifies that it's not an error to remove a non-existing option - /// - [Test] - public void CanRemoveNonExistingOption() { - var configurationFile = new ConfigurationFileStore(); - Assert.That(configurationFile.Remove(null, "first"), Is.False); - } - - /// - /// Verifies that the configuration file store can identify various types of values - /// - [ - Test, - TestCase("nothing=", typeof(string)), - TestCase("text = world", typeof(string)), - TestCase("short=9", typeof(int)), - TestCase("integer = 123", typeof(int)), - TestCase("integer = 123 ", typeof(int)), - TestCase("string=x", typeof(string)), - TestCase("string = 123s", typeof(string)), - TestCase("float = 123.45", typeof(float)), - TestCase("float = 123.45 ", typeof(float)), - TestCase("boolean = true", typeof(bool)), - TestCase("boolean = false", typeof(bool)), - TestCase("boolean = yes", typeof(bool)), - TestCase("boolean = no", typeof(bool)) - ] - public void OptionTypeCanBeIdentified(string assignment, Type expectedType) { - ConfigurationFileStore configurationFile = load(assignment); - - OptionInfo info; - using( - IEnumerator enumerator = configurationFile.EnumerateOptions().GetEnumerator() - ) { - Assert.That(enumerator.MoveNext(), Is.True); - info = enumerator.Current; - Assert.That(enumerator.MoveNext(), Is.False); - } - - Assert.That(info.OptionType, Is.EqualTo(expectedType)); - } - - /// - /// Verifies that configuration files containing duplicate option names can not - /// be used with the configuration file store - /// - [Test] - public void FilesWithDuplicateOptionNamesCannotBeProcessed() { - string fileContents = - "duplicate name = 1\r\n" + - "duplicate name = 2"; - - Assert.That(() => load(fileContents), Throws.Exception); - } - - /// - /// Verifies that attempting to cast a value to an incompatible data type causes - /// a FormatException to be thrown - /// - [Test] - public void ImpossibleCastCausesFormatException() { - string fileContents = "fail = yesnomaybe"; - ConfigurationFileStore configurationFile = load(fileContents); - - Assert.That( - () => configurationFile.Get(null, "fail"), - Throws.Exception.AssignableTo() - ); - } - - /// - /// Verifies that configuration files containing duplicate option names can not - /// be used with the configuration file store - /// - [ - Test, - TestCase("value = yes", true), - TestCase("value = true", true), - TestCase("value = no", false), - TestCase("value = false", false) - ] - public void BooleanLiteralsAreUnderstood(string fileContents, bool expectedValue) { - ConfigurationFileStore configurationFile = load(fileContents); - - if(expectedValue) { - Assert.That(configurationFile.Get(null, "value"), Is.True); - } else { - Assert.That(configurationFile.Get(null, "value"), Is.False); - } - } - - /// Loads a configuration file from a string - /// Contents of the configuration file - /// The configuration file loaded from the string - private static ConfigurationFileStore load(string fileContents) { - using(var reader = new StringReader(fileContents)) { - return ConfigurationFileStore.Parse(reader); - } - } - - /// Saves a configuration file into a string - /// Configuration file that will be saved - /// Contents of the configuration file - private static string save(ConfigurationFileStore configurationFile) { - var builder = new StringBuilder(); - - using(var writer = new StringWriter(builder)) { - configurationFile.Save(writer); - writer.Flush(); - } - - return builder.ToString(); - } - - } - -} // namespace Nuclex.Support.Settings - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +using NUnit.Framework; + +namespace Nuclex.Support.Settings { + + /// Unit tests for the configuration file store + [TestFixture] + internal class ConfigurationFileStoreTest { + + /// + /// Verifies that loading an empty file doesn't lead to an exception + /// + [Test] + public void CanParseEmptyFile() { + Assert.That(() => load(string.Empty), Throws.Nothing); + } + + /// + /// Verifies that categories can be parsed from a configuration file + /// + [Test] + public void CanParseCategories() { + string[] categoryNames = new string[] { "Category1", "Category 2" }; + string fileContents = + "[" + categoryNames[0] + "]\r\n" + + " [ " + categoryNames[1] + " ] \r\n"; + ConfigurationFileStore configurationFile = load(fileContents); + + Assert.That(configurationFile.EnumerateCategories(), Is.EquivalentTo(categoryNames)); + } + + /// + /// Verifies that malformed categories can be handled by the parser + /// + [Test] + public void MalformedCategoriesAreIgnored() { + string fileContents = + "[ Not a category\r\n" + + " ["; + ConfigurationFileStore configurationFile = load(fileContents); + + Assert.That(configurationFile.EnumerateCategories(), Is.Empty); + } + + /// + /// Verifies that empty lines in the configuration file have no meaning + /// + [Test] + public void EmptyLinesAreSkipped() { + string fileContents = + "\r\n" + + " "; + ConfigurationFileStore configurationFile = load(fileContents); + Assert.That(configurationFile.EnumerateCategories(), Is.Empty); + } + + /// + /// Verifies that category definitions after a comment sign are ignored + /// + [Test] + public void CommentedOutCategoriesAreIgnored() { + string fileContents = + "#[NotACategory]\r\n" + + "; [ Also Not A Category ]\r\n"; + ConfigurationFileStore configurationFile = load(fileContents); + Assert.That(configurationFile.EnumerateCategories(), Is.Empty); + } + + /// + /// Verifies that assignments without an option name are ignored by the parser + /// + [Test] + public void NamelessAssignmentsAreIgnored() { + string fileContents = + "=\r\n" + + " = \r\n" + + " = hello"; + ConfigurationFileStore configurationFile = load(fileContents); + Assert.That(configurationFile.EnumerateCategories(), Is.Empty); + Assert.That(configurationFile.EnumerateOptions(), Is.Empty); + } + + /// + /// Verifies that assignments without an option name are ignored by the parser + /// + [Test] + public void OptionsCanHaveEmptyValues() { + string fileContents = + "a =\r\n" + + "b = \r\n" + + "c = ; hello"; + ConfigurationFileStore configurationFile = load(fileContents); + Assert.That(configurationFile.EnumerateCategories(), Is.Empty); + + var options = new List(configurationFile.EnumerateOptions()); + Assert.That(options.Count, Is.EqualTo(3)); + + for(int index = 0; index < options.Count; ++index) { + Assert.That( + configurationFile.Get(null, options[index].Name), Is.Null.Or.Empty + ); + } + } + + /// + /// Verifies that values assigned to options can contain space charcters + /// + [Test] + public void OptionValuesCanContainSpaces() { + string fileContents = + "test = hello world"; + ConfigurationFileStore configurationFile = load(fileContents); + + Assert.That(configurationFile.Get(null, "test"), Is.EqualTo("hello world")); + } + + /// + /// Verifies that values enclosed in quotes can embed comment characters + /// + [Test] + public void OptionValuesWithQuotesCanEmbedComments() { + string fileContents = + "test = \"This ; is # not a comment\" # but this is"; + ConfigurationFileStore configurationFile = load(fileContents); + + Assert.That( + configurationFile.Get(null, "test"), + Is.EqualTo("\"This ; is # not a comment\"") + ); + } + + /// + /// Verifies that values can end on a quote without causing trouble + /// + [Test] + public void CommentsCanEndWithAQuote() { + string fileContents = + "test = \"This value ends with a quote\""; + ConfigurationFileStore configurationFile = load(fileContents); + + Assert.That( + configurationFile.Get(null, "test"), + Is.EqualTo("\"This value ends with a quote\"") + ); + } + + /// + /// Verifies that values can forget the closing quote without causing trouble + /// + [Test] + public void ClosingQuoteCanBeOmmitted() { + string fileContents = + "test = \"No closing quote"; + ConfigurationFileStore configurationFile = load(fileContents); + + Assert.That( + configurationFile.Get(null, "test"), + Is.EqualTo("\"No closing quote") + ); + } + + /// + /// Verifies that text placed after the closing quote will also be part of + /// an option's value + /// + [Test] + public void TextAfterClosingQuoteBecomesPartOfValue() { + string fileContents = + "test = \"Begins here\" end ends here"; + ConfigurationFileStore configurationFile = load(fileContents); + + Assert.That( + configurationFile.Get(null, "test"), + Is.EqualTo("\"Begins here\" end ends here") + ); + } + + /// + /// Verifies that text placed after the closing quote will also be part of + /// an option's value + /// + [Test] + public void OptionValuesCanBeChanged() { + string fileContents = "test = 123 ; comment"; + ConfigurationFileStore configurationFile = load(fileContents); + + configurationFile.Set(null, "test", "hello world"); + + Assert.That( + save(configurationFile), + Contains.Substring("hello world").And.ContainsSubstring("comment") + ); + } + + /// + /// Verifies that options can be added to the configuration file + /// + [Test] + public void OptionsCanBeAdded() { + var configurationFile = new ConfigurationFileStore(); + + configurationFile.Set(null, "test", "123"); + Assert.That(configurationFile.Get(null, "test"), Is.EqualTo("123")); + } + + /// + /// Verifies that options can be added to the configuration file + /// + [Test] + public void CategoriesCanBeAdded() { + var configurationFile = new ConfigurationFileStore(); + + configurationFile.Set("general", "sol", "42"); + + Assert.That( + configurationFile.EnumerateCategories(), Is.EquivalentTo(new string[] { "general" }) + ); + Assert.That(save(configurationFile), Contains.Substring("[general]")); + } + + /// + /// Verifies that accessing an option that doesn't exist throws an exception + /// + [Test] + public void AccessingNonExistingOptionThrowsException() { + var configurationFile = new ConfigurationFileStore(); + + Assert.That( + () => configurationFile.Get(null, "doesn't exist"), + Throws.Exception.AssignableTo() + ); + } + + /// + /// Verifies that accessing a category that doesn't exist throws an exception + /// + [Test] + public void AccessingNonExistingCategoryThrowsException() { + var configurationFile = new ConfigurationFileStore(); + configurationFile.Set(null, "test", "123"); + + Assert.That( + () => configurationFile.Get("doesn't exist", "test"), + Throws.Exception.AssignableTo() + ); + } + + /// + /// Verifies that it's possible to enumerate a category that doesn't exist + /// + [Test] + public void NonExistingCategoryCanBeEnumerated() { + var configurationFile = new ConfigurationFileStore(); + + Assert.That(configurationFile.EnumerateOptions("doesn't exist"), Is.Empty); + } + + /// + /// Verifies that it's possible to create an option without a value + /// + [Test] + public void ValuelessOptionsCanBeCreated() { + var configurationFile = new ConfigurationFileStore(); + + configurationFile.Set(null, "test", null); + Assert.That(configurationFile.Get(null, "test"), Is.Null.Or.Empty); + } + + /// + /// Verifies that it's possible to assign an empty value to an option + /// + [Test] + public void OptionValueCanBeCleared() { + string fileContents = "test = 123 ; comment"; + ConfigurationFileStore configurationFile = load(fileContents); + + configurationFile.Set(null, "test", null); + Assert.That(configurationFile.Get(null, "test"), Is.Null.Or.Empty); + } + + /// + /// Verifies that it's possible to remove options from the configuration file + /// + [Test] + public void OptionsCanBeRemoved() { + var configurationFile = new ConfigurationFileStore(); + configurationFile.Set(null, "test", null); + + Assert.That(configurationFile.Remove(null, "test"), Is.True); + + string value; + Assert.That(configurationFile.TryGet(null, "test", out value), Is.False); + } + + /// + /// Verifies that options are removed from the configuration file correctly + /// + [Test] + public void RemovingOptionShiftsFollowingOptionsUp() { + string fileContents = + "first = 1\r\n" + + "second = 2"; + ConfigurationFileStore configurationFile = load(fileContents); + + Assert.That(configurationFile.Remove(null, "first"), Is.True); + configurationFile.Set(null, "second", "yay! first!"); + + Assert.That(save(configurationFile), Has.No.ContainsSubstring("1")); + Assert.That(save(configurationFile), Contains.Substring("second")); + Assert.That(save(configurationFile), Contains.Substring("yay! first!")); + } + + /// + /// Verifies that it's not an error to remove an option from a non-existing category + /// + [Test] + public void CanRemoveOptionFromNonExistingCategory() { + var configurationFile = new ConfigurationFileStore(); + Assert.That(configurationFile.Remove("nothing", "first"), Is.False); + } + + /// + /// Verifies that it's not an error to remove a non-existing option + /// + [Test] + public void CanRemoveNonExistingOption() { + var configurationFile = new ConfigurationFileStore(); + Assert.That(configurationFile.Remove(null, "first"), Is.False); + } + + /// + /// Verifies that the configuration file store can identify various types of values + /// + [ + Test, + TestCase("nothing=", typeof(string)), + TestCase("text = world", typeof(string)), + TestCase("short=9", typeof(int)), + TestCase("integer = 123", typeof(int)), + TestCase("integer = 123 ", typeof(int)), + TestCase("string=x", typeof(string)), + TestCase("string = 123s", typeof(string)), + TestCase("float = 123.45", typeof(float)), + TestCase("float = 123.45 ", typeof(float)), + TestCase("boolean = true", typeof(bool)), + TestCase("boolean = false", typeof(bool)), + TestCase("boolean = yes", typeof(bool)), + TestCase("boolean = no", typeof(bool)) + ] + public void OptionTypeCanBeIdentified(string assignment, Type expectedType) { + ConfigurationFileStore configurationFile = load(assignment); + + OptionInfo info; + using( + IEnumerator enumerator = configurationFile.EnumerateOptions().GetEnumerator() + ) { + Assert.That(enumerator.MoveNext(), Is.True); + info = enumerator.Current; + Assert.That(enumerator.MoveNext(), Is.False); + } + + Assert.That(info.OptionType, Is.EqualTo(expectedType)); + } + + /// + /// Verifies that configuration files containing duplicate option names can not + /// be used with the configuration file store + /// + [Test] + public void FilesWithDuplicateOptionNamesCannotBeProcessed() { + string fileContents = + "duplicate name = 1\r\n" + + "duplicate name = 2"; + + Assert.That(() => load(fileContents), Throws.Exception); + } + + /// + /// Verifies that attempting to cast a value to an incompatible data type causes + /// a FormatException to be thrown + /// + [Test] + public void ImpossibleCastCausesFormatException() { + string fileContents = "fail = yesnomaybe"; + ConfigurationFileStore configurationFile = load(fileContents); + + Assert.That( + () => configurationFile.Get(null, "fail"), + Throws.Exception.AssignableTo() + ); + } + + /// + /// Verifies that configuration files containing duplicate option names can not + /// be used with the configuration file store + /// + [ + Test, + TestCase("value = yes", true), + TestCase("value = true", true), + TestCase("value = no", false), + TestCase("value = false", false) + ] + public void BooleanLiteralsAreUnderstood(string fileContents, bool expectedValue) { + ConfigurationFileStore configurationFile = load(fileContents); + + if(expectedValue) { + Assert.That(configurationFile.Get(null, "value"), Is.True); + } else { + Assert.That(configurationFile.Get(null, "value"), Is.False); + } + } + + /// Loads a configuration file from a string + /// Contents of the configuration file + /// The configuration file loaded from the string + private static ConfigurationFileStore load(string fileContents) { + using(var reader = new StringReader(fileContents)) { + return ConfigurationFileStore.Parse(reader); + } + } + + /// Saves a configuration file into a string + /// Configuration file that will be saved + /// Contents of the configuration file + private static string save(ConfigurationFileStore configurationFile) { + var builder = new StringBuilder(); + + using(var writer = new StringWriter(builder)) { + configurationFile.Save(writer); + writer.Flush(); + } + + return builder.ToString(); + } + + } + +} // namespace Nuclex.Support.Settings + +#endif // UNITTEST diff --git a/Source/Settings/ConfigurationFileStore.cs b/Source/Settings/ConfigurationFileStore.cs index 2432269..0eff533 100644 --- a/Source/Settings/ConfigurationFileStore.cs +++ b/Source/Settings/ConfigurationFileStore.cs @@ -1,431 +1,430 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2017 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Text; - -namespace Nuclex.Support.Settings { - - /// Represents an ini- or cfg-like configuration file - /// - /// - /// This class tries its best to preserve the formatting of configuration files. - /// Changing a value will keep the line it appears in intact. The parser also takes - /// as much data from a line as it can - anything to the left of an equals sign - /// becomes the name, anything to the right (excluding comments) becomes the value. - /// - /// - /// To access the contents of a configuration file, simply parse it and use it like - /// you would any other settings store: - /// - /// - /// - /// // # Settings.ini - /// // message = hello world ; the usual... - /// // show message = true - /// ISettingsStore settings; - /// using(var reader = new StreamReader("settings.ini")) { - /// settings = ConfigurationFile.Parse(reader); - /// } - /// - /// if(settings.Get<bool>(null, "show message")) { - /// Console.WriteLine(settings.Get<string>(null, "message")); - /// } - /// - /// - /// - /// It's usually a good idea to keep an application and all of its required files - /// together, whether it's code or data, but platforms often have their own conventions: - /// - /// - /// - /// Operating System - /// Convention - /// - /// - /// Linux - /// - /// System-wide configuration goes into /etc/<appname>/, user-specific - /// configuration goes into ~/.<appname>/ while static configuration that is - /// known at build time resides with the application in /opt/<appname>/ - /// - /// - /// - /// Windows - /// - /// System-wide configuration goes into %ProgramData%, user-specific configuration - /// has no real place (try %AppData%/<appname>/ if you want to hide it from - /// the user, %UserProfile%/Documents/<appname> if the user should see it) - /// and static configuration resides with your application - /// in %ProgramFiles%/<appname>/. - /// - /// - /// - /// MacOS - /// - /// System-wide configuration goes into /etc/<appname>/, user-specific - /// configuration goes into /Users/<username>/.<appname>/ while static - /// configuration resides with the application in /Applications/<appname>/ - /// - /// - /// - /// - public partial class ConfigurationFileStore : ISettingsStore { - - #region class Category - - /// Stores informations about a category found in the configuration file - private class Category { - - /// Name of the category as a string - public StringSegment CategoryName; - - /// Lookup table for the options in this category - public IDictionary OptionLookup; - - /// Lines this category and its options consist of - public IList Lines; - - } - - #endregion // class Category - - #region class Option - - /// Stores informations about an option found in the configuration file - private class Option { - - /// Index of the line the option is defined in - public int LineIndex; - - /// Name of the option as a string - public StringSegment OptionName; - - /// Value of the option as a string - public StringSegment OptionValue; - - } - - #endregion // class Option - - /// Initializes a new, empty configuration file - public ConfigurationFileStore() { - this.options = new List