diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/BaseConverterTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/BaseConverterTests.cs new file mode 100644 index 0000000000..50757f19a2 --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/BaseConverterTests.cs @@ -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(() => BaseConverter.Convert(BigInteger.One, toBase)); + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/QueryTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/QueryTests.cs index fa8a441d43..2a31631d64 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/QueryTests.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/QueryTests.cs @@ -6,6 +6,7 @@ using System.Linq; using Microsoft.CmdPal.Ext.Calc.Helper; using Microsoft.CmdPal.Ext.Calc.Pages; using Microsoft.CmdPal.Ext.UnitTestBase; +using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.VisualStudio.TestTools.UnitTesting; 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}"); } + + [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().Any(item => item.Title == expectedHexContext), + "Large integer results should still include integer conversion context items."); + } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/BaseConverter.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/BaseConverter.cs new file mode 100644 index 0000000000..7602086dde --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/BaseConverter.cs @@ -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; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ResultHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ResultHelper.cs index 0147f73c07..edc18d8068 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ResultHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ResultHelper.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Numerics; using ManagedCommon; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -66,57 +67,65 @@ public static class ResultHelper } var decimalResult = roundedResult?.ToString(outputCulture); + var decimalValue = (decimal)roundedResult; List context = []; - if (decimal.IsInteger((decimal)roundedResult)) + try { - context.Add(new Separator()); - - var i = decimal.ToInt64((decimal)roundedResult); - - // hexadecimal - try + if (decimal.IsInteger(decimalValue)) { - var hexResult = "0x" + i.ToString("X", outputCulture); - context.Add(new CommandContextItem(new CopyTextCommand(hexResult) { Name = Properties.Resources.calculator_copy_hex }) + context.Add(new Separator()); + + var i = (BigInteger)decimalValue; + + // hexadecimal + try { - Title = hexResult, - }); - } - catch (Exception ex) - { - Logger.LogError("Error converting to hex format", ex); - } - - // binary - try - { - var binaryResult = "0b" + i.ToString("B", outputCulture); - context.Add(new CommandContextItem(new CopyTextCommand(binaryResult) { Name = Properties.Resources.calculator_copy_binary }) + var hexResult = BaseConverter.Convert(i, 16); + context.Add(new CommandContextItem(new CopyTextCommand(hexResult) { Name = Properties.Resources.calculator_copy_hex }) + { + Title = hexResult, + }); + } + catch (Exception ex) { - Title = binaryResult, - }); - } - catch (Exception ex) - { - Logger.LogError("Error converting to binary format", ex); - } + Logger.LogError("Error converting to hex format", ex); + } - // octal - try - { - var octalResult = "0o" + Convert.ToString(i, 8); - context.Add(new CommandContextItem(new CopyTextCommand(octalResult) { Name = Properties.Resources.calculator_copy_octal }) + // binary + try { - Title = octalResult, - }); - } - catch (Exception ex) - { - Logger.LogError("Error converting to octal format", ex); + var binaryResult = BaseConverter.Convert(i, 2); + context.Add(new CommandContextItem(new CopyTextCommand(binaryResult) { Name = Properties.Resources.calculator_copy_binary }) + { + Title = binaryResult, + }); + } + 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)) {