From eb85ba3f7c834ac89e19fb976e1292fbaba3c6cb Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Mon, 1 Jan 2018 13:15:08 +1300 Subject: [PATCH] -Refactored enum name writing -Improved performance of reading and writing enum names -Tests --- .../JsonTextWriterAsyncTests.cs | 26 ++ .../Utilities/EnumUtilsTests.cs | 311 ++++++++++++++++++ .../Converters/StringEnumConverter.cs | 12 +- Src/Newtonsoft.Json/JsonWriter.Async.cs | 20 +- Src/Newtonsoft.Json/JsonWriter.cs | 2 +- .../Schema/JsonSchemaGenerator.cs | 2 +- Src/Newtonsoft.Json/Utilities/EnumUtils.cs | 128 +++++-- 7 files changed, 454 insertions(+), 47 deletions(-) create mode 100644 Src/Newtonsoft.Json.Tests/Utilities/EnumUtilsTests.cs diff --git a/Src/Newtonsoft.Json.Tests/JsonTextWriterAsyncTests.cs b/Src/Newtonsoft.Json.Tests/JsonTextWriterAsyncTests.cs index 74c730e3..4e4dc1e5 100644 --- a/Src/Newtonsoft.Json.Tests/JsonTextWriterAsyncTests.cs +++ b/Src/Newtonsoft.Json.Tests/JsonTextWriterAsyncTests.cs @@ -201,6 +201,32 @@ namespace Newtonsoft.Json.Tests ] }", sw.ToString()); } + + [Test] + public async Task WriteLazy_Property() + { + LazyStringWriter sw = new LazyStringWriter(CultureInfo.InvariantCulture); + + using (JsonTextWriter writer = new JsonTextWriter(sw)) + { + writer.Indentation = 4; + writer.Formatting = Formatting.Indented; + + await writer.WriteStartArrayAsync(); + + await writer.WriteStartObjectAsync(); + + await writer.WritePropertyNameAsync("IncompleteProp"); + + await writer.WriteEndArrayAsync(); + } + + StringAssert.AreEqual(@"[ + { + ""IncompleteProp"": null + } +]", sw.ToString()); + } #endif [Test] diff --git a/Src/Newtonsoft.Json.Tests/Utilities/EnumUtilsTests.cs b/Src/Newtonsoft.Json.Tests/Utilities/EnumUtilsTests.cs new file mode 100644 index 00000000..6304e8f2 --- /dev/null +++ b/Src/Newtonsoft.Json.Tests/Utilities/EnumUtilsTests.cs @@ -0,0 +1,311 @@ +#region License +// Copyright (c) 2007 James Newton-King +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !(NET20 || NET35) +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using Newtonsoft.Json.Utilities; +#if DNXCORE50 +using Xunit; +using Test = Xunit.FactAttribute; +using Assert = Newtonsoft.Json.Tests.XUnitAssert; +using TestCaseSource = Xunit.MemberDataAttribute; +#else +using NUnit.Framework; +#endif + +namespace Newtonsoft.Json.Tests.Utilities +{ + public class EnumUtilsTests : TestFixtureBase + { +#if DNXCORE50 + [Theory] +#endif + [TestCaseSource(nameof(Parse_TestData))] + public void Parse(string value, object expected) + { + Type enumType = expected.GetType(); + + Enum result = (Enum)EnumUtils.ParseEnumName(value, false, false, enumType); + Assert.AreEqual(expected, result); + } + +#if DNXCORE50 + [Theory] +#endif + [TestCaseSource(nameof(Parse_Invalid_TestData))] + public void Parse_Invalid(Type enumType, string value, Type exceptionType) + { + try + { + EnumUtils.ParseEnumName(value, false, false, enumType); + } + catch (Exception ex) when (ex.GetType() == exceptionType) + { + // nom nom nom + return; + } + + Assert.Fail($"Expected {exceptionType.FullName} exception."); + } + +#if DNXCORE50 + [Theory] +#endif + [TestCaseSource(nameof(ToString_Format_TestData))] + public static void ToString_Format(Enum e, string expected) + { + EnumUtils.TryToString(e.GetType(), e, false, out string result); + + Assert.AreEqual(expected, result); + } + + #region Test data + // test data from https://github.com/dotnet/corefx/blob/master/src/System.Runtime/tests/System/EnumTests.cs + public static IEnumerable Parse_TestData() + { + // SByte + yield return new object[] { "Min", SByteEnum.Min }; + yield return new object[] { "mAx", SByteEnum.Max }; + yield return new object[] { "1", SByteEnum.One }; + yield return new object[] { "5", (SByteEnum)5 }; + + // Byte + yield return new object[] { "Min", ByteEnum.Min }; + yield return new object[] { "mAx", ByteEnum.Max }; + yield return new object[] { "1", ByteEnum.One }; + yield return new object[] { "5", (ByteEnum)5 }; + + // Int16 + yield return new object[] { "Min", Int16Enum.Min }; + yield return new object[] { "mAx", Int16Enum.Max }; + yield return new object[] { "1", Int16Enum.One }; + yield return new object[] { "5", (Int16Enum)5 }; + + // UInt16 + yield return new object[] { "Min", UInt16Enum.Min }; + yield return new object[] { "mAx", UInt16Enum.Max }; + yield return new object[] { "1", UInt16Enum.One }; + yield return new object[] { "5", (UInt16Enum)5 }; + + // Int32 + yield return new object[] { "Min", Int32Enum.Min }; + yield return new object[] { "mAx", Int32Enum.Max }; + yield return new object[] { "1", Int32Enum.One }; + yield return new object[] { "5", (Int32Enum)5 }; + + // UInt32 + yield return new object[] { "Min", UInt32Enum.Min }; + yield return new object[] { "mAx", UInt32Enum.Max }; + yield return new object[] { "1", UInt32Enum.One }; + yield return new object[] { "5", (UInt32Enum)5 }; + + // Int64 + yield return new object[] { "Min", Int64Enum.Min }; + yield return new object[] { "mAx", Int64Enum.Max }; + yield return new object[] { "1", Int64Enum.One }; + yield return new object[] { "5", (Int64Enum)5 }; + + // UInt64 + yield return new object[] { "Min", UInt64Enum.Min }; + yield return new object[] { "mAx", UInt64Enum.Max }; + yield return new object[] { "1", UInt64Enum.One }; + yield return new object[] { "5", (UInt64Enum)5 }; + + // SimpleEnum + yield return new object[] { "Red", SimpleEnum.Red }; + yield return new object[] { " Red", SimpleEnum.Red }; + yield return new object[] { "Red ", SimpleEnum.Red }; + yield return new object[] { " red ", SimpleEnum.Red }; + yield return new object[] { "B", SimpleEnum.B }; + yield return new object[] { "B,B", SimpleEnum.B }; + yield return new object[] { " Red , Blue ", SimpleEnum.Red | SimpleEnum.Blue }; + yield return new object[] { "Blue,Red,Green", SimpleEnum.Red | SimpleEnum.Blue | SimpleEnum.Green }; + yield return new object[] { "Blue,Red,Red,Red,Green", SimpleEnum.Red | SimpleEnum.Blue | SimpleEnum.Green }; + yield return new object[] { "Red,Blue, Green", SimpleEnum.Red | SimpleEnum.Blue | SimpleEnum.Green }; + yield return new object[] { "1", SimpleEnum.Red }; + yield return new object[] { " 1 ", SimpleEnum.Red }; + yield return new object[] { "2", SimpleEnum.Blue }; + yield return new object[] { "99", (SimpleEnum)99 }; + yield return new object[] { "-42", (SimpleEnum)(-42) }; + yield return new object[] { " -42", (SimpleEnum)(-42) }; + yield return new object[] { " -42 ", (SimpleEnum)(-42) }; + } + + // test data from https://github.com/dotnet/corefx/blob/master/src/System.Runtime/tests/System/EnumTests.cs + public static IEnumerable Parse_Invalid_TestData() + { + // SimpleEnum + yield return new object[] { null, "", typeof(ArgumentNullException) }; + yield return new object[] { typeof(SimpleEnum), null, typeof(ArgumentNullException) }; + yield return new object[] { typeof(object), "", typeof(ArgumentException) }; + yield return new object[] { typeof(SimpleEnum), "", typeof(ArgumentException) }; + yield return new object[] { typeof(SimpleEnum), " \t", typeof(ArgumentException) }; + yield return new object[] { typeof(SimpleEnum), "Purple", typeof(ArgumentException) }; + yield return new object[] { typeof(SimpleEnum), ",Red", typeof(ArgumentException) }; + yield return new object[] { typeof(SimpleEnum), "Red,", typeof(ArgumentException) }; + yield return new object[] { typeof(SimpleEnum), "B,", typeof(ArgumentException) }; + yield return new object[] { typeof(SimpleEnum), " , , ,", typeof(ArgumentException) }; + yield return new object[] { typeof(SimpleEnum), "Red,Blue,", typeof(ArgumentException) }; + yield return new object[] { typeof(SimpleEnum), "Red,,Blue", typeof(ArgumentException) }; + yield return new object[] { typeof(SimpleEnum), "Red,Blue, ", typeof(ArgumentException) }; + yield return new object[] { typeof(SimpleEnum), "Red Blue", typeof(ArgumentException) }; + yield return new object[] { typeof(SimpleEnum), "1,Blue", typeof(ArgumentException) }; + yield return new object[] { typeof(SimpleEnum), "Blue,1", typeof(ArgumentException) }; + yield return new object[] { typeof(SimpleEnum), "Blue, 1", typeof(ArgumentException) }; + yield return new object[] { typeof(SimpleEnum), "2147483649", typeof(OverflowException) }; + yield return new object[] { typeof(SimpleEnum), "2147483648", typeof(OverflowException) }; + } + + // test data from https://github.com/dotnet/corefx/blob/master/src/System.Runtime/tests/System/EnumTests.cs + public static IEnumerable ToString_Format_TestData() + { + yield return new object[] { SByteEnum.Min, "Min" }; + yield return new object[] { (SByteEnum)5, null }; + yield return new object[] { SByteEnum.Max, "Max" }; + + yield return new object[] { ByteEnum.Min, "Min" }; + yield return new object[] { (ByteEnum)5, null }; + yield return new object[] { (ByteEnum)0xff, "Max" }; + yield return new object[] { (ByteEnum)3, null }; + + yield return new object[] { Int16Enum.Min, "Min" }; + yield return new object[] { (Int16Enum)5, null }; + yield return new object[] { Int16Enum.Max, "Max" }; + yield return new object[] { (Int16Enum)3, null }; + + yield return new object[] { UInt16Enum.Min, "Min" }; + yield return new object[] { (UInt16Enum)5, null }; + yield return new object[] { UInt16Enum.Max, "Max" }; + yield return new object[] { (UInt16Enum)3, null }; + + yield return new object[] { Int32Enum.Min, "Min" }; + yield return new object[] { (Int32Enum)5, null }; + yield return new object[] { Int32Enum.Max, "Max" }; + yield return new object[] { (Int32Enum)3, null }; + + yield return new object[] { UInt32Enum.Min, "Min" }; + yield return new object[] { (UInt32Enum)5, null }; + yield return new object[] { UInt32Enum.Max, "Max" }; + yield return new object[] { (UInt32Enum)3, null }; + + yield return new object[] { Int64Enum.Min, "Min" }; + yield return new object[] { (Int64Enum)5, null }; + yield return new object[] { Int64Enum.Max, "Max" }; + yield return new object[] { (Int64Enum)3, null }; + + yield return new object[] { UInt64Enum.Min, "Min" }; + yield return new object[] { (UInt64Enum)5, null }; + yield return new object[] { UInt64Enum.Max, "Max" }; + yield return new object[] { (UInt64Enum)3, null }; + + yield return new object[] { SimpleEnum.Red, "Red" }; + yield return new object[] { SimpleEnum.Blue, "Blue" }; + yield return new object[] { (SimpleEnum)99, null }; + yield return new object[] { (SimpleEnum)0, null }; + + yield return new object[] { AttributeTargets.Class | AttributeTargets.Delegate, "Class, Delegate" }; + } + #endregion + } + + public enum SimpleEnum + { + Red = 1, + Blue = 2, + Green = 3, + Green_a = 3, + Green_b = 3, + B = 4 + } + + public enum ByteEnum : byte + { + Min = byte.MinValue, + One = 1, + Two = 2, + Max = byte.MaxValue, + } + + public enum SByteEnum : sbyte + { + Min = sbyte.MinValue, + One = 1, + Two = 2, + Max = sbyte.MaxValue, + } + + public enum UInt16Enum : ushort + { + Min = ushort.MinValue, + One = 1, + Two = 2, + Max = ushort.MaxValue, + } + + public enum Int16Enum : short + { + Min = short.MinValue, + One = 1, + Two = 2, + Max = short.MaxValue, + } + + public enum UInt32Enum : uint + { + Min = uint.MinValue, + One = 1, + Two = 2, + Max = uint.MaxValue, + } + + public enum Int32Enum : int + { + Min = int.MinValue, + One = 1, + Two = 2, + Max = int.MaxValue, + } + + public enum UInt64Enum : ulong + { + Min = ulong.MinValue, + One = 1, + Two = 2, + Max = ulong.MaxValue, + } + + public enum Int64Enum : long + { + Min = long.MinValue, + One = 1, + Two = 2, + Max = long.MaxValue, + } +} +#endif \ No newline at end of file diff --git a/Src/Newtonsoft.Json/Converters/StringEnumConverter.cs b/Src/Newtonsoft.Json/Converters/StringEnumConverter.cs index 5d5e9149..bba8d764 100644 --- a/Src/Newtonsoft.Json/Converters/StringEnumConverter.cs +++ b/Src/Newtonsoft.Json/Converters/StringEnumConverter.cs @@ -93,13 +93,11 @@ namespace Newtonsoft.Json.Converters Enum e = (Enum)value; - string enumName = e.ToString("G"); - - if (char.IsNumber(enumName[0]) || enumName[0] == '-') + if (!EnumUtils.TryToString(e.GetType(), value, CamelCaseText, out string enumName)) { if (!AllowIntegerValues) { - throw JsonSerializationException.Create(null, writer.ContainerPath, "Integer value {0} is not allowed.".FormatWith(CultureInfo.InvariantCulture, enumName), null); + throw JsonSerializationException.Create(null, writer.ContainerPath, "Integer value {0} is not allowed.".FormatWith(CultureInfo.InvariantCulture, e.ToString("D")), null); } // enum value has no name so write number @@ -107,11 +105,7 @@ namespace Newtonsoft.Json.Converters } else { - Type enumType = e.GetType(); - - string finalName = EnumUtils.ToEnumName(enumType, enumName, CamelCaseText); - - writer.WriteValue(finalName); + writer.WriteValue(enumName); } } diff --git a/Src/Newtonsoft.Json/JsonWriter.Async.cs b/Src/Newtonsoft.Json/JsonWriter.Async.cs index f7adefc3..c53c0022 100644 --- a/Src/Newtonsoft.Json/JsonWriter.Async.cs +++ b/Src/Newtonsoft.Json/JsonWriter.Async.cs @@ -293,7 +293,7 @@ namespace Newtonsoft.Json t = WriteNullAsync(cancellationToken); if (!t.IsCompletedSucessfully()) { - return AwaitProperty(t, levelsToComplete, token, type, cancellationToken); + return AwaitProperty(t, levelsToComplete, token, cancellationToken); } } @@ -304,7 +304,7 @@ namespace Newtonsoft.Json t = WriteIndentAsync(cancellationToken); if (!t.IsCompletedSucessfully()) { - return AwaitIndent(t, levelsToComplete, token, type, cancellationToken); + return AwaitIndent(t, levelsToComplete, token, cancellationToken); } } } @@ -312,7 +312,7 @@ namespace Newtonsoft.Json t = WriteEndAsync(token, cancellationToken); if (!t.IsCompletedSucessfully()) { - return AwaitEnd(t, levelsToComplete, type, cancellationToken); + return AwaitEnd(t, levelsToComplete, cancellationToken); } UpdateCurrentState(); @@ -321,7 +321,7 @@ namespace Newtonsoft.Json return AsyncUtils.CompletedTask; // Local functions, params renamed (capitalized) so as not to capture and allocate when calling async - async Task AwaitProperty(Task task, int LevelsToComplete, JsonToken token, JsonContainerType Type, CancellationToken CancellationToken) + async Task AwaitProperty(Task task, int LevelsToComplete, JsonToken token, CancellationToken CancellationToken) { await task.ConfigureAwait(false); @@ -338,10 +338,10 @@ namespace Newtonsoft.Json UpdateCurrentState(); - await AwaitRemaining(LevelsToComplete, Type, CancellationToken).ConfigureAwait(false); + await AwaitRemaining(LevelsToComplete, CancellationToken).ConfigureAwait(false); } - async Task AwaitIndent(Task task, int LevelsToComplete, JsonToken token, JsonContainerType Type, CancellationToken CancellationToken) + async Task AwaitIndent(Task task, int LevelsToComplete, JsonToken token, CancellationToken CancellationToken) { await task.ConfigureAwait(false); @@ -351,10 +351,10 @@ namespace Newtonsoft.Json UpdateCurrentState(); - await AwaitRemaining(LevelsToComplete, Type, CancellationToken).ConfigureAwait(false); + await AwaitRemaining(LevelsToComplete, CancellationToken).ConfigureAwait(false); } - async Task AwaitEnd(Task task, int LevelsToComplete, JsonContainerType Type, CancellationToken CancellationToken) + async Task AwaitEnd(Task task, int LevelsToComplete, CancellationToken CancellationToken) { await task.ConfigureAwait(false); @@ -362,10 +362,10 @@ namespace Newtonsoft.Json UpdateCurrentState(); - await AwaitRemaining(LevelsToComplete, Type, CancellationToken).ConfigureAwait(false); + await AwaitRemaining(LevelsToComplete, CancellationToken).ConfigureAwait(false); } - async Task AwaitRemaining(int LevelsToComplete, JsonContainerType Type, CancellationToken CancellationToken) + async Task AwaitRemaining(int LevelsToComplete, CancellationToken CancellationToken) { while (LevelsToComplete-- > 0) { diff --git a/Src/Newtonsoft.Json/JsonWriter.cs b/Src/Newtonsoft.Json/JsonWriter.cs index 14c12cdb..7a62d7d9 100644 --- a/Src/Newtonsoft.Json/JsonWriter.cs +++ b/Src/Newtonsoft.Json/JsonWriter.cs @@ -82,7 +82,7 @@ namespace Newtonsoft.Json State[] errorStates = StateArrayTempate[0]; State[] valueStates = StateArrayTempate[7]; - TypeValuesAndNames enumValuesAndNames = EnumUtils.GetEnumValuesAndNames(typeof(JsonToken)); + EnumInfo enumValuesAndNames = EnumUtils.GetEnumValuesAndNames(typeof(JsonToken)); foreach (ulong valueToken in enumValuesAndNames.Values) { diff --git a/Src/Newtonsoft.Json/Schema/JsonSchemaGenerator.cs b/Src/Newtonsoft.Json/Schema/JsonSchemaGenerator.cs index e6a85786..0d47416e 100644 --- a/Src/Newtonsoft.Json/Schema/JsonSchemaGenerator.cs +++ b/Src/Newtonsoft.Json/Schema/JsonSchemaGenerator.cs @@ -309,7 +309,7 @@ namespace Newtonsoft.Json.Schema { CurrentSchema.Enum = new List(); - TypeValuesAndNames enumValues = EnumUtils.GetEnumValuesAndNames(type); + EnumInfo enumValues = EnumUtils.GetEnumValuesAndNames(type); for (int i = 0; i < enumValues.Names.Length; i++) { ulong v = enumValues.Values[i]; diff --git a/Src/Newtonsoft.Json/Utilities/EnumUtils.cs b/Src/Newtonsoft.Json/Utilities/EnumUtils.cs index 70aa3ac1..5f5e6736 100644 --- a/Src/Newtonsoft.Json/Utilities/EnumUtils.cs +++ b/Src/Newtonsoft.Json/Utilities/EnumUtils.cs @@ -25,6 +25,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Runtime.Serialization; #if !HAVE_LINQ @@ -33,16 +34,18 @@ using Newtonsoft.Json.Utilities.LinqBridge; using System.Linq; #endif using System.Reflection; +using System.Text; namespace Newtonsoft.Json.Utilities { internal static class EnumUtils { private const char EnumSeparatorChar = ','; + private const string EnumSeparatorString = ", "; - private static readonly ThreadSafeStore ValuesAndNamesPerEnum = new ThreadSafeStore(InitializeValuesAndNames); + private static readonly ThreadSafeStore ValuesAndNamesPerEnum = new ThreadSafeStore(InitializeValuesAndNames); - private static TypeValuesAndNames InitializeValuesAndNames(Type enumType) + private static EnumInfo InitializeValuesAndNames(Type enumType) { string[] names = Enum.GetNames(enumType); string[] resolvedNames = new string[names.Length]; @@ -72,7 +75,9 @@ namespace Newtonsoft.Json.Utilities resolvedNames[i] = resolvedName; } - return new TypeValuesAndNames(values, names, resolvedNames); + bool isFlags = enumType.IsDefined(typeof(FlagsAttribute), false); + + return new EnumInfo(isFlags, values, names, resolvedNames); } public static IList GetFlagsValues(T value) where T : struct @@ -87,7 +92,7 @@ namespace Newtonsoft.Json.Utilities Type underlyingType = Enum.GetUnderlyingType(value.GetType()); ulong num = ToUInt64(value); - TypeValuesAndNames enumNameValues = GetEnumValuesAndNames(enumType); + EnumInfo enumNameValues = GetEnumValuesAndNames(enumType); IList selectedFlagsValues = new List(); for (int i = 0; i < enumNameValues.Values.Length; i++) @@ -118,34 +123,103 @@ namespace Newtonsoft.Json.Utilities return ParseEnum(t, enumText, disallowValue); } - public static string ToEnumName(Type enumType, string enumText, bool camelCaseText) + public static bool TryToString(Type enumType, object value, bool camelCaseText, out string name) { - TypeValuesAndNames enumValuesAndNames = ValuesAndNamesPerEnum.Get(enumType); + EnumInfo enumInfo = ValuesAndNamesPerEnum.Get(enumType); + ulong v = ToUInt64(value); - string[] names = enumText.Split(','); - for (int i = 0; i < names.Length; i++) + if (!enumInfo.IsFlags) { - string name = names[i].Trim(); - - int? matchingIndex = FindIndexByName(enumValuesAndNames.Names, name, 0, name.Length, StringComparison.Ordinal); - string resolvedEnumName = matchingIndex != null - ? enumValuesAndNames.ResolvedNames[matchingIndex.Value] - : name; - - if (camelCaseText) + int index = Array.BinarySearch(enumInfo.Values, v); + if (index >= 0) { - resolvedEnumName = StringUtils.ToCamelCase(resolvedEnumName); + name = enumInfo.ResolvedNames[index]; + if (camelCaseText) + { + name = StringUtils.ToCamelCase(name); + } + + return true; } - names[i] = resolvedEnumName; + // is number value + name = null; + return false; + } + else // These are flags OR'ed together (We treat everything as unsigned types) + { + name = InternalFlagsFormat(enumInfo, v, camelCaseText); + return name != null; } - - string finalName = string.Join(", ", names); - - return finalName; } - public static TypeValuesAndNames GetEnumValuesAndNames(Type enumType) + private static String InternalFlagsFormat(EnumInfo entry, ulong result, bool camelCaseText) + { + string[] resolvedNames = entry.ResolvedNames; + ulong[] values = entry.Values; + + int index = values.Length - 1; + StringBuilder sb = new StringBuilder(); + bool firstTime = true; + ulong saveResult = result; + + // We will not optimize this code further to keep it maintainable. There are some boundary checks that can be applied + // to minimize the comparsions required. This code works the same for the best/worst case. In general the number of + // items in an enum are sufficiently small and not worth the optimization. + while (index >= 0) + { + if (index == 0 && values[index] == 0) + { + break; + } + + if ((result & values[index]) == values[index]) + { + result -= values[index]; + if (!firstTime) + { + sb.Insert(0, EnumSeparatorString); + } + + string resolvedName = resolvedNames[index]; + sb.Insert(0, camelCaseText ? StringUtils.ToCamelCase(resolvedName) : resolvedName); + firstTime = false; + } + + index--; + } + + string returnString; + if (result != 0) + { + // We were unable to represent this number as a bitwise or of valid flags + returnString = null; // return null so the caller knows to .ToString() the input + } + else if (saveResult == 0) + { + // For the cases when we have zero + if (values.Length > 0 && values[0] == 0) + { + returnString = resolvedNames[0]; // Zero was one of the enum values. + if (camelCaseText) + { + returnString = StringUtils.ToCamelCase(returnString); + } + } + else + { + returnString = null; + } + } + else + { + returnString = sb.ToString(); // Return the string representation + } + + return returnString; + } + + public static EnumInfo GetEnumValuesAndNames(Type enumType) { return ValuesAndNamesPerEnum.Get(enumType); } @@ -193,7 +267,7 @@ namespace Newtonsoft.Json.Utilities throw new ArgumentException("Type provided must be an Enum.", nameof(enumType)); } - TypeValuesAndNames entry = ValuesAndNamesPerEnum.Get(enumType); + EnumInfo entry = ValuesAndNamesPerEnum.Get(enumType); string[] enumNames = entry.Names; string[] resolvedNames = entry.ResolvedNames; ulong[] enumValues = entry.Values; @@ -333,15 +407,17 @@ namespace Newtonsoft.Json.Utilities } } - internal class TypeValuesAndNames + internal class EnumInfo { - public TypeValuesAndNames(ulong[] values, string[] names, string[] resolvedNames) + public EnumInfo(bool isFlags, ulong[] values, string[] names, string[] resolvedNames) { + IsFlags = isFlags; Values = values; Names = names; ResolvedNames = resolvedNames; } + public readonly bool IsFlags; public readonly ulong[] Values; public readonly string[] Names; public readonly string[] ResolvedNames;