From 8df48d0c75fdadbc904151e8ce01885f1aa5d272 Mon Sep 17 00:00:00 2001 From: Jonathan Watt Date: Mon, 6 May 2013 00:23:18 +0100 Subject: [PATCH] Bug 853525 - Convert much of HTMLInputElement (step handling, validation, events, etc.) to use mozilla::Decimal (a Binary Coded Decimal type) to avoid many rounding issues when it has fractional step values. r=mounir --- .../src/html/HTMLFormControlAccessible.cpp | 8 +- content/html/content/src/HTMLInputElement.cpp | 276 ++++++++++-------- content/html/content/src/HTMLInputElement.h | 59 ++-- .../test/forms/test_step_attribute.html | 3 +- .../forms/test_valueasnumber_attribute.html | 4 +- layout/forms/nsRangeFrame.cpp | 41 ++- layout/forms/nsRangeFrame.h | 3 +- 7 files changed, 209 insertions(+), 185 deletions(-) diff --git a/accessible/src/html/HTMLFormControlAccessible.cpp b/accessible/src/html/HTMLFormControlAccessible.cpp index 16ed577fcdb3..8a1337e1cfea 100644 --- a/accessible/src/html/HTMLFormControlAccessible.cpp +++ b/accessible/src/html/HTMLFormControlAccessible.cpp @@ -580,7 +580,7 @@ HTMLRangeAccessible::GetMaximumValue(double* aMaximumValue) if (rv != NS_OK_NO_ARIA_VALUE) return rv; - *aMaximumValue = HTMLInputElement::FromContent(mContent)->GetMaximum(); + *aMaximumValue = HTMLInputElement::FromContent(mContent)->GetMaximum().toDouble(); return NS_OK; } @@ -592,7 +592,7 @@ HTMLRangeAccessible::GetMinimumValue(double* aMinimumValue) if (rv != NS_OK_NO_ARIA_VALUE) return rv; - *aMinimumValue = HTMLInputElement::FromContent(mContent)->GetMinimum(); + *aMinimumValue = HTMLInputElement::FromContent(mContent)->GetMinimum().toDouble(); return NS_OK; } @@ -604,7 +604,7 @@ HTMLRangeAccessible::GetMinimumIncrement(double* aMinimumIncrement) if (rv != NS_OK_NO_ARIA_VALUE) return rv; - *aMinimumIncrement = HTMLInputElement::FromContent(mContent)->GetStep(); + *aMinimumIncrement = HTMLInputElement::FromContent(mContent)->GetStep().toDouble(); return NS_OK; } @@ -615,7 +615,7 @@ HTMLRangeAccessible::GetCurrentValue(double* aCurrentValue) if (rv != NS_OK_NO_ARIA_VALUE) return rv; - *aCurrentValue = HTMLInputElement::FromContent(mContent)->GetValueAsDouble(); + *aCurrentValue = HTMLInputElement::FromContent(mContent)->GetValueAsDecimal().toDouble(); return NS_OK; } diff --git a/content/html/content/src/HTMLInputElement.cpp b/content/html/content/src/HTMLInputElement.cpp index 6a0f940593b8..dc276c5159b7 100644 --- a/content/html/content/src/HTMLInputElement.cpp +++ b/content/html/content/src/HTMLInputElement.cpp @@ -105,6 +105,14 @@ NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(Input) static NS_DEFINE_CID(kXULControllersCID, NS_XULCONTROLLERS_CID); +// This must come outside of any namespace, or else it won't overload with the +// double based version in nsMathUtils.h +inline NS_HIDDEN_(mozilla::Decimal) +NS_floorModulo(mozilla::Decimal x, mozilla::Decimal y) +{ + return (x - y * (x / y).floor()); +} + namespace mozilla { namespace dom { @@ -183,13 +191,13 @@ static const nsAttrValue::EnumTable kInputInputmodeTable[] = { // Default inputmode value is "auto". static const nsAttrValue::EnumTable* kInputDefaultInputmode = &kInputInputmodeTable[0]; -const double HTMLInputElement::kStepScaleFactorDate = 86400000; -const double HTMLInputElement::kStepScaleFactorNumberRange = 1; -const double HTMLInputElement::kStepScaleFactorTime = 1000; -const double HTMLInputElement::kDefaultStepBase = 0; -const double HTMLInputElement::kDefaultStep = 1; -const double HTMLInputElement::kDefaultStepTime = 60; -const double HTMLInputElement::kStepAny = 0; +const Decimal HTMLInputElement::kStepScaleFactorDate = 86400000; +const Decimal HTMLInputElement::kStepScaleFactorNumberRange = 1; +const Decimal HTMLInputElement::kStepScaleFactorTime = 1000; +const Decimal HTMLInputElement::kDefaultStepBase = 0; +const Decimal HTMLInputElement::kDefaultStep = 1; +const Decimal HTMLInputElement::kDefaultStepTime = 60; +const Decimal HTMLInputElement::kStepAny = 0; #define NS_INPUT_ELEMENT_STATE_IID \ { /* dc3b3d14-23e2-4479-b513-7b369343e3a0 */ \ @@ -1144,9 +1152,19 @@ HTMLInputElement::IsValueEmpty() const return value.IsEmpty(); } +static Decimal StringToDecimal(nsAString& aValue) +{ + if (!IsASCII(aValue)) { + return Decimal::nan(); + } + NS_LossyConvertUTF16toASCII asciiString(aValue); + std::string stdString = asciiString.get(); + return Decimal::fromString(stdString); +} + bool HTMLInputElement::ConvertStringToNumber(nsAString& aValue, - double& aResultValue) const + Decimal& aResultValue) const { MOZ_ASSERT(DoesValueAsNumberApply(), "ConvertStringToNumber only applies if .valueAsNumber applies"); @@ -1155,12 +1173,10 @@ HTMLInputElement::ConvertStringToNumber(nsAString& aValue, case NS_FORM_INPUT_NUMBER: case NS_FORM_INPUT_RANGE: { - nsresult ec; - aResultValue = PromiseFlatString(aValue).ToDouble(&ec); - if (NS_FAILED(ec) || !MOZ_DOUBLE_IS_FINITE(aResultValue)) { + aResultValue = StringToDecimal(aValue); + if (!aResultValue.isFinite()) { return false; } - return true; } case NS_FORM_INPUT_DATE: @@ -1175,7 +1191,7 @@ HTMLInputElement::ConvertStringToNumber(nsAString& aValue, return false; } - aResultValue = date; + aResultValue = Decimal::fromDouble(date); return true; } case NS_FORM_INPUT_TIME: @@ -1184,7 +1200,7 @@ HTMLInputElement::ConvertStringToNumber(nsAString& aValue, return false; } - aResultValue = static_cast(milliseconds); + aResultValue = int32_t(milliseconds); return true; default: MOZ_ASSERT(false, "Unrecognized input type"); @@ -1192,16 +1208,16 @@ HTMLInputElement::ConvertStringToNumber(nsAString& aValue, } } -double -HTMLInputElement::GetValueAsDouble() const +Decimal +HTMLInputElement::GetValueAsDecimal() const { - double doubleValue; + Decimal decimalValue; nsAutoString stringValue; GetValueInternal(stringValue); - return !ConvertStringToNumber(stringValue, doubleValue) ? MOZ_DOUBLE_NaN() - : doubleValue; + return !ConvertStringToNumber(stringValue, decimalValue) ? Decimal::nan() + : decimalValue; } void @@ -1301,11 +1317,11 @@ HTMLInputElement::GetList(nsIDOMHTMLElement** aValue) } void -HTMLInputElement::SetValue(double aValue) +HTMLInputElement::SetValue(Decimal aValue) { - MOZ_ASSERT(!MOZ_DOUBLE_IS_INFINITE(aValue), "aValue must not be Infinity!"); + MOZ_ASSERT(!aValue.isInfinity(), "aValue must not be Infinity!"); - if (MOZ_DOUBLE_IS_NaN(aValue)) { + if (aValue.isNaN()) { SetValue(EmptyString()); return; } @@ -1316,12 +1332,12 @@ HTMLInputElement::SetValue(double aValue) } bool -HTMLInputElement::ConvertNumberToString(double aValue, +HTMLInputElement::ConvertNumberToString(Decimal aValue, nsAString& aResultString) const { MOZ_ASSERT(DoesValueAsNumberApply(), "ConvertNumberToString is only implemented for types implementing .valueAsNumber"); - MOZ_ASSERT(!MOZ_DOUBLE_IS_NaN(aValue) && !MOZ_DOUBLE_IS_INFINITE(aValue), + MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number."); aResultString.Truncate(); @@ -1329,16 +1345,21 @@ HTMLInputElement::ConvertNumberToString(double aValue, switch (mType) { case NS_FORM_INPUT_NUMBER: case NS_FORM_INPUT_RANGE: - aResultString.AppendFloat(aValue); - return true; + { + char buf[32]; + bool ok = aValue.toString(buf, ArrayLength(buf)); + aResultString.AssignASCII(buf); + MOZ_ASSERT(ok, "buf not big enough"); + return ok; + } case NS_FORM_INPUT_DATE: { // The specs (and our JS APIs) require |aValue| to be truncated. - aValue = floor(aValue); + aValue = aValue.floor(); - double year = JS::YearFromTime(aValue); - double month = JS::MonthFromTime(aValue); - double day = JS::DayFromTime(aValue); + double year = JS::YearFromTime(aValue.toDouble()); + double month = JS::MonthFromTime(aValue.toDouble()); + double day = JS::DayFromTime(aValue.toDouble()); if (MOZ_DOUBLE_IS_NaN(year) || MOZ_DOUBLE_IS_NaN(month) || @@ -1356,7 +1377,7 @@ HTMLInputElement::ConvertNumberToString(double aValue, // Per spec, we need to truncate |aValue| and we should only represent // times inside a day [00:00, 24:00[, which means that we should do a // modulo on |aValue| using the number of milliseconds in a day (86400000). - uint32_t value = NS_floorModulo(floor(aValue), 86400000); + uint32_t value = NS_floorModulo(aValue.floor(), 86400000).toDouble(); uint16_t milliseconds = value % 1000; value /= 1000; @@ -1438,7 +1459,7 @@ HTMLInputElement::SetValueAsDate(Nullable aDate, ErrorResult& aRv) return; } - SetValue(aDate.Value().TimeStamp()); + SetValue(Decimal::fromDouble(aDate.Value().TimeStamp())); } NS_IMETHODIMP @@ -1463,7 +1484,7 @@ HTMLInputElement::SetValueAsNumber(double aValueAsNumber, ErrorResult& aRv) return; } - SetValue(aValueAsNumber); + SetValue(Decimal::fromDouble(aValueAsNumber)); } NS_IMETHODIMP @@ -1474,15 +1495,15 @@ HTMLInputElement::SetValueAsNumber(double aValueAsNumber) return rv.ErrorCode(); } -double +Decimal HTMLInputElement::GetMinimum() const { MOZ_ASSERT(DoesValueAsNumberApply(), - "GetMinAsDouble() should only be used for types that allow .valueAsNumber"); + "GetMinimum() should only be used for types that allow .valueAsNumber"); // Only type=range has a default minimum - double defaultMinimum = - mType == NS_FORM_INPUT_RANGE ? 0.0 : MOZ_DOUBLE_NaN(); + Decimal defaultMinimum = + mType == NS_FORM_INPUT_RANGE ? 0 : Decimal::nan(); if (!HasAttr(kNameSpaceID_None, nsGkAtoms::min)) { return defaultMinimum; @@ -1491,19 +1512,19 @@ HTMLInputElement::GetMinimum() const nsAutoString minStr; GetAttr(kNameSpaceID_None, nsGkAtoms::min, minStr); - double min; + Decimal min; return ConvertStringToNumber(minStr, min) ? min : defaultMinimum; } -double +Decimal HTMLInputElement::GetMaximum() const { MOZ_ASSERT(DoesValueAsNumberApply(), - "GetMaxAsDouble() should only be used for types that allow .valueAsNumber"); + "GetMaximum() should only be used for types that allow .valueAsNumber"); // Only type=range has a default maximum - double defaultMaximum = - mType == NS_FORM_INPUT_RANGE ? 100.0 : MOZ_DOUBLE_NaN(); + Decimal defaultMaximum = + mType == NS_FORM_INPUT_RANGE ? 100 : Decimal::nan(); if (!HasAttr(kNameSpaceID_None, nsGkAtoms::max)) { return defaultMaximum; @@ -1512,11 +1533,11 @@ HTMLInputElement::GetMaximum() const nsAutoString maxStr; GetAttr(kNameSpaceID_None, nsGkAtoms::max, maxStr); - double max; + Decimal max; return ConvertStringToNumber(maxStr, max) ? max : defaultMaximum; } -double +Decimal HTMLInputElement::GetStepBase() const { MOZ_ASSERT(mType == NS_FORM_INPUT_NUMBER || @@ -1525,7 +1546,7 @@ HTMLInputElement::GetStepBase() const mType == NS_FORM_INPUT_RANGE, "Check that kDefaultStepBase is correct for this new type"); - double stepBase; + Decimal stepBase; // Do NOT use GetMinimum here - the spec says to use "the min content // attribute", not "the minimum". @@ -1552,20 +1573,20 @@ HTMLInputElement::ApplyStep(int32_t aStep) return NS_ERROR_DOM_INVALID_STATE_ERR; } - double step = GetStep(); + Decimal step = GetStep(); if (step == kStepAny) { return NS_ERROR_DOM_INVALID_STATE_ERR; } - double value = GetValueAsDouble(); - if (MOZ_DOUBLE_IS_NaN(value)) { + Decimal value = GetValueAsDecimal(); + if (value.isNaN()) { return NS_OK; } - double minimum = GetMinimum(); + Decimal minimum = GetMinimum(); - double maximum = GetMaximum(); - if (!MOZ_DOUBLE_IS_NaN(maximum)) { + Decimal maximum = GetMaximum(); + if (!maximum.isNaN()) { // "max - (max - stepBase) % step" is the nearest valid value to max. maximum = maximum - NS_floorModulo(maximum - GetStepBase(), step); } @@ -1589,15 +1610,16 @@ HTMLInputElement::ApplyStep(int32_t aStep) } } - value += aStep * step; + value += step * aStep; // For date inputs, the value can hold a string that is not a day. We do not // want to round it, as it might result in a step mismatch. Instead we want to // clamp to the next valid value. if (mType == NS_FORM_INPUT_DATE && NS_floorModulo(value - GetStepBase(), GetStepScaleFactor()) != 0) { - double validStep = EuclidLCM(static_cast(step), - static_cast(GetStepScaleFactor())); + MOZ_ASSERT(GetStep() > 0); + Decimal validStep = EuclidLCM(GetStep().floor(), + GetStepScaleFactor().floor()); if (aStep > 0) { value -= NS_floorModulo(value - GetStepBase(), validStep); value += validStep; @@ -1610,12 +1632,12 @@ HTMLInputElement::ApplyStep(int32_t aStep) // minimum unless stepUp() moves us higher than minimum. if (GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW) && aStep > 0 && value <= minimum) { - MOZ_ASSERT(!MOZ_DOUBLE_IS_NaN(minimum), "Can't be NaN if we are here"); + MOZ_ASSERT(!minimum.isNaN(), "Can't be NaN if we are here"); value = minimum; // Same goes for stepDown() and maximum. } else if (GetValidityState(VALIDITY_STATE_RANGE_OVERFLOW) && aStep < 0 && value >= maximum) { - MOZ_ASSERT(!MOZ_DOUBLE_IS_NaN(maximum), "Can't be NaN if we are here"); + MOZ_ASSERT(!maximum.isNaN(), "Can't be NaN if we are here"); value = maximum; // If we go down, we want to clamp on min. } else if (aStep < 0 && minimum == minimum) { @@ -2587,7 +2609,7 @@ void HTMLInputElement::StartRangeThumbDrag(nsGUIEvent* aEvent) { mIsDraggingRange = true; - mRangeThumbDragStartValue = GetValueAsDouble(); + mRangeThumbDragStartValue = GetValueAsDecimal(); nsIPresShell::SetCapturingContent(this, CAPTURE_IGNOREALLOWED | CAPTURE_RETARGETTOELEMENT); nsRangeFrame* rangeFrame = do_QueryFrame(GetPrimaryFrame()); @@ -2639,9 +2661,9 @@ HTMLInputElement::CancelRangeThumbDrag(bool aIsForUserEvent) } void -HTMLInputElement::SetValueOfRangeForUserEvent(double aValue) +HTMLInputElement::SetValueOfRangeForUserEvent(Decimal aValue) { - MOZ_ASSERT(MOZ_DOUBLE_IS_FINITE(aValue)); + MOZ_ASSERT(aValue.isFinite()); nsAutoString val; ConvertNumberToString(aValue, val); @@ -2980,19 +3002,17 @@ HTMLInputElement::PostHandleEvent(nsEventChainPostVisitor& aVisitor) keyEvent->keyCode == NS_VK_PAGE_DOWN || keyEvent->keyCode == NS_VK_HOME || keyEvent->keyCode == NS_VK_END)) { - double minimum = GetMinimum(); - double maximum = GetMaximum(); - MOZ_ASSERT(MOZ_DOUBLE_IS_FINITE(minimum) && - MOZ_DOUBLE_IS_FINITE(maximum)); + Decimal minimum = GetMinimum(); + Decimal maximum = GetMaximum(); + MOZ_ASSERT(minimum.isFinite() && maximum.isFinite()); if (minimum < maximum) { // else the value is locked to the minimum - double value = GetValueAsDouble(); - double step = GetStep(); + Decimal value = GetValueAsDecimal(); + Decimal step = GetStep(); if (step == kStepAny) { step = GetDefaultStep(); } - MOZ_ASSERT(MOZ_DOUBLE_IS_FINITE(value) && - MOZ_DOUBLE_IS_FINITE(step)); - double newValue; + MOZ_ASSERT(value.isFinite() && step.isFinite()); + Decimal newValue; switch (keyEvent->keyCode) { case NS_VK_LEFT: newValue = value + (IsLTR(this) ? -step : step); @@ -3017,10 +3037,10 @@ HTMLInputElement::PostHandleEvent(nsEventChainPostVisitor& aVisitor) case NS_VK_PAGE_UP: // For PgUp/PgDn we jump 10% of the total range, unless step // requires us to jump more. - newValue = value + std::max(step, 0.1 * (maximum - minimum)); + newValue = value + std::max(step, (maximum - minimum) / 10); break; case NS_VK_PAGE_DOWN: - newValue = value - std::max(step, 0.1 * (maximum - minimum)); + newValue = value - std::max(step, (maximum - minimum) / 10); break; } SetValueOfRangeForUserEvent(newValue); @@ -3430,10 +3450,9 @@ HTMLInputElement::SanitizeValue(nsAString& aValue) break; case NS_FORM_INPUT_RANGE: { - double minimum = GetMinimum(); - double maximum = GetMaximum(); - MOZ_ASSERT(MOZ_DOUBLE_IS_FINITE(minimum) && - MOZ_DOUBLE_IS_FINITE(maximum), + Decimal minimum = GetMinimum(); + Decimal maximum = GetMaximum(); + MOZ_ASSERT(minimum.isFinite() && maximum.isFinite(), "type=range should have a default maximum/minimum"); // We use this to avoid modifying the string unnecessarily, since that @@ -3441,12 +3460,12 @@ HTMLInputElement::SanitizeValue(nsAString& aValue) // parse out from aValue needs to be sanitized. bool needSanitization = false; - double value; + Decimal value; bool ok = ConvertStringToNumber(aValue, value); if (!ok) { needSanitization = true; // Set value to midway between minimum and maximum. - value = maximum <= minimum ? minimum : minimum + (maximum - minimum)/2.0; + value = maximum <= minimum ? minimum : minimum + (maximum - minimum)/2; } else if (value < minimum || maximum < minimum) { needSanitization = true; value = minimum; @@ -3455,13 +3474,13 @@ HTMLInputElement::SanitizeValue(nsAString& aValue) value = maximum; } - double step = GetStep(); + Decimal step = GetStep(); if (step != kStepAny) { - double stepBase = GetStepBase(); + Decimal stepBase = GetStepBase(); // There could be rounding issues below when dealing with fractional // numbers, but let's ignore that until ECMAScript supplies us with a // decimal number type. - double deltaToStep = NS_floorModulo(value - stepBase, step); + Decimal deltaToStep = NS_floorModulo(value - stepBase, step); if (deltaToStep != 0) { // "suffering from a step mismatch" // Round the element's value to the nearest number for which the @@ -3470,9 +3489,9 @@ HTMLInputElement::SanitizeValue(nsAString& aValue) // less than the minimum, which is less than or equal to the // maximum, if there is a number that matches these constraints: MOZ_ASSERT(deltaToStep > 0, "stepBelow/stepAbove will be wrong"); - double stepBelow = value - deltaToStep; - double stepAbove = value - deltaToStep + step; - double halfStep = step / 2; + Decimal stepBelow = value - deltaToStep; + Decimal stepAbove = value - deltaToStep + step; + Decimal halfStep = step / 2; bool stepAboveIsClosest = (stepAbove - value) <= halfStep; bool stepAboveInRange = stepAbove >= minimum && stepAbove <= maximum; @@ -3490,8 +3509,10 @@ HTMLInputElement::SanitizeValue(nsAString& aValue) } if (needSanitization) { - aValue.Truncate(); - aValue.AppendFloat(value); + char buf[32]; + DebugOnly ok = value.toString(buf, ArrayLength(buf)); + aValue.AssignASCII(buf); + MOZ_ASSERT(ok, "buf not big enough"); } } break; @@ -4976,7 +4997,7 @@ HTMLInputElement::DoesMinMaxApply() const } } -double +Decimal HTMLInputElement::GetStep() const { MOZ_ASSERT(DoesStepApply(), "GetStep() can only be called if @step applies"); @@ -4993,9 +5014,8 @@ HTMLInputElement::GetStep() const return kStepAny; } - nsresult ec; - double step = stepStr.ToDouble(&ec); - if (NS_FAILED(ec) || !MOZ_DOUBLE_IS_FINITE(step) || step <= 0) { + Decimal step = StringToDecimal(stepStr); + if (!step.isFinite() || step <= 0) { step = GetDefaultStep(); } @@ -5140,13 +5160,13 @@ HTMLInputElement::IsRangeOverflow() const return false; } - double maximum = GetMaximum(); - if (MOZ_DOUBLE_IS_NaN(maximum)) { + Decimal maximum = GetMaximum(); + if (maximum.isNaN()) { return false; } - double value = GetValueAsDouble(); - if (MOZ_DOUBLE_IS_NaN(value)) { + Decimal value = GetValueAsDecimal(); + if (value.isNaN()) { return false; } @@ -5160,13 +5180,13 @@ HTMLInputElement::IsRangeUnderflow() const return false; } - double minimum = GetMinimum(); - if (MOZ_DOUBLE_IS_NaN(minimum)) { + Decimal minimum = GetMinimum(); + if (minimum.isNaN()) { return false; } - double value = GetValueAsDouble(); - if (MOZ_DOUBLE_IS_NaN(value)) { + Decimal value = GetValueAsDecimal(); + if (value.isNaN()) { return false; } @@ -5180,13 +5200,13 @@ HTMLInputElement::HasStepMismatch() const return false; } - double value = GetValueAsDouble(); - if (MOZ_DOUBLE_IS_NaN(value)) { + Decimal value = GetValueAsDecimal(); + if (value.isNaN()) { // The element can't suffer from step mismatch if it's value isn't a number. return false; } - double step = GetStep(); + Decimal step = GetStep(); if (step == kStepAny) { return false; } @@ -5197,7 +5217,7 @@ HTMLInputElement::HasStepMismatch() const // an integer (millisecond precision), we can get rid of the precision // loss by rounding step. This will however lead to erroneous results // when step was intented to have a precision superior to a millisecond. - step = NS_round(step); + step = step.round(); } // Value has to be an integral multiple of step. @@ -5432,10 +5452,13 @@ HTMLInputElement::GetValidationMessage(nsAString& aValidationMessage, if (mType == NS_FORM_INPUT_NUMBER || mType == NS_FORM_INPUT_RANGE) { //We want to show the value as parsed when it's a number - double maximum = GetMaximum(); - MOZ_ASSERT(!MOZ_DOUBLE_IS_NaN(maximum)); + Decimal maximum = GetMaximum(); + MOZ_ASSERT(!maximum.isNaN()); - maxStr.AppendFloat(maximum); + char buf[32]; + DebugOnly ok = maximum.toString(buf, ArrayLength(buf)); + maxStr.AssignASCII(buf); + MOZ_ASSERT(ok, "buf not big enough"); } else if (mType == NS_FORM_INPUT_DATE || mType == NS_FORM_INPUT_TIME) { GetAttr(kNameSpaceID_None, nsGkAtoms::max, maxStr); } else { @@ -5456,10 +5479,13 @@ HTMLInputElement::GetValidationMessage(nsAString& aValidationMessage, nsAutoString minStr; if (mType == NS_FORM_INPUT_NUMBER || mType == NS_FORM_INPUT_RANGE) { - double minimum = GetMinimum(); - MOZ_ASSERT(!MOZ_DOUBLE_IS_NaN(minimum)); + Decimal minimum = GetMinimum(); + MOZ_ASSERT(!minimum.isNaN()); - minStr.AppendFloat(minimum); + char buf[32]; + DebugOnly ok = minimum.toString(buf, ArrayLength(buf)); + minStr.AssignASCII(buf); + MOZ_ASSERT(ok, "buf not big enough"); } else if (mType == NS_FORM_INPUT_DATE || mType == NS_FORM_INPUT_TIME) { GetAttr(kNameSpaceID_None, nsGkAtoms::min, minStr); } else { @@ -5477,11 +5503,11 @@ HTMLInputElement::GetValidationMessage(nsAString& aValidationMessage, { nsXPIDLString message; - double value = GetValueAsDouble(); - MOZ_ASSERT(!MOZ_DOUBLE_IS_NaN(value)); + Decimal value = GetValueAsDecimal(); + MOZ_ASSERT(!value.isNaN()); - double step = GetStep(); - MOZ_ASSERT(step != kStepAny); + Decimal step = GetStep(); + MOZ_ASSERT(step != kStepAny && step > 0); // In case this is a date and the step is not an integer, we don't want to // display the dates corresponding to the truncated timestamps of valueLow @@ -5489,18 +5515,18 @@ HTMLInputElement::GetValidationMessage(nsAString& aValidationMessage, // Instead we want the timestamps to correspond to a rounded day. That is, // we want a multiple of the step scale factor (1 day) as well as of step. if (mType == NS_FORM_INPUT_DATE) { - step = EuclidLCM(static_cast(step), - static_cast(GetStepScaleFactor())); + step = EuclidLCM(step.floor(), + GetStepScaleFactor().floor()); } - double stepBase = GetStepBase(); + Decimal stepBase = GetStepBase(); - double valueLow = value - NS_floorModulo(value - stepBase, step); - double valueHigh = value + step - NS_floorModulo(value - stepBase, step); + Decimal valueLow = value - NS_floorModulo(value - stepBase, step); + Decimal valueHigh = value + step - NS_floorModulo(value - stepBase, step); - double maximum = GetMaximum(); + Decimal maximum = GetMaximum(); - if (MOZ_DOUBLE_IS_NaN(maximum) || valueHigh <= maximum) { + if (maximum.isNaN() || valueHigh <= maximum) { nsAutoString valueLowStr, valueHighStr; ConvertNumberToString(valueLow, valueLowStr); ConvertNumberToString(valueHigh, valueHighStr); @@ -5940,7 +5966,7 @@ HTMLInputElement::GetFilterFromAccept() return filter; } -double +Decimal HTMLInputElement::GetStepScaleFactor() const { MOZ_ASSERT(DoesStepApply()); @@ -5955,11 +5981,11 @@ HTMLInputElement::GetStepScaleFactor() const return kStepScaleFactorTime; default: MOZ_ASSERT(false, "Unrecognized input type"); - return MOZ_DOUBLE_NaN(); + return Decimal::nan(); } } -double +Decimal HTMLInputElement::GetDefaultStep() const { MOZ_ASSERT(DoesStepApply()); @@ -5973,7 +5999,7 @@ HTMLInputElement::GetDefaultStep() const return kDefaultStepTime; default: MOZ_ASSERT(false, "Unrecognized input type"); - return MOZ_DOUBLE_NaN(); + return Decimal::nan(); } } @@ -6008,14 +6034,14 @@ HTMLInputElement::UpdateHasRange() return; } - double minimum = GetMinimum(); - if (!MOZ_DOUBLE_IS_NaN(minimum)) { + Decimal minimum = GetMinimum(); + if (!minimum.isNaN()) { mHasRange = true; return; } - double maximum = GetMaximum(); - if (!MOZ_DOUBLE_IS_NaN(maximum)) { + Decimal maximum = GetMaximum(); + if (!maximum.isNaN()) { mHasRange = true; return; } diff --git a/content/html/content/src/HTMLInputElement.h b/content/html/content/src/HTMLInputElement.h index 6daea9ba0e3c..a26c5f1ef0cf 100644 --- a/content/html/content/src/HTMLInputElement.h +++ b/content/html/content/src/HTMLInputElement.h @@ -20,6 +20,7 @@ #include "nsIFile.h" #include "nsIFilePicker.h" #include "nsIContentPrefService2.h" +#include "mozilla/Decimal.h" class nsDOMFileList; class nsIFilePicker; @@ -154,7 +155,7 @@ public: void StartRangeThumbDrag(nsGUIEvent* aEvent); void FinishRangeThumbDrag(nsGUIEvent* aEvent = nullptr); void CancelRangeThumbDrag(bool aIsForUserEvent = true); - void SetValueOfRangeForUserEvent(double aValue); + void SetValueOfRangeForUserEvent(Decimal aValue); virtual nsresult BindToTree(nsIDocument* aDocument, nsIContent* aParent, nsIContent* aBindingParent, @@ -314,12 +315,12 @@ public: void FireChangeEventIfNeeded(); /** - * Returns the input element's value as a double-precision float. + * Returns the input element's value as a Decimal. * Returns NaN if the current element's value is not a floating point number. * - * @return the input element's value as a double-precision float. + * @return the input element's value as a Decimal. */ - double GetValueAsDouble() const; + Decimal GetValueAsDecimal() const; /** * Returns the input's "minimum" (as defined by the HTML5 spec) as a double. @@ -329,7 +330,7 @@ public: * * NOTE: Only call this if you know DoesMinMaxApply() returns true. */ - double GetMinimum() const; + Decimal GetMinimum() const; /** * Returns the input's "maximum" (as defined by the HTML5 spec) as a double. @@ -339,7 +340,7 @@ public: * * NOTE:Only call this if you know DoesMinMaxApply() returns true. */ - double GetMaximum() const; + Decimal GetMaximum() const; // WebIDL @@ -579,7 +580,7 @@ public: double ValueAsNumber() const { - return DoesValueAsNumberApply() ? GetValueAsDouble() + return DoesValueAsNumberApply() ? GetValueAsDecimal().toDouble() : MOZ_DOUBLE_NaN(); } @@ -608,7 +609,7 @@ public: * * @return the current step value. */ - double GetStep() const; + Decimal GetStep() const; void GetValidationMessage(nsAString& aValidationMessage, ErrorResult& aRv); @@ -947,28 +948,28 @@ protected: nsIRadioGroupContainer* GetRadioGroupContainer() const; /** - * Convert a string to a number in a type specific way, + * Convert a string to a Decimal number in a type specific way, * http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#concept-input-value-string-number * ie parse a date string to a timestamp if type=date, * or parse a number string to its value if type=number. * @param aValue the string to be parsed. - * @param aResultValue the timestamp as a double. + * @param aResultValue the number as a Decimal. * @result whether the parsing was successful. */ - bool ConvertStringToNumber(nsAString& aValue, double& aResultValue) const; + bool ConvertStringToNumber(nsAString& aValue, Decimal& aResultValue) const; /** - * Convert a double to a string in a type specific way, ie convert a timestamp + * Convert a Decimal to a string in a type specific way, ie convert a timestamp * to a date string if type=date or append the number string representing the * value if type=number. * - * @param aValue the double to be converted - * @param aResultString [out] the string representing the double + * @param aValue the Decimal to be converted + * @param aResultString [out] the string representing the Decimal * @return whether the function succeded, it will fail if the current input's * type is not supported or the number can't be converted to a string * as expected by the type. */ - bool ConvertNumberToString(double aValue, nsAString& aResultString) const; + bool ConvertNumberToString(Decimal aValue, nsAString& aResultString) const; /** * Parse a date string of the form yyyy-mm-dd @@ -1017,11 +1018,11 @@ protected: static bool ParseTime(const nsAString& aValue, uint32_t* aResult); /** - * Sets the value of the element to the string representation of the double. + * Sets the value of the element to the string representation of the Decimal. * - * @param aValue The double that will be used to set the value. + * @param aValue The Decimal that will be used to set the value. */ - void SetValue(double aValue); + void SetValue(Decimal aValue); /** * Update the HAS_RANGE bit field value. @@ -1033,7 +1034,7 @@ protected: * See: * http://www.whatwg.org/specs/web-apps/current-work/multipage/common-input-element-attributes.html#concept-input-step-scale */ - double GetStepScaleFactor() const; + Decimal GetStepScaleFactor() const; /** * Return the base used to compute if a value matches step. @@ -1041,13 +1042,13 @@ protected: * * @return The step base. */ - double GetStepBase() const; + Decimal GetStepBase() const; /** * Returns the default step for the current type. * @return the default step for the current type. */ - double GetDefaultStep() const; + Decimal GetDefaultStep() const; /** * Apply a step change from stepUp or stepDown by multiplying aStep by the @@ -1123,22 +1124,22 @@ protected: * the drag started. Used to reset the input to its old value if the drag is * canceled. */ - double mRangeThumbDragStartValue; + Decimal mRangeThumbDragStartValue; // Step scale factor values, for input types that have one. - static const double kStepScaleFactorDate; - static const double kStepScaleFactorNumberRange; - static const double kStepScaleFactorTime; + static const Decimal kStepScaleFactorDate; + static const Decimal kStepScaleFactorNumberRange; + static const Decimal kStepScaleFactorTime; // Default step base value when a type do not have specific one. - static const double kDefaultStepBase; + static const Decimal kDefaultStepBase; // Default step used when there is no specified step. - static const double kDefaultStep; - static const double kDefaultStepTime; + static const Decimal kDefaultStep; + static const Decimal kDefaultStepTime; // Float value returned by GetStep() when the step attribute is set to 'any'. - static const double kStepAny; + static const Decimal kStepAny; /** * The type of this input () as an integer. diff --git a/content/html/content/test/forms/test_step_attribute.html b/content/html/content/test/forms/test_step_attribute.html index b4bb2f656205..a0cb9036d205 100644 --- a/content/html/content/test/forms/test_step_attribute.html +++ b/content/html/content/test/forms/test_step_attribute.html @@ -681,8 +681,7 @@ for (var test of data) { { step: '0.001', value: '15:05:05', min: '00:00', result: true }, { step: '0.000001', value: '15:05:05', min: '00:00', result: true }, // This value has conversion to double issues. - { step: '0.0000001', value: '15:05:05', min: '00:00', result: true, - todo: true }, + { step: '0.0000001', value: '15:05:05', min: '00:00', result: true }, // Some random values. { step: '100', value: '15:06:40', min: '00:00', result: true }, { step: '100', value: '15:05:05.010', min: '00:00', result: false, diff --git a/content/html/content/test/forms/test_valueasnumber_attribute.html b/content/html/content/test/forms/test_valueasnumber_attribute.html index eaf9ee68210b..8b7205122c96 100644 --- a/content/html/content/test/forms/test_valueasnumber_attribute.html +++ b/content/html/content/test/forms/test_valueasnumber_attribute.html @@ -108,7 +108,7 @@ function checkNumberGet() ["42", 42], ["-42", -42], // should work for negative values ["42.1234", 42.1234], - ["123.12345678912345", 123.12345678912345], // double precision + ["123.123456789123", 123.123456789123], // double precision ["1e2", 100], // e should be usable ["2e1", 20], ["1e-1", 0.1], // value after e can be negative @@ -196,7 +196,7 @@ function checkRangeGet() ["42", 42], ["-42", -42], // should work for negative values ["42.1234", 42.1234], - ["123.12345678912345", 123.12345678912345], // double precision + ["123.123456789123", 123.123456789123], // double precision ["1e2", 100], // e should be usable ["2e1", 20], ["1e-1", 0.1], // value after e can be negative diff --git a/layout/forms/nsRangeFrame.cpp b/layout/forms/nsRangeFrame.cpp index 5076ca886dd7..b2f163a71c92 100644 --- a/layout/forms/nsRangeFrame.cpp +++ b/layout/forms/nsRangeFrame.cpp @@ -369,13 +369,11 @@ nsRangeFrame::GetValueAsFractionOfRange() MOZ_ASSERT(input->GetType() == NS_FORM_INPUT_RANGE); - double value = input->GetValueAsDouble(); - double minimum = input->GetMinimum(); - double maximum = input->GetMaximum(); + Decimal value = input->GetValueAsDecimal(); + Decimal minimum = input->GetMinimum(); + Decimal maximum = input->GetMaximum(); - MOZ_ASSERT(MOZ_DOUBLE_IS_FINITE(value) && - MOZ_DOUBLE_IS_FINITE(minimum) && - MOZ_DOUBLE_IS_FINITE(maximum), + MOZ_ASSERT(value.isFinite() && minimum.isFinite() && maximum.isFinite(), "type=range should have a default maximum/minimum"); if (maximum <= minimum) { @@ -385,10 +383,10 @@ nsRangeFrame::GetValueAsFractionOfRange() MOZ_ASSERT(value >= minimum && value <= maximum, "Unsanitized value"); - return (value - minimum) / (maximum - minimum); + return ((value - minimum) / (maximum - minimum)).toDouble(); } -double +Decimal nsRangeFrame::GetValueAtEventPoint(nsGUIEvent* aEvent) { MOZ_ASSERT(aEvent->eventStructType == NS_MOUSE_EVENT || @@ -400,15 +398,14 @@ nsRangeFrame::GetValueAtEventPoint(nsGUIEvent* aEvent) MOZ_ASSERT(input->GetType() == NS_FORM_INPUT_RANGE); - double minimum = input->GetMinimum(); - double maximum = input->GetMaximum(); - MOZ_ASSERT(MOZ_DOUBLE_IS_FINITE(minimum) && - MOZ_DOUBLE_IS_FINITE(maximum), + Decimal minimum = input->GetMinimum(); + Decimal maximum = input->GetMaximum(); + MOZ_ASSERT(minimum.isFinite() && maximum.isFinite(), "type=range should have a default maximum/minimum"); if (maximum <= minimum) { return minimum; } - double range = maximum - minimum; + Decimal range = maximum - minimum; nsIntPoint absPoint; if (aEvent->eventStructType == NS_TOUCH_EVENT) { @@ -423,7 +420,7 @@ nsRangeFrame::GetValueAtEventPoint(nsGUIEvent* aEvent) if (point == nsPoint(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE)) { // We don't want to change the current value for this error state. - return GetValue(); + return static_cast(mContent)->GetValueAsDecimal(); } nsRect rangeContentRect = GetContentRectRelativeToSelf(); @@ -449,7 +446,7 @@ nsRangeFrame::GetValueAtEventPoint(nsGUIEvent* aEvent) } } - double fraction; + Decimal fraction; if (IsHorizontal()) { nscoord traversableDistance = rangeContentRect.width - thumbSize.width; if (traversableDistance <= 0) { @@ -458,9 +455,9 @@ nsRangeFrame::GetValueAtEventPoint(nsGUIEvent* aEvent) nscoord posAtStart = rangeContentRect.x + thumbSize.width/2; nscoord posAtEnd = posAtStart + traversableDistance; nscoord posOfPoint = mozilla::clamped(point.x, posAtStart, posAtEnd); - fraction = (posOfPoint - posAtStart) / double(traversableDistance); + fraction = Decimal(posOfPoint - posAtStart) / traversableDistance; if (StyleVisibility()->mDirection == NS_STYLE_DIRECTION_RTL) { - fraction = 1.0 - fraction; + fraction = Decimal(1) - fraction; } } else { nscoord traversableDistance = rangeContentRect.height - thumbSize.height; @@ -472,10 +469,10 @@ nsRangeFrame::GetValueAtEventPoint(nsGUIEvent* aEvent) nscoord posOfPoint = mozilla::clamped(point.y, posAtStart, posAtEnd); // For a vertical range, the top (posAtStart) is the highest value, so we // subtract the fraction from 1.0 to get that polarity correct. - fraction = 1.0 - (posOfPoint - posAtStart) / double(traversableDistance); + fraction = Decimal(1) - Decimal(posOfPoint - posAtStart) / traversableDistance; } - MOZ_ASSERT(fraction >= 0.0 && fraction <= 1.0); + MOZ_ASSERT(fraction >= 0 && fraction <= 1); return minimum + fraction * range; } @@ -723,19 +720,19 @@ nsRangeFrame::IsHorizontal(const nsSize *aFrameSizeOverride) const double nsRangeFrame::GetMin() const { - return static_cast(mContent)->GetMinimum(); + return static_cast(mContent)->GetMinimum().toDouble(); } double nsRangeFrame::GetMax() const { - return static_cast(mContent)->GetMaximum(); + return static_cast(mContent)->GetMaximum().toDouble(); } double nsRangeFrame::GetValue() const { - return static_cast(mContent)->GetValueAsDouble(); + return static_cast(mContent)->GetValueAsDecimal().toDouble(); } nsIAtom* diff --git a/layout/forms/nsRangeFrame.h b/layout/forms/nsRangeFrame.h index 164610d683a4..3456f05d1f1d 100644 --- a/layout/forms/nsRangeFrame.h +++ b/layout/forms/nsRangeFrame.h @@ -7,6 +7,7 @@ #define nsRangeFrame_h___ #include "mozilla/Attributes.h" +#include "mozilla/Decimal.h" #include "nsContainerFrame.h" #include "nsIAnonymousContentCreator.h" #include "nsCOMPtr.h" @@ -110,7 +111,7 @@ public: */ bool ShouldUseNativeStyle() const; - double GetValueAtEventPoint(nsGUIEvent* aEvent); + mozilla::Decimal GetValueAtEventPoint(nsGUIEvent* aEvent); /** * Helper that's used when the value of the range changes to reposition the