From c684902fba5b5e63fa750e66270a25b6889b4ec6 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 18 Jun 2019 20:56:43 -0700 Subject: [PATCH] Use the original precision (prior-dotnet-core-3) for double/fload-to-string conversion (#9893) .NET Core changes to return "shortest roundtrippable string" by default for the ToString() method of double and float types. This results in ToString() for double/float values sometimes return a string in 17-digit/9-digit precision format. This PR updated the double/float-to-string conversion in PowerShell to continue using the old precision specifier before the change in .NET Core 3.0. --- .../engine/LanguagePrimitives.cs | 13 ++++ .../engine/MshObject.cs | 72 +++++++++++-------- .../Interop/DotNet/DotNetAPI.Tests.ps1 | 18 ++--- .../Language/Parser/Conversions.Tests.ps1 | 26 +++++++ .../Language/Parser/Parser.Tests.ps1 | 4 +- 5 files changed, 92 insertions(+), 41 deletions(-) diff --git a/src/System.Management.Automation/engine/LanguagePrimitives.cs b/src/System.Management.Automation/engine/LanguagePrimitives.cs index d445b7e2ff..1df4ffe1c4 100644 --- a/src/System.Management.Automation/engine/LanguagePrimitives.cs +++ b/src/System.Management.Automation/engine/LanguagePrimitives.cs @@ -305,6 +305,8 @@ namespace System.Management.Automation internal delegate void MemberSetValueError(SetValueException e); internal const string OrderedAttribute = "ordered"; + internal const string DoublePrecision = "G15"; + internal const string SinglePrecision = "G7"; internal static void CreateMemberNotFoundError(PSObject pso, DictionaryEntry property, Type resultType) { @@ -3311,6 +3313,17 @@ namespace System.Management.Automation try { // Ignore formatProvider here, the conversion should be culture invariant. + var numberFormat = CultureInfo.InvariantCulture.NumberFormat; + if (valueToConvert is double dbl) + { + return dbl.ToString(DoublePrecision, numberFormat); + } + + if (valueToConvert is float sgl) + { + return sgl.ToString(SinglePrecision, numberFormat); + } + return (string)Convert.ChangeType(valueToConvert, resultType, CultureInfo.InvariantCulture.NumberFormat); } catch (Exception e) diff --git a/src/System.Management.Automation/engine/MshObject.cs b/src/System.Management.Automation/engine/MshObject.cs index 6e8590dd05..944f541216 100644 --- a/src/System.Management.Automation/engine/MshObject.cs +++ b/src/System.Management.Automation/engine/MshObject.cs @@ -1269,6 +1269,42 @@ namespace System.Management.Automation /// internal static string ToString(ExecutionContext context, object obj, string separator, string format, IFormatProvider formatProvider, bool recurse, bool unravelEnumeratorOnRecurse) { + bool TryFastTrackPrimitiveTypes(object value, out string str) + { + switch (Convert.GetTypeCode(value)) + { + case TypeCode.String: + str = (string)value; + break; + case TypeCode.Byte: + case TypeCode.SByte: + case TypeCode.Int16: + case TypeCode.UInt16: + case TypeCode.Int32: + case TypeCode.UInt32: + case TypeCode.Int64: + case TypeCode.UInt64: + case TypeCode.DateTime: + case TypeCode.Decimal: + var formattable = (IFormattable)value; + str = formattable.ToString(format, formatProvider); + break; + case TypeCode.Double: + var dbl = (double)value; + str = dbl.ToString(format ?? LanguagePrimitives.DoublePrecision, formatProvider); + break; + case TypeCode.Single: + var sgl = (float)value; + str = sgl.ToString(format ?? LanguagePrimitives.SinglePrecision, formatProvider); + break; + default: + str = null; + return false; + } + + return true; + } + PSObject mshObj = obj as PSObject; #region plain object @@ -1279,35 +1315,9 @@ namespace System.Management.Automation return string.Empty; } - // Fast-track the primitive types... - Type objType = obj.GetType(); - TypeCode code = objType.GetTypeCode(); - switch (code) + if (TryFastTrackPrimitiveTypes(obj, out string objString)) { - case TypeCode.String: - return (string)obj; - case TypeCode.Byte: - case TypeCode.SByte: - case TypeCode.Int16: - case TypeCode.UInt16: - case TypeCode.Int32: - case TypeCode.UInt32: - case TypeCode.Int64: - case TypeCode.UInt64: - return obj.ToString(); - case TypeCode.DateTime: - DateTime dt = (DateTime)obj; - return dt.ToString(formatProvider); - case TypeCode.Decimal: - Decimal dec = (Decimal)obj; - return dec.ToString(formatProvider); - case TypeCode.Double: - double dbl = (double)obj; - return dbl.ToString(formatProvider); - - case TypeCode.Single: - float sgl = (float)obj; - return sgl.ToString(formatProvider); + return objString; } #region recurse @@ -1482,6 +1492,12 @@ namespace System.Management.Automation // Since we don't have a brokered ToString and the enumerations were not necessary or failed // we try the BaseObject's ToString object baseObject = mshObj._immediateBaseObject; + + if (TryFastTrackPrimitiveTypes(baseObject, out string baseObjString)) + { + return baseObjString; + } + IFormattable msjObjFormattable = baseObject as IFormattable; try { diff --git a/test/powershell/Language/Interop/DotNet/DotNetAPI.Tests.ps1 b/test/powershell/Language/Interop/DotNet/DotNetAPI.Tests.ps1 index be3cf81d1b..07d1c9995f 100644 --- a/test/powershell/Language/Interop/DotNet/DotNetAPI.Tests.ps1 +++ b/test/powershell/Language/Interop/DotNet/DotNetAPI.Tests.ps1 @@ -2,28 +2,24 @@ # Licensed under the MIT License. Describe "DotNetAPI" -Tags "CI" { - $posh_E = 2.718281828459045 - $posh_pi = 3.14159265358979 It "Should be able to use static .NET classes and get a constant" { - [System.Math]::E | Should -Match $posh_E.ToString() - [System.Math]::PI | Should -Match $posh_pi.ToString() + [System.Math]::E | Should -Be 2.718281828459045 + [System.Math]::PI | Should -Be 3.141592653589793 } It "Should be able to invoke a method" { - [System.Environment]::GetEnvironmentVariable("PATH") | Should -Be $env:PATH + [System.Environment]::GetEnvironmentVariable("PATH") | Should -Be $env:PATH } It "Should not require 'system' in front of static classes" { - [Environment]::CommandLine | Should -Be ([System.Environment]::CommandLine) - - [Math]::E | Should -Be ([System.Math]::E) + [Environment]::CommandLine | Should -Be ([System.Environment]::CommandLine) + [Math]::E | Should -Be ([System.Math]::E) } It "Should be able to create a new instance of a .Net object" { - [System.Guid]$guidVal = [System.Guid]::NewGuid() - - $guidVal | Should -BeOfType Guid + [System.Guid]$guidVal = [System.Guid]::NewGuid() + $guidVal | Should -BeOfType Guid } It "Should access types in System.Console" { diff --git a/test/powershell/Language/Parser/Conversions.Tests.ps1 b/test/powershell/Language/Parser/Conversions.Tests.ps1 index efe016c7d8..b5265c5e34 100644 --- a/test/powershell/Language/Parser/Conversions.Tests.ps1 +++ b/test/powershell/Language/Parser/Conversions.Tests.ps1 @@ -1,5 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. + Describe 'conversion syntax' -Tags "CI" { # these test suite covers ([]).() syntax. # it mixes two purposes: casting and super-class method calls. @@ -518,3 +519,28 @@ Describe 'method conversion' -Tags 'CI' { $Result | Should -BeNullOrEmpty } } + +Describe 'float/double precision when converting to string' -Tags "CI" { + It "-to-[string] conversion in PowerShell should use the precision specifier " -TestCases @( + @{ SourceType = [double]; Format = "G15"; ValueScript = { 1.1 * 3 }; StringConversionResult = "3.3"; ToStringResult = "3.3000000000000003" } + @{ SourceType = [double]; Format = "G15"; ValueScript = { 1.1 * 6 }; StringConversionResult = "6.6"; ToStringResult = "6.6000000000000005" } + @{ SourceType = [double]; Format = "G15"; ValueScript = { [System.Math]::E }; StringConversionResult = [System.Math]::E.ToString("G15"); ToStringResult = [System.Math]::E.ToString() } + @{ SourceType = [double]; Format = "G15"; ValueScript = { [System.Math]::PI }; StringConversionResult = [System.Math]::PI.ToString("G15"); ToStringResult = [System.Math]::PI.ToString() } + @{ SourceType = [float]; Format = "G7"; ValueScript = { [float]$f = 1.1; ($f * 3).ToSingle([cultureinfo]::InvariantCulture) }; StringConversionResult = "3.3"; ToStringResult = "3.3000002" } + @{ SourceType = [float]; Format = "G7"; ValueScript = { [float]$f = 1.1; ($f * 6).ToSingle([cultureinfo]::InvariantCulture) }; StringConversionResult = "6.6"; ToStringResult = "6.6000004" } + @{ SourceType = [float]; Format = "G7"; ValueScript = { [float]::MaxValue }; StringConversionResult = [float]::MaxValue.ToString("G7"); ToStringResult = [float]::MaxValue.ToString() } + @{ SourceType = [float]; Format = "G7"; ValueScript = { [float]::MinValue }; StringConversionResult = [float]::MinValue.ToString("G7"); ToStringResult = [float]::MinValue.ToString() } + ) { + param($SourceType, $ValueScript, $StringConversionResult, $ToStringResult) + + $value = & $ValueScript + $value | Should -BeOfType $SourceType + $value.ToString() | Should -BeExactly $ToStringResult + + $value -as [string] | Should -BeExactly $StringConversionResult + [string]$value | Should -BeExactly $StringConversionResult + [System.Management.Automation.LanguagePrimitives]::ConvertTo($value, [string]) | Should -BeExactly $StringConversionResult + "$value" | Should -BeExactly $StringConversionResult + $value | Out-String | ForEach-Object -MemberName Trim | Should -BeExactly $StringConversionResult + } +} diff --git a/test/powershell/Language/Parser/Parser.Tests.ps1 b/test/powershell/Language/Parser/Parser.Tests.ps1 index 48cdb098d2..defba16137 100644 --- a/test/powershell/Language/Parser/Parser.Tests.ps1 +++ b/test/powershell/Language/Parser/Parser.Tests.ps1 @@ -650,8 +650,8 @@ foo``u{2195}abc @{ Script = "-6.5"; ExpectedValue = "-6.5"; ExpectedType = [double] } @{ Script = "9.12"; ExpectedValue = "9.12"; ExpectedType = [double] } @{ Script = ".01"; ExpectedValue = "0.01"; ExpectedType = [double] } - @{ Script = $([single]::MinValue); ExpectedValue = $([float]::MinValue).ToString(); ExpectedType = [double] } - @{ Script = $([float]::MaxValue); ExpectedValue = $([float]::MaxValue).ToString(); ExpectedType = [double] } + @{ Script = $([single]::MinValue); ExpectedValue = $([float]::MinValue).ToString("G7"); ExpectedType = [double] } + @{ Script = $([float]::MaxValue); ExpectedValue = $([float]::MaxValue).ToString("G7"); ExpectedType = [double] } #Exponential @{ Script = "0e0"; ExpectedValue = "0"; ExpectedType = [double] } @{ Script = "0e1"; ExpectedValue = "0"; ExpectedType = [double] }