Skip to content

Commit

Permalink
NumberBox: Add expression calculation (#1602)
Browse files Browse the repository at this point in the history
Add expression calculation back to NumberBox.
  • Loading branch information
teaP committed Nov 14, 2019
1 parent 1186ee7 commit 284999d
Show file tree
Hide file tree
Showing 11 changed files with 450 additions and 44 deletions.
23 changes: 23 additions & 0 deletions dev/Generated/NumberBox.properties.cpp
Expand Up @@ -8,6 +8,7 @@

CppWinRTActivatableClassWithDPFactory(NumberBox)

GlobalDependencyProperty NumberBoxProperties::s_AcceptsCalculationProperty{ nullptr };
GlobalDependencyProperty NumberBoxProperties::s_BasicValidationModeProperty{ nullptr };
GlobalDependencyProperty NumberBoxProperties::s_HeaderProperty{ nullptr };
GlobalDependencyProperty NumberBoxProperties::s_HyperScrollEnabledProperty{ nullptr };
Expand All @@ -29,6 +30,17 @@ NumberBoxProperties::NumberBoxProperties()

void NumberBoxProperties::EnsureProperties()
{
if (!s_AcceptsCalculationProperty)
{
s_AcceptsCalculationProperty =
InitializeDependencyProperty(
L"AcceptsCalculation",
winrt::name_of<bool>(),
winrt::name_of<winrt::NumberBox>(),
false /* isAttached */,
ValueHelper<bool>::BoxValueIfNecessary(false),
nullptr);
}
if (!s_BasicValidationModeProperty)
{
s_BasicValidationModeProperty =
Expand Down Expand Up @@ -165,6 +177,7 @@ void NumberBoxProperties::EnsureProperties()

void NumberBoxProperties::ClearProperties()
{
s_AcceptsCalculationProperty = nullptr;
s_BasicValidationModeProperty = nullptr;
s_HeaderProperty = nullptr;
s_HyperScrollEnabledProperty = nullptr;
Expand Down Expand Up @@ -245,6 +258,16 @@ void NumberBoxProperties::OnValuePropertyChanged(
winrt::get_self<NumberBox>(owner)->OnValuePropertyChanged(args);
}

void NumberBoxProperties::AcceptsCalculation(bool value)
{
static_cast<NumberBox*>(this)->SetValue(s_AcceptsCalculationProperty, ValueHelper<bool>::BoxValueIfNecessary(value));
}

bool NumberBoxProperties::AcceptsCalculation()
{
return ValueHelper<bool>::CastOrUnbox(static_cast<NumberBox*>(this)->GetValue(s_AcceptsCalculationProperty));
}

void NumberBoxProperties::BasicValidationMode(winrt::NumberBoxBasicValidationMode const& value)
{
static_cast<NumberBox*>(this)->SetValue(s_BasicValidationModeProperty, ValueHelper<winrt::NumberBoxBasicValidationMode>::BoxValueIfNecessary(value));
Expand Down
5 changes: 5 additions & 0 deletions dev/Generated/NumberBox.properties.h
Expand Up @@ -9,6 +9,9 @@ class NumberBoxProperties
public:
NumberBoxProperties();

void AcceptsCalculation(bool value);
bool AcceptsCalculation();

void BasicValidationMode(winrt::NumberBoxBasicValidationMode const& value);
winrt::NumberBoxBasicValidationMode BasicValidationMode();

Expand Down Expand Up @@ -45,6 +48,7 @@ class NumberBoxProperties
void WrapEnabled(bool value);
bool WrapEnabled();

static winrt::DependencyProperty AcceptsCalculationProperty() { return s_AcceptsCalculationProperty; }
static winrt::DependencyProperty BasicValidationModeProperty() { return s_BasicValidationModeProperty; }
static winrt::DependencyProperty HeaderProperty() { return s_HeaderProperty; }
static winrt::DependencyProperty HyperScrollEnabledProperty() { return s_HyperScrollEnabledProperty; }
Expand All @@ -58,6 +62,7 @@ class NumberBoxProperties
static winrt::DependencyProperty ValueProperty() { return s_ValueProperty; }
static winrt::DependencyProperty WrapEnabledProperty() { return s_WrapEnabledProperty; }

static GlobalDependencyProperty s_AcceptsCalculationProperty;
static GlobalDependencyProperty s_BasicValidationModeProperty;
static GlobalDependencyProperty s_HeaderProperty;
static GlobalDependencyProperty s_HyperScrollEnabledProperty;
Expand Down
85 changes: 83 additions & 2 deletions dev/NumberBox/InteractionTests/NumberBoxTests.cs
Expand Up @@ -245,13 +245,95 @@ public void CoersionTest()
}
}

[TestMethod]
public void BasicCalculationTest()
{
using (var setup = new TestSetupHelper("NumberBox Tests"))
{
RangeValueSpinner numBox = FindElement.ByName<RangeValueSpinner>("TestNumberBox");

Log.Comment("Verify that calculations don't work if AcceptsCalculations is false");
EnterText(numBox, "5 + 3");
Verify.AreEqual(0, numBox.Value);

Check("CalculationCheckBox");

int numErrors = 0;
const double resetValue = 1234;

Dictionary<string, double> expressions = new Dictionary<string, double>
{
// Valid expressions. None of these should evaluate to the reset value.
{ "5", 5 },
{ "-358", -358 },
{ "12.34", 12.34 },
{ "5 + 3", 8 },
{ "12345 + 67 + 890", 13302 },
{ "000 + 0011", 11 },
{ "5 - 3 + 2", 4 },
{ "3 + 2 - 5", 0 },
{ "9 - 2 * 6 / 4", 6 },
{ "9 - -7", 16 },
{ "9-3*2", 3 }, // no spaces
{ " 10 * 6 ", 60 }, // extra spaces
{ "10 /( 2 + 3 )", 2 },
{ "5 * -40", -200 },
{ "(1 - 4) / (2 + 1)", -1 },
{ "3 * ((4 + 8) / 2)", 18 },
{ "23 * ((0 - 48) / 8)", -138 },
{ "((74-71)*2)^3", 216 },
{ "2 - 2 ^ 3", -6 },
{ "2 ^ 2 ^ 2 / 2 + 9", 17 },
{ "5 ^ -2", 0.04 },
{ "5.09 + 14.333", 19.423 },
{ "2.5 * 0.35", 0.875 },
{ "-2 - 5", -7 }, // begins with negative number
{ "(10)", 10 }, // number in parens
{ "(-9)", -9 }, // negative number in parens
{ "0^0", 1 }, // who knew?

// These should not parse, which means they will reset back to the previous value.
{ "5x + 3y", resetValue }, // invalid chars
{ "5 + (3", resetValue }, // mismatched parens
{ "9 + (2 + 3))", resetValue },
{ "(2 + 3)(1 + 5)", resetValue }, // missing operator
{ "9 + + 7", resetValue }, // extra operators
{ "9 - * 7", resetValue },
{ "9 - - 7", resetValue },
{ "+9", resetValue },
{ "1 / 0", resetValue }, // divide by zero

// These don't currently work, but maybe should.
{ "-(3 + 5)", resetValue }, // negative sign in front of parens -- should be -8
};
foreach (KeyValuePair<string, double> pair in expressions)
{
numBox.SetValue(resetValue);
Wait.ForIdle();

EnterText(numBox, pair.Key);
string output = "Expression '" + pair.Key + "' - expected: " + pair.Value + ", actual: " + numBox.Value;
if (Math.Abs(pair.Value - numBox.Value) > 0.00001)
{
numErrors++;
Log.Warning(output);
}
else
{
Log.Comment(output);
}
}

Verify.AreEqual(0, numErrors);
}
}

Button FindButton(UIObject parent, string buttonName)
{
foreach (UIObject elem in parent.Children)
{
if (elem.Name.Equals(buttonName))
{
Log.Comment("Found " + buttonName + " button for object " + parent.Name);
return new Button(elem);
}
}
Expand All @@ -265,7 +347,6 @@ Edit FindTextBox(UIObject parent)
{
if (elem.ClassName.Equals("TextBox"))
{
Log.Comment("Found TextBox for object " + parent.Name);
return new Edit(elem);
}
}
Expand Down
52 changes: 14 additions & 38 deletions dev/NumberBox/NumberBox.cpp
Expand Up @@ -5,6 +5,7 @@
#include "common.h"
#include "NumberBox.h"
#include "NumberBoxAutomationPeer.h"
#include "NumberBoxParser.h"
#include "RuntimeProfiler.h"
#include "ResourceAccessor.h"
#include "Utils.h"
Expand Down Expand Up @@ -71,11 +72,8 @@ void NumberBox::OnApplyTemplate()
return textBox;
}());

// Initializing precision formatter. This formatter works neutrally to protect against floating point imprecision resulting from stepping/calc
m_stepPrecisionFormatter.FractionDigits(0);
m_stepPrecisionFormatter.IntegerDigits(1);
m_stepPrecisionFormatter.NumberRounder(nullptr);
m_stepPrecisionRounder.RoundingAlgorithm(winrt::RoundingAlgorithm::RoundHalfAwayFromZero);
// .NET rounds to 12 significant digits when displaying doubles, so we will do the same.
m_displayRounder.SignificantDigits(12);

SetSpinButtonVisualState();
UpdateTextToValue();
Expand Down Expand Up @@ -195,9 +193,12 @@ void NumberBox::ValidateInput()
{
// Setting NumberFormatter to something that isn't an INumberParser will throw an exception, so this should be safe
const auto numberParser = NumberFormatter().as<winrt::INumberParser>();
const auto parsedNum = numberParser.ParseDouble(text);

if (!parsedNum)
const winrt::IReference<double> value = AcceptsCalculation()
? NumberBoxParser::Compute(textBox.Text(), numberParser)
: numberParser.ParseDouble(text);

if (!value)
{
if (BasicValidationMode() == winrt::NumberBoxBasicValidationMode::InvalidInputOverwritten)
{
Expand All @@ -207,7 +208,7 @@ void NumberBox::ValidateInput()
}
else
{
Value(parsedNum.Value());
Value(value.Value());
}
}
}
Expand Down Expand Up @@ -291,47 +292,22 @@ void NumberBox::StepValue(bool isPositive)
}
}

