CmdPal: Fix exception when converting calc result to different bases (#46176)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

This PR fixes an exception that prevents showing result for big items:
- Uses `BigInteger` and custom base converter for secondary results menu
items.
- Adds extra error handler to prevent exception when creating a
secondary menu item from showing the main result to the user.
- Adds some unit tests for the new base converter.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #46167
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
This commit is contained in:
Jiří Polášek
2026-03-28 22:09:06 +01:00
committed by GitHub
parent 96f97064be
commit 72bdfb073b
4 changed files with 264 additions and 40 deletions

View File

@@ -0,0 +1,145 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Numerics;
using Microsoft.CmdPal.Ext.Calc.Helper;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Calc.UnitTests;
[TestClass]
public class BaseConverterTests
{
// Hex tests
[DataTestMethod]
[DataRow(0L, "0x0")]
[DataRow(1L, "0x1")]
[DataRow(16L, "0x10")]
[DataRow(255L, "0xFF")]
[DataRow(-1L, "-0x1")]
[DataRow(long.MaxValue, "0x7FFFFFFFFFFFFFFF")]
public void Convert_Hex_ReturnsExpected_WhenCalled(long input, string expected)
{
var result = BaseConverter.Convert(new BigInteger(input), 16);
Assert.AreEqual(expected, result);
}
[TestMethod]
public void Convert_Hex_HandlesLargeValues_WhenCalled()
{
var large = BigInteger.Parse("99999999999999999999", CultureInfo.InvariantCulture);
var result = BaseConverter.Convert(large, 16);
Assert.AreEqual("0x56BC75E2D630FFFFF", result);
}
// Binary tests
[DataTestMethod]
[DataRow(0L, "0b0")]
[DataRow(1L, "0b1")]
[DataRow(10L, "0b1010")]
[DataRow(255L, "0b11111111")]
[DataRow(-5L, "-0b101")]
public void Convert_Binary_ReturnsExpected_WhenCalled(long input, string expected)
{
var result = BaseConverter.Convert(new BigInteger(input), 2);
Assert.AreEqual(expected, result);
}
[TestMethod]
public void Convert_Binary_HandlesLargeValues_WhenCalled()
{
var large = BigInteger.Parse("99999999999999999999", CultureInfo.InvariantCulture);
var result = BaseConverter.Convert(large, 2);
Assert.IsTrue(result.StartsWith("0b", StringComparison.Ordinal));
Assert.IsTrue(result.Length > 60);
}
// Octal tests
[DataTestMethod]
[DataRow(0L, "0o0")]
[DataRow(1L, "0o1")]
[DataRow(8L, "0o10")]
[DataRow(255L, "0o377")]
[DataRow(-1L, "-0o1")]
[DataRow(-255L, "-0o377")]
[DataRow(long.MaxValue, "0o777777777777777777777")]
public void Convert_Octal_ReturnsExpected_WhenCalled(long input, string expected)
{
var result = BaseConverter.Convert(new BigInteger(input), 8);
Assert.AreEqual(expected, result);
}
[TestMethod]
public void Convert_Octal_HandlesLargeValues_WhenCalled()
{
var large = (BigInteger)long.MaxValue + 1;
var result = BaseConverter.Convert(large, 8);
Assert.AreEqual("0o1000000000000000000000", result);
}
[TestMethod]
public void Convert_Octal_HandlesLargeNegativeValues_WhenCalled()
{
var value = -BigInteger.Parse("99999999999999999999", CultureInfo.InvariantCulture);
var result = BaseConverter.Convert(value, 8);
Assert.IsTrue(result.StartsWith("-0o", StringComparison.Ordinal));
}
[TestMethod]
public void Convert_Hex_DecimalMaxValue_WhenCalled()
{
var value = BigInteger.Parse("79228162514264337593543950335", CultureInfo.InvariantCulture);
Assert.AreEqual("0xFFFFFFFFFFFFFFFFFFFFFFFF", BaseConverter.Convert(value, 16));
}
[TestMethod]
public void Convert_Hex_NegativeDecimalMaxValue_WhenCalled()
{
var value = -BigInteger.Parse("79228162514264337593543950335", CultureInfo.InvariantCulture);
Assert.AreEqual("-0xFFFFFFFFFFFFFFFFFFFFFFFF", BaseConverter.Convert(value, 16));
}
[TestMethod]
public void Convert_Binary_DecimalMaxValue_WhenCalled()
{
var value = BigInteger.Parse("79228162514264337593543950335", CultureInfo.InvariantCulture);
var result = BaseConverter.Convert(value, 2);
Assert.AreEqual("0b" + new string('1', 96), result);
}
[TestMethod]
public void Convert_Binary_NegativeDecimalMaxValue_WhenCalled()
{
var value = -BigInteger.Parse("79228162514264337593543950335", CultureInfo.InvariantCulture);
var result = BaseConverter.Convert(value, 2);
Assert.AreEqual("-0b" + new string('1', 96), result);
}
[TestMethod]
public void Convert_Octal_DecimalMaxValue_WhenCalled()
{
var value = BigInteger.Parse("79228162514264337593543950335", CultureInfo.InvariantCulture);
Assert.AreEqual("0o" + new string('7', 32), BaseConverter.Convert(value, 8));
}
[TestMethod]
public void Convert_Octal_NegativeDecimalMaxValue_WhenCalled()
{
var value = -BigInteger.Parse("79228162514264337593543950335", CultureInfo.InvariantCulture);
Assert.AreEqual("-0o" + new string('7', 32), BaseConverter.Convert(value, 8));
}
// Invalid base
[DataTestMethod]
[DataRow(0)]
[DataRow(1)]
[DataRow(17)]
[DataRow(-1)]
public void Convert_ThrowsArgumentOutOfRange_WhenBaseInvalid(int toBase)
{
Assert.ThrowsException<ArgumentOutOfRangeException>(() => BaseConverter.Convert(BigInteger.One, toBase));
}
}

View File

@@ -6,6 +6,7 @@ using System.Linq;
using Microsoft.CmdPal.Ext.Calc.Helper; using Microsoft.CmdPal.Ext.Calc.Helper;
using Microsoft.CmdPal.Ext.Calc.Pages; using Microsoft.CmdPal.Ext.Calc.Pages;
using Microsoft.CmdPal.Ext.UnitTestBase; using Microsoft.CmdPal.Ext.UnitTestBase;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Calc.UnitTests; namespace Microsoft.CmdPal.Ext.Calc.UnitTests;
@@ -82,4 +83,24 @@ public class QueryTests : CommandPaletteUnitTestBase
Assert.IsTrue(result.Title.Contains(expected, System.StringComparison.Ordinal), $"Calc trigMode convert result isn't correct. Current result: {result.Title}"); Assert.IsTrue(result.Title.Contains(expected, System.StringComparison.Ordinal), $"Calc trigMode convert result isn't correct. Current result: {result.Title}");
} }
[DataTestMethod]
[DataRow("2^64", "18446744073709551616", "0x10000000000000000")]
[DataRow("0-(2^64)", "-18446744073709551616", "-0x10000000000000000")]
public void TopLevelPageQuery_ReturnsResult_WhenIntegerExceedsInt64Bounds(string input, string expectedTitle, string expectedHexContext)
{
var settings = new Settings(outputUseEnglishFormat: true);
var page = new CalculatorListPage(settings);
page.UpdateSearchText(string.Empty, input);
var results = page.GetItems();
var result = results.FirstOrDefault();
Assert.AreEqual(1, results.Length, "Large integer results should still produce the main result item.");
Assert.IsNotNull(result);
Assert.AreEqual(expectedTitle, result!.Title);
Assert.IsTrue(
result.MoreCommands.OfType<CommandContextItem>().Any(item => item.Title == expectedHexContext),
"Large integer results should still include integer conversion context items.");
}
} }

