From a134fd2a6f9b05c849bd9777a661625d4a568104 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 13 Jun 2019 12:52:54 -0700 Subject: [PATCH 1/5] Use the original precision (prior-dotnet-core-3) for double/fload-to-string conversion --- .../engine/LanguagePrimitives.cs | 13 ++++ .../engine/MshObject.cs | 65 ++++++++++++++----- .../Language/Parser/Conversions.Tests.ps1 | 22 +++++++ 3 files changed, 82 insertions(+), 18 deletions(-) diff --git a/src/System.Management.Automation/engine/LanguagePrimitives.cs b/src/System.Management.Automation/engine/LanguagePrimitives.cs index d445b7e2ffc..1df4ffe1c45 100644 --- a/src/System.Management.Automation/engine/LanguagePrimitives.cs +++ b/src/System.Management.Automation/engine/LanguagePrimitives.cs @@ -305,6 +305,8 @@ public static class LanguagePrimitives 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 @@ private static string ConvertNumericToString(object valueToConvert, 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 6e8590dd050..692104e7bd2 100644 --- a/src/System.Management.Automation/engine/MshObject.cs +++ b/src/System.Management.Automation/engine/MshObject.cs @@ -1269,23 +1269,19 @@ internal static string ToStringParser(ExecutionContext context, object obj, IFor /// internal static string ToString(ExecutionContext context, object obj, string separator, string format, IFormatProvider formatProvider, bool recurse, bool unravelEnumeratorOnRecurse) { - PSObject mshObj = obj as PSObject; - - #region plain object - if (mshObj == null) + bool TryFastTrackPrimitiveTypes(object obj, out string str) { - if (obj == null) - { - return string.Empty; - } + str = null; + bool success = true; + + Type valueType = obj.GetType(); + TypeCode code = valueType.GetTypeCode(); - // Fast-track the primitive types... - Type objType = obj.GetType(); - TypeCode code = objType.GetTypeCode(); switch (code) { case TypeCode.String: - return (string)obj; + str = (string)obj; + break; case TypeCode.Byte: case TypeCode.SByte: case TypeCode.Int16: @@ -1294,20 +1290,46 @@ internal static string ToString(ExecutionContext context, object obj, string sep case TypeCode.UInt32: case TypeCode.Int64: case TypeCode.UInt64: - return obj.ToString(); + str = obj.ToString(); + break; case TypeCode.DateTime: DateTime dt = (DateTime)obj; - return dt.ToString(formatProvider); + str = dt.ToString(formatProvider); + break; case TypeCode.Decimal: Decimal dec = (Decimal)obj; - return dec.ToString(formatProvider); + str = dec.ToString(formatProvider); + break; case TypeCode.Double: double dbl = (double)obj; - return dbl.ToString(formatProvider); - + str = dbl.ToString(LanguagePrimitives.DoublePrecision, formatProvider); + break; case TypeCode.Single: float sgl = (float)obj; - return sgl.ToString(formatProvider); + str = sgl.ToString(LanguagePrimitives.SinglePrecision, formatProvider); + break; + default: + success = false; + break; + } + + return success; + } + + PSObject mshObj = obj as PSObject; + + #region plain object + if (mshObj == null) + { + if (obj == null) + { + return string.Empty; + } + + // Fast-track the primitive types... + if (TryFastTrackPrimitiveTypes(obj, out string objString)) + { + return objString; } #region recurse @@ -1482,6 +1504,13 @@ internal static string ToString(ExecutionContext context, object obj, string sep // 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; + + // Fast-track the primitive types... + if (TryFastTrackPrimitiveTypes(baseObject, out string baseObjString)) + { + return baseObjString; + } + IFormattable msjObjFormattable = baseObject as IFormattable; try { diff --git a/test/powershell/Language/Parser/Conversions.Tests.ps1 b/test/powershell/Language/Parser/Conversions.Tests.ps1 index efe016c7d8b..7fc44cd45dd 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,24 @@ 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 = [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" } + ) { + 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 + } +} From 0ec2977ea59f881ee3da6e0bd638e6281e8b08b8 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 13 Jun 2019 14:37:02 -0700 Subject: [PATCH 2/5] Address Rob's comment --- src/System.Management.Automation/engine/MshObject.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/System.Management.Automation/engine/MshObject.cs b/src/System.Management.Automation/engine/MshObject.cs index 692104e7bd2..0fdd229a1a1 100644 --- a/src/System.Management.Automation/engine/MshObject.cs +++ b/src/System.Management.Automation/engine/MshObject.cs @@ -1271,9 +1271,6 @@ internal static string ToString(ExecutionContext context, object obj, string sep { bool TryFastTrackPrimitiveTypes(object obj, out string str) { - str = null; - bool success = true; - Type valueType = obj.GetType(); TypeCode code = valueType.GetTypeCode(); @@ -1309,11 +1306,11 @@ bool TryFastTrackPrimitiveTypes(object obj, out string str) str = sgl.ToString(LanguagePrimitives.SinglePrecision, formatProvider); break; default: - success = false; - break; + str = null; + return false; } - return success; + return true; } PSObject mshObj = obj as PSObject; @@ -1326,7 +1323,6 @@ bool TryFastTrackPrimitiveTypes(object obj, out string str) return string.Empty; } - // Fast-track the primitive types... if (TryFastTrackPrimitiveTypes(obj, out string objString)) { return objString; @@ -1505,7 +1501,6 @@ bool TryFastTrackPrimitiveTypes(object obj, out string str) // we try the BaseObject's ToString object baseObject = mshObj._immediateBaseObject; - // Fast-track the primitive types... if (TryFastTrackPrimitiveTypes(baseObject, out string baseObjString)) { return baseObjString; From 3fcfa67e78c2b487e8a9da86f01a046d3739d98d Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Fri, 14 Jun 2019 08:19:57 -0700 Subject: [PATCH 3/5] Fix test failures --- .../engine/MshObject.cs | 26 +++++++------------ .../Interop/DotNet/DotNetAPI.Tests.ps1 | 4 +-- .../Language/Parser/Parser.Tests.ps1 | 4 +-- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/src/System.Management.Automation/engine/MshObject.cs b/src/System.Management.Automation/engine/MshObject.cs index 0fdd229a1a1..944f541216d 100644 --- a/src/System.Management.Automation/engine/MshObject.cs +++ b/src/System.Management.Automation/engine/MshObject.cs @@ -1269,15 +1269,12 @@ internal static string ToStringParser(ExecutionContext context, object obj, IFor /// internal static string ToString(ExecutionContext context, object obj, string separator, string format, IFormatProvider formatProvider, bool recurse, bool unravelEnumeratorOnRecurse) { - bool TryFastTrackPrimitiveTypes(object obj, out string str) + bool TryFastTrackPrimitiveTypes(object value, out string str) { - Type valueType = obj.GetType(); - TypeCode code = valueType.GetTypeCode(); - - switch (code) + switch (Convert.GetTypeCode(value)) { case TypeCode.String: - str = (string)obj; + str = (string)value; break; case TypeCode.Byte: case TypeCode.SByte: @@ -1287,23 +1284,18 @@ bool TryFastTrackPrimitiveTypes(object obj, out string str) case TypeCode.UInt32: case TypeCode.Int64: case TypeCode.UInt64: - str = obj.ToString(); - break; case TypeCode.DateTime: - DateTime dt = (DateTime)obj; - str = dt.ToString(formatProvider); - break; case TypeCode.Decimal: - Decimal dec = (Decimal)obj; - str = dec.ToString(formatProvider); + var formattable = (IFormattable)value; + str = formattable.ToString(format, formatProvider); break; case TypeCode.Double: - double dbl = (double)obj; - str = dbl.ToString(LanguagePrimitives.DoublePrecision, formatProvider); + var dbl = (double)value; + str = dbl.ToString(format ?? LanguagePrimitives.DoublePrecision, formatProvider); break; case TypeCode.Single: - float sgl = (float)obj; - str = sgl.ToString(LanguagePrimitives.SinglePrecision, formatProvider); + var sgl = (float)value; + str = sgl.ToString(format ?? LanguagePrimitives.SinglePrecision, formatProvider); break; default: str = null; diff --git a/test/powershell/Language/Interop/DotNet/DotNetAPI.Tests.ps1 b/test/powershell/Language/Interop/DotNet/DotNetAPI.Tests.ps1 index be3cf81d1ba..80233e9096b 100644 --- a/test/powershell/Language/Interop/DotNet/DotNetAPI.Tests.ps1 +++ b/test/powershell/Language/Interop/DotNet/DotNetAPI.Tests.ps1 @@ -6,8 +6,8 @@ Describe "DotNetAPI" -Tags "CI" { $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 -Match $posh_E.ToString("G15") + [System.Math]::PI | Should -Match $posh_pi.ToString("G15") } It "Should be able to invoke a method" { diff --git a/test/powershell/Language/Parser/Parser.Tests.ps1 b/test/powershell/Language/Parser/Parser.Tests.ps1 index 48cdb098d20..defba161371 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] } From 42c2761c07531924d353973b524937b965c5fae1 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Fri, 14 Jun 2019 10:25:28 -0700 Subject: [PATCH 4/5] Update tests --- .../Language/Interop/DotNet/DotNetAPI.Tests.ps1 | 16 +++++++--------- .../Language/Parser/Conversions.Tests.ps1 | 4 ++++ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/test/powershell/Language/Interop/DotNet/DotNetAPI.Tests.ps1 b/test/powershell/Language/Interop/DotNet/DotNetAPI.Tests.ps1 index 80233e9096b..c9c79d4f47c 100644 --- a/test/powershell/Language/Interop/DotNet/DotNetAPI.Tests.ps1 +++ b/test/powershell/Language/Interop/DotNet/DotNetAPI.Tests.ps1 @@ -2,28 +2,26 @@ # 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("G15") - [System.Math]::PI | Should -Match $posh_pi.ToString("G15") + [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) + [Environment]::CommandLine | Should -Be ([System.Environment]::CommandLine) - [Math]::E | Should -Be ([System.Math]::E) + [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() + [System.Guid]$guidVal = [System.Guid]::NewGuid() - $guidVal | Should -BeOfType Guid + $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 7fc44cd45dd..b5265c5e347 100644 --- a/test/powershell/Language/Parser/Conversions.Tests.ps1 +++ b/test/powershell/Language/Parser/Conversions.Tests.ps1 @@ -524,8 +524,12 @@ 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) From e7e71c6e1287c49bfb3b3a5647fa73c6964fd51d Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Fri, 14 Jun 2019 10:27:10 -0700 Subject: [PATCH 5/5] fix the indentation --- .../Language/Interop/DotNet/DotNetAPI.Tests.ps1 | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/test/powershell/Language/Interop/DotNet/DotNetAPI.Tests.ps1 b/test/powershell/Language/Interop/DotNet/DotNetAPI.Tests.ps1 index c9c79d4f47c..07d1c9995fe 100644 --- a/test/powershell/Language/Interop/DotNet/DotNetAPI.Tests.ps1 +++ b/test/powershell/Language/Interop/DotNet/DotNetAPI.Tests.ps1 @@ -9,19 +9,17 @@ Describe "DotNetAPI" -Tags "CI" { } 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" {