// Safeguard for floating point imprecision errors
m_stepPrecisionRounder.SignificantDigits(ComputePrecisionRounderSigDigits(newVal));
newVal = m_stepPrecisionRounder.RoundDouble(newVal);

// Update Text and Revalidate new value
Value(newVal);
}

// Computes the number of significant digits that precision rounder should use. This helps to prevent floating point imprecision errors.
int NumberBox::ComputePrecisionRounderSigDigits(double newVal)
{
const auto oldVal = Value();

// Run formatter on both values to discard trailing and leading 0's.
const auto formattedVal = wstring_view(m_stepPrecisionFormatter.Format(oldVal));
const auto formattedStep = wstring_view(m_stepPrecisionFormatter.Format(StepFrequency()));
const auto formattedNew = wstring_view(m_stepPrecisionFormatter.Format(newVal));

// Get size of only decimal portion of both old numbers.
const auto oldValSig = static_cast<int>(formattedVal.substr(formattedVal.find_first_of('.') + 1).size());
const auto stepSig = static_cast<int>(formattedStep.substr(formattedStep.find_first_of('.') + 1).size());

// Pick bigger of two decimal sigDigits
auto result = std::max(oldValSig, stepSig);

// append # of integer digits from new value
result += (int)formattedNew.substr(0, formattedNew.find_first_of('.')).size();
return result;
}