View File

@@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Numerics;
using System.Text;
namespace Microsoft.CmdPal.Ext.Calc.Helper;
public static class BaseConverter
{
private const string Digits = "0123456789ABCDEF";
public static string Convert(BigInteger value, int toBase)
{
var prefix = toBase switch
{
2 => "0b",
8 => "0o",
16 => "0x",
_ => string.Empty,
};
if (toBase is < 2 or > 16)
{
throw new ArgumentOutOfRangeException(nameof(toBase), "Base must be between 2 and 16.");
}
if (value == BigInteger.Zero)
{
return prefix + "0";
}
var abs = BigInteger.Abs(value);
var sb = new StringBuilder();
while (abs > 0)
{
var digit = (int)(abs % toBase);
sb.Insert(0, Digits[digit]);
abs /= toBase;
}
var sign = value < 0 ? "-" : string.Empty;
return sign + prefix + sb;
}
}

View File

@@ -5,6 +5,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Numerics;
using ManagedCommon; using ManagedCommon;
using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -66,57 +67,65 @@ public static class ResultHelper
} }
var decimalResult = roundedResult?.ToString(outputCulture); var decimalResult = roundedResult?.ToString(outputCulture);
var decimalValue = (decimal)roundedResult;
List<IContextItem> context = []; List<IContextItem> context = [];
if (decimal.IsInteger((decimal)roundedResult)) try
{ {
context.Add(new Separator()); if (decimal.IsInteger(decimalValue))
var i = decimal.ToInt64((decimal)roundedResult);
// hexadecimal
try
{ {
var hexResult = "0x" + i.ToString("X", outputCulture); context.Add(new Separator());
context.Add(new CommandContextItem(new CopyTextCommand(hexResult) { Name = Properties.Resources.calculator_copy_hex })
var i = (BigInteger)decimalValue;
// hexadecimal
try
{ {
Title = hexResult, var hexResult = BaseConverter.Convert(i, 16);
}); context.Add(new CommandContextItem(new CopyTextCommand(hexResult) { Name = Properties.Resources.calculator_copy_hex })
} {
catch (Exception ex) Title = hexResult,
{ });
Logger.LogError("Error converting to hex format", ex); }
} catch (Exception ex)
// binary
try
{
var binaryResult = "0b" + i.ToString("B", outputCulture);
context.Add(new CommandContextItem(new CopyTextCommand(binaryResult) { Name = Properties.Resources.calculator_copy_binary })
{ {
Title = binaryResult, Logger.LogError("Error converting to hex format", ex);
}); }
}
catch (Exception ex)
{
Logger.LogError("Error converting to binary format", ex);
}
// octal // binary
try try
{
var octalResult = "0o" + Convert.ToString(i, 8);
context.Add(new CommandContextItem(new CopyTextCommand(octalResult) { Name = Properties.Resources.calculator_copy_octal })
{ {
Title = octalResult, var binaryResult = BaseConverter.Convert(i, 2);
}); context.Add(new CommandContextItem(new CopyTextCommand(binaryResult) { Name = Properties.Resources.calculator_copy_binary })
} {
catch (Exception ex) Title = binaryResult,
{ });
Logger.LogError("Error converting to octal format", ex); }
catch (Exception ex)
{
Logger.LogError("Error converting to binary format", ex);
}
// octal
try
{
var octalResult = BaseConverter.Convert(i, 8);
context.Add(new CommandContextItem(new CopyTextCommand(octalResult) { Name = Properties.Resources.calculator_copy_octal })
{
Title = octalResult,
});
}
catch (Exception ex)
{
Logger.LogError("Error converting to octal format", ex);
}
} }
} }
catch (Exception ex)
{
Logger.LogError("Error creating integer context items", ex);
}
return new ListItem(new CopyTextCommand(decimalResult)) return new ListItem(new CopyTextCommand(decimalResult))
{ {