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..944f541216d 100644 --- a/src/System.Management.Automation/engine/MshObject.cs +++ b/src/System.Management.Automation/engine/MshObject.cs @@ -1269,23 +1269,13 @@ 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 value, out string str) { - if (obj == null) - { - return string.Empty; - } - - // Fast-track the primitive types... - Type objType = obj.GetType(); - TypeCode code = objType.GetTypeCode(); - switch (code) + switch (Convert.GetTypeCode(value)) { case TypeCode.String: - return (string)obj; + str = (string)value; + break; case TypeCode.Byte: case TypeCode.SByte: case TypeCode.Int16: @@ -1294,20 +1284,40 @@ internal static string ToString(ExecutionContext context, object obj, string sep 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); + var formattable = (IFormattable)value; + str = formattable.ToString(format, formatProvider); + break; case TypeCode.Double: - double dbl = (double)obj; - return dbl.ToString(formatProvider); - + var dbl = (double)value; + str = dbl.ToString(format ?? LanguagePrimitives.DoublePrecision, formatProvider); + break; case TypeCode.Single: - float sgl = (float)obj; - return sgl.ToString(formatProvider); + 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 + if (mshObj == null) + { + if (obj == null) + { + return string.Empty; + } + + if (TryFastTrackPrimitiveTypes(obj, out string objString)) + { + return objString; } #region recurse @@ -1482,6 +1492,12 @@ 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; + + 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 be3cf81d1ba..07d1c9995fe 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 efe016c7d8b..b5265c5e347 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 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] }