// Runs formatter and updates TextBox to it's value property, run on construction if Value != 0
// Updates TextBox.Text with the formatted Value
void NumberBox::UpdateTextToValue()
{
if (auto&& textBox = m_textBox.get())
{
const auto formattedValue = NumberFormatter().FormatDouble(Value());
// Rounding the value here will prevent displaying digits caused by floating point imprecision.
const auto roundedValue = m_displayRounder.RoundDouble(Value());

const auto formattedValue = NumberFormatter().FormatDouble(roundedValue);
textBox.Text(formattedValue);
}
}

// Enables or Disables Spin Buttons
void NumberBox::SetSpinButtonVisualState()
{
if (SpinButtonPlacementMode() == winrt::NumberBoxSpinButtonPlacementMode::Inline)
Expand Down
5 changes: 1 addition & 4 deletions dev/NumberBox/NumberBox.h
Expand Up @@ -9,7 +9,6 @@
#include "NumberBoxValueChangedEventArgs.g.h"
#include "NumberBox.properties.h"
#include "Windows.Globalization.NumberFormatting.h"
#include <regex>

class NumberBoxValueChangedEventArgs :
public winrt::implementation::NumberBoxValueChangedEventArgsT<NumberBoxValueChangedEventArgs>
Expand Down Expand Up @@ -66,7 +65,6 @@ class NumberBox :
void ValidateInput();
void CoerceValue();
void UpdateTextToValue();
int ComputePrecisionRounderSigDigits(double newVal);

void SetSpinButtonVisualState();
void StepValue(bool isPositive);
Expand All @@ -75,8 +73,7 @@ class NumberBox :

bool IsInBounds(double value);

winrt::DecimalFormatter m_stepPrecisionFormatter{};
winrt::SignificantDigitsNumberRounder m_stepPrecisionRounder{};
winrt::SignificantDigitsNumberRounder m_displayRounder{};

tracker_ref<winrt::TextBox> m_textBox{ this };

Expand Down
4 changes: 4 additions & 0 deletions dev/NumberBox/NumberBox.idl
Expand Up @@ -66,6 +66,9 @@ unsealed runtimeclass NumberBox : Windows.UI.Xaml.Controls.Control
[MUX_DEFAULT_VALUE("false")]
Boolean WrapEnabled;

[MUX_DEFAULT_VALUE("false")]
Boolean AcceptsCalculation;

[MUX_PROPERTY_CHANGED_CALLBACK(TRUE)]
[MUX_PROPERTY_VALIDATION_CALLBACK("ValidateNumberFormatter")]
Windows.Globalization.NumberFormatting.INumberFormatter2 NumberFormatter;
Expand All @@ -85,6 +88,7 @@ unsealed runtimeclass NumberBox : Windows.UI.Xaml.Controls.Control
static Windows.UI.Xaml.DependencyProperty SpinButtonPlacementModeProperty{ get; };
static Windows.UI.Xaml.DependencyProperty HyperScrollEnabledProperty{ get; };
static Windows.UI.Xaml.DependencyProperty WrapEnabledProperty{ get; };
static Windows.UI.Xaml.DependencyProperty AcceptsCalculationProperty{ get; };

static Windows.UI.Xaml.DependencyProperty NumberFormatterProperty{ get; };
}
Expand Down
2 changes: 2 additions & 0 deletions dev/NumberBox/NumberBox.vcxitems
Expand Up @@ -17,6 +17,7 @@
<ClCompile Include="$(MSBuildThisFileDirectory)..\Generated\NumberBox.properties.cpp" />
<ClCompile Include="$(MSBuildThisFileDirectory)NumberBox.cpp" />
<ClCompile Include="$(MSBuildThisFileDirectory)NumberBoxAutomationPeer.cpp" />
<ClCompile Include="$(MSBuildThisFileDirectory)NumberBoxParser.cpp" />
</ItemGroup>
<ItemGroup>
<Page Include="$(MSBuildThisFileDirectory)NumberBox.xaml">
Expand All @@ -34,6 +35,7 @@
<ItemGroup>
<ClInclude Include="$(MSBuildThisFileDirectory)NumberBox.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)NumberBoxAutomationPeer.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)NumberBoxParser.h" />
</ItemGroup>
<ItemGroup>
<PRIResource Include="$(MSBuildThisFileDirectory)Strings\en-us\Resources.resw" />
Expand Down
2 changes: 2 additions & 0 deletions dev/NumberBox/NumberBox.vcxitems.filters
Expand Up @@ -4,6 +4,7 @@
<ClCompile Include="$(MSBuildThisFileDirectory)..\Generated\NumberBox.properties.cpp" />
<ClCompile Include="$(MSBuildThisFileDirectory)NumberBox.cpp" />
<ClCompile Include="$(MSBuildThisFileDirectory)NumberBoxAutomationPeer.cpp" />
<ClCompile Include="$(MSBuildThisFileDirectory)NumberBoxParser.cpp" />
</ItemGroup>
<ItemGroup>
<Midl Include="$(MSBuildThisFileDirectory)NumberBox.idl" />
Expand All @@ -15,6 +16,7 @@
<ItemGroup>
<ClInclude Include="$(MSBuildThisFileDirectory)NumberBox.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)NumberBoxAutomationPeer.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)NumberBoxParser.h" />
</ItemGroup>
<ItemGroup>
<PRIResource Include="$(MSBuildThisFileDirectory)Strings\en-us\Resources.resw" />
Expand Down

0 comments on commit 284999d

Please sign in to comment.