From 08baf27b80e604d1685c065ea75761508634de12 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 18 Jul 2022 14:27:06 -0700 Subject: [PATCH] Render decimal numbers in a table using current culture (#17650) --- .../DisplayDatabase/displayDescriptionData.cs | 1 + .../common/FormatViewGenerator_Table.cs | 6 ++ .../common/Utilities/MshObjectUtil.cs | 79 ++++++++++++------- .../Format-List.Tests.ps1 | 21 +++++ .../Format-Table.Tests.ps1 | 19 +++++ 5 files changed, 98 insertions(+), 28 deletions(-) diff --git a/src/System.Management.Automation/FormatAndOutput/common/DisplayDatabase/displayDescriptionData.cs b/src/System.Management.Automation/FormatAndOutput/common/DisplayDatabase/displayDescriptionData.cs index f126dddcad..7c9d824165 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/DisplayDatabase/displayDescriptionData.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/DisplayDatabase/displayDescriptionData.cs @@ -361,6 +361,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format internal sealed class FieldFormattingDirective { internal string formatString = null; // optional + internal bool isTable = false; } #endregion Elementary Tokens diff --git a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Table.cs b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Table.cs index bfd3291dd4..f4a6aa4e45 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Table.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Table.cs @@ -483,6 +483,12 @@ namespace Microsoft.PowerShell.Commands.Internal.Format directive = activeAssociationList[k].OriginatingParameter.GetEntry(FormatParameterDefinitionKeys.FormatStringEntryKey) as FieldFormattingDirective; } + if (directive is null) + { + directive = new FieldFormattingDirective(); + directive.isTable = true; + } + fpf.propertyValue = this.GetExpressionDisplayValue(so, enumerationLimit, this.activeAssociationList[k].ResolvedExpression, directive); tre.formatPropertyFieldList.Add(fpf); } diff --git a/src/System.Management.Automation/FormatAndOutput/common/Utilities/MshObjectUtil.cs b/src/System.Management.Automation/FormatAndOutput/common/Utilities/MshObjectUtil.cs index 8d95f8fd9a..ce3590520a 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/Utilities/MshObjectUtil.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/Utilities/MshObjectUtil.cs @@ -208,8 +208,9 @@ namespace Microsoft.PowerShell.Commands.Internal.Format /// Expression factory to create PSPropertyExpression. /// Limit on IEnumerable enumeration. /// Stores errors during string conversion. + /// Determine if to format floating point numbers using current culture. /// String representation. - internal static string SmartToString(PSObject so, PSPropertyExpressionFactory expressionFactory, int enumerationLimit, StringFormatError formatErrorObject) + internal static string SmartToString(PSObject so, PSPropertyExpressionFactory expressionFactory, int enumerationLimit, StringFormatError formatErrorObject, bool formatFloat = false) { if (so == null) return string.Empty; @@ -294,7 +295,23 @@ namespace Microsoft.PowerShell.Commands.Internal.Format return sb.ToString(); } - // take care of the case there is no base object + if (formatFloat && so.BaseObject is not null) + { + // format numbers using the current culture + if (so.BaseObject is double dbl) + { + return dbl.ToString("F"); + } + else if (so.BaseObject is float f) + { + return f.ToString("F"); + } + else if (so.BaseObject is decimal d) + { + return d.ToString("F"); + } + } + return so.ToString(); } catch (Exception e) when (e is ExtendedTypeSystemException || e is InvalidOperationException) @@ -333,43 +350,49 @@ namespace Microsoft.PowerShell.Commands.Internal.Format StringFormatError formatErrorObject, PSPropertyExpressionFactory expressionFactory) { PSObject so = PSObjectHelper.AsPSObject(val); - if (directive != null && !string.IsNullOrEmpty(directive.formatString)) + bool isTable = false; + if (directive is not null) { - // we have a formatting directive, apply it - // NOTE: with a format directive, we do not make any attempt - // to deal with IEnumerable - try + isTable = directive.isTable; + if (!string.IsNullOrEmpty(directive.formatString)) { - // use some heuristics to determine if we have "composite formatting" - // 2004/11/16-JonN This is heuristic but should be safe enough - if (directive.formatString.Contains("{0") || directive.formatString.Contains('}')) + // we have a formatting directive, apply it + // NOTE: with a format directive, we do not make any attempt + // to deal with IEnumerable + try { - // we do have it, just use it - return string.Format(CultureInfo.CurrentCulture, directive.formatString, so); + // use some heuristics to determine if we have "composite formatting" + // 2004/11/16-JonN This is heuristic but should be safe enough + if (directive.formatString.Contains("{0") || directive.formatString.Contains('}')) + { + // we do have it, just use it + return string.Format(CultureInfo.CurrentCulture, directive.formatString, so); + } + // we fall back to the PSObject's IFormattable.ToString() + // pass a null IFormatProvider + return so.ToString(directive.formatString, formatProvider: null); } - // we fall back to the PSObject's IFormattable.ToString() - // pass a null IFormatProvider - return so.ToString(directive.formatString, null); - } - catch (Exception e) // 2004/11/17-JonN This covers exceptions thrown in - // string.Format and PSObject.ToString(). - // I think we can swallow these. - { - // NOTE: we catch all the exceptions, since we do not know - // what the underlying object access would throw - if (formatErrorObject != null) + catch (Exception e) // 2004/11/17-JonN This covers exceptions thrown in + // string.Format and PSObject.ToString(). + // I think we can swallow these. { - formatErrorObject.sourceObject = so; - formatErrorObject.exception = e; - formatErrorObject.formatString = directive.formatString; - return string.Empty; + // NOTE: we catch all the exceptions, since we do not know + // what the underlying object access would throw + if (formatErrorObject is not null) + { + formatErrorObject.sourceObject = so; + formatErrorObject.exception = e; + formatErrorObject.formatString = directive.formatString; + return string.Empty; + } } } } + // we do not have a formatting directive or we failed the formatting (fallback) // but we did not report as an error; // this call would deal with IEnumerable if the object implements it - return PSObjectHelper.SmartToString(so, expressionFactory, enumerationLimit, formatErrorObject); + return PSObjectHelper.SmartToString(so, expressionFactory, enumerationLimit, formatErrorObject, isTable); } private static PSMemberSet MaskDeserializedAndGetStandardMembers(PSObject so) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-List.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-List.Tests.ps1 index cd8d0d037b..675d1fd510 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-List.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-List.Tests.ps1 @@ -209,6 +209,27 @@ dbda : KM $actual = $actual -replace "`r`n", "`n" $actual | Should -BeExactly $expected } + + It 'Float, double, and decimal should not be truncated to number of decimals from current culture' { + $o = [PSCustomObject]@{ + double = [double]1234.56789 + float = [float]9876.543 + decimal = [decimal]4567.123456789 + } + + $expected = @" + +double : 1234.56789 +float : 9876.543 +decimal : 4567.123456789 + + + +"@ + + $actual = $o | Format-List | Out-String + ($actual.Replace("`r`n", "`n")) | Should -BeExactly ($expected.Replace("`r`n", "`n")) + } } Describe 'Format-List color tests' { diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-Table.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-Table.Tests.ps1 index 3d4111e89a..47364842b7 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-Table.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-Table.Tests.ps1 @@ -845,6 +845,25 @@ A Name B $actual = $obj | Format-Table | Out-String ($actual.Replace("`r`n", "`n")) | Should -BeExactly ($expected.Replace("`r`n", "`n")) } + + It 'Table should format floats, doubles, and decimals with number of decimals from current culture' { + $o = [PSCustomObject]@{ + double = [double]1234.56789 + float = [float]9876.54321 + decimal = [decimal]4567.123456789 + } + + $table = $o | Format-Table | Out-String + + $line = foreach ($line in $table.split([System.Environment]::NewLine)) { if ($line -match '^1234') { $line } } + $line | Should -Not -BeNullOrEmpty + $expectedDecimals = (Get-Culture).NumberFormat.NumberDecimalDigits + + foreach ($num in $line.split(' ', [System.StringSplitOptions]::RemoveEmptyEntries)) { + $numDecimals = $num.length - $num.indexOf((Get-Culture).NumberFormat.NumberDecimalSeparator) - 1 + $numDecimals | Should -Be $expectedDecimals -Because $num + } + } } Describe 'Table color tests' {