calculator/src/CalcManager/CEngine/scidisp.cpp

385 lines
12 KiB
C++

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
/****************************Module*Header***********************************\
* Module Name: SCIDISP.C
*
* Module Description:
*
* Warnings:
*
* Created:
*
* Author:
\****************************************************************************/
#include <sstream>
#include <regex>
#include "Header Files/CalcEngine.h"
using namespace std;
using namespace CalcEngine;
constexpr int MAX_EXPONENT = 4;
constexpr uint32_t MAX_GROUPING_SIZE = 16;
constexpr wstring_view c_decPreSepStr = L"[+-]?(\\d*)[";
constexpr wstring_view c_decPostSepStr = L"]?(\\d*)(?:e[+-]?(\\d*))?$";
/****************************************************************************\
* void DisplayNum(void)
*
* Convert m_currentVal to a string in the current radix.
*
* Updates the following variables:
* m_currentVal, m_numberString
\****************************************************************************/
//
// State of calc last time DisplayNum was called
//
typedef struct
{
Rational value;
int32_t precision;
uint32_t radix;
int nFE;
NUM_WIDTH numwidth;
bool fIntMath;
bool bRecord;
bool bUseSep;
} LASTDISP;
static LASTDISP gldPrevious = { 0, -1, 0, -1, (NUM_WIDTH)-1, false, false, false };
// Truncates if too big, makes it a non negative - the number in rat. Doesn't do anything if not in INT mode
CalcEngine::Rational CCalcEngine::TruncateNumForIntMath(CalcEngine::Rational const& rat)
{
if (!m_fIntegerMode)
{
return rat;
}
// Truncate to an integer. Do not round here.
auto result = RationalMath::Integer(rat);
// Can be converting a dec negative number to Hex/Oct/Bin rep. Use 2's complement form
// Check the range.
if (result < 0)
{
// if negative make positive by doing a twos complement
result = -(result)-1;
result ^= GetChopNumber();
}
result &= GetChopNumber();
return result;
}
void CCalcEngine::DisplayNum(void)
{
//
// Only change the display if
// we are in record mode -OR-
// this is the first time DisplayNum has been called, -OR-
// something important has changed since the last time DisplayNum was
// called.
//
if (m_bRecord || gldPrevious.value != m_currentVal || gldPrevious.precision != m_precision || gldPrevious.radix != m_radix || gldPrevious.nFE != (int)m_nFE
|| gldPrevious.bUseSep != true || gldPrevious.numwidth != m_numwidth || gldPrevious.fIntMath != m_fIntegerMode || gldPrevious.bRecord != m_bRecord)
{
gldPrevious.precision = m_precision;
gldPrevious.radix = m_radix;
gldPrevious.nFE = (int)m_nFE;
gldPrevious.numwidth = m_numwidth;
gldPrevious.fIntMath = m_fIntegerMode;
gldPrevious.bRecord = m_bRecord;
gldPrevious.bUseSep = true;
if (m_bRecord)
{
// Display the string and return.
m_numberString = m_input.ToString(m_radix);
}
else
{
// If we're in Programmer mode, perform integer truncation so e.g. 5 / 2 * 2 results in 4, not 5.
if (m_fIntegerMode)
{
m_currentVal = TruncateNumForIntMath(m_currentVal);
}
m_numberString = GetStringForDisplay(m_currentVal, m_radix);
}
// Displayed number can go through transformation. So copy it after transformation
gldPrevious.value = m_currentVal;
if ((m_radix == 10) && IsNumberInvalid(m_numberString, MAX_EXPONENT, m_precision, m_radix))
{
DisplayError(CALC_E_OVERFLOW);
}
else
{
// Display the string and return.
SetPrimaryDisplay(GroupDigitsPerRadix(m_numberString, m_radix));
}
}
}
int CCalcEngine::IsNumberInvalid(const wstring& numberString, int iMaxExp, int iMaxMantissa, uint32_t radix) const
{
int iError = 0;
if (radix == 10)
{
// start with an optional + or -
// followed by zero or more digits
// followed by an optional decimal point
// followed by zero or more digits
// followed by an optional exponent
// in case there's an exponent:
// its optionally followed by a + or -
// which is followed by zero or more digits
wregex rx(wstring{ c_decPreSepStr } + m_decimalSeparator + wstring{ c_decPostSepStr });
wsmatch matches;
if (regex_match(numberString, matches, rx))
{
// Check that exponent isn't too long
if (matches.length(3) > iMaxExp)
{
iError = IDS_ERR_INPUT_OVERFLOW;
}
else
{
wstring exp = matches.str(1);
auto intItr = exp.begin();
auto intEnd = exp.end();
while (intItr != intEnd && *intItr == L'0')
{
intItr++;
}
auto iMantissa = distance(intItr, intEnd) + matches.length(2);
if (iMantissa > iMaxMantissa)
{
iError = IDS_ERR_INPUT_OVERFLOW;
}
}
}
else
{
iError = IDS_ERR_UNK_CH;
}
}
else
{
for (const wchar_t& c : numberString)
{
if (radix == 16)
{
if (!(iswdigit(c) || (c >= L'A' && c <= L'F')))
{
iError = IDS_ERR_UNK_CH;
}
}
else if (c < L'0' || c >= L'0' + radix)
{
iError = IDS_ERR_UNK_CH;
}
}
}
return iError;
}
/****************************************************************************\
*
* DigitGroupingStringToGroupingVector
*
* Description:
* This will take the digit grouping string found in the regional applet and
* represent this string as a vector.
*
* groupingString
* 0;0 - no grouping
* 3;0 - group every 3 digits
* 3 - group 1st 3, then no grouping after
* 3;0;0 - group 1st 3, then no grouping after
* 3;2;0 - group 1st 3 and then every 2 digits
* 4;0 - group every 4 digits
* 5;3;2;0 - group 5, then 3, then every 2
* 5;3;2 - group 5, then 3, then 2, then no grouping after
*
* Returns: the groupings as a vector
*
\****************************************************************************/
vector<uint32_t> CCalcEngine::DigitGroupingStringToGroupingVector(wstring_view groupingString)
{
vector<uint32_t> grouping;
uint32_t currentGroup = 0;
wchar_t* next = nullptr;
const wchar_t* begin = groupingString.data();
const wchar_t* end = begin + groupingString.length();
for (auto itr = begin; itr != end; ++itr)
{
// Try to parse a grouping number from the string
currentGroup = wcstoul(itr, &next, 10);
// If we successfully parsed a group, add it to the grouping.
if (currentGroup < MAX_GROUPING_SIZE)
{
grouping.emplace_back(currentGroup);
}
// If we found a grouping and aren't at the end of the string yet,
// jump to the next position in the string (the ';').
// The loop will then increment us to the next character, which should be a number.
if (next && (static_cast<size_t>(next - begin) < groupingString.length()))
{
itr = next;
}
}
return grouping;
}
wstring CCalcEngine::GroupDigitsPerRadix(wstring_view numberString, uint32_t radix)
{
if (numberString.empty())
{
return wstring{};
}
switch (radix)
{
case 10:
return GroupDigits(wstring{ m_groupSeparator }, m_decGrouping, numberString, (L'-' == numberString[0]));
case 8:
return GroupDigits(L" ", { 3, 0 }, numberString);
case 2:
case 16:
return GroupDigits(L" ", { 4, 0 }, numberString);
default:
return wstring{ numberString };
}
}
/****************************************************************************\
*
* GroupDigits
*
* Description:
* This routine will take a grouping vector and the display string and
* add the separator according to the pattern indicated by the separator.
*
* Grouping
* 0,0 - no grouping
* 3,0 - group every 3 digits
* 3 - group 1st 3, then no grouping after
* 3,0,0 - group 1st 3, then no grouping after
* 3,2,0 - group 1st 3 and then every 2 digits
* 4,0 - group every 4 digits
* 5,3,2,0 - group 5, then 3, then every 2
* 5,3,2 - group 5, then 3, then 2, then no grouping after
*
\***************************************************************************/
wstring CCalcEngine::GroupDigits(wstring_view delimiter, vector<uint32_t> const& grouping, wstring_view displayString, bool isNumNegative)
{
// if there's nothing to do, bail
if (delimiter.empty() || grouping.empty())
{
return wstring{ displayString };
}
// Find the position of exponential 'e' in the string
size_t exp = displayString.find(L'e');
bool hasExponent = (exp != wstring_view::npos);
// Find the position of decimal point in the string
size_t dec = displayString.find(m_decimalSeparator);
bool hasDecimal = (dec != wstring_view::npos);
// Create an iterator that points to the end of the portion of the number subject to grouping (i.e. left of the decimal)
auto ritr = displayString.rend();
if (hasDecimal)
{
ritr -= dec;
}
else if (hasExponent)
{
ritr -= exp;
}
else
{
ritr = displayString.rbegin();
}
wstring result;
uint32_t groupingSize = 0;
auto groupItr = grouping.begin();
auto currGrouping = *groupItr;
// Mark the 'end' of the string as either rend() or rend()-1 if there is a negative sign
// We exclude the sign here because we don't want to end up with e.g. "-,123,456"
// Then, iterate from back to front, adding group delimiters as needed.
auto reverse_end = displayString.rend() - (isNumNegative ? 1 : 0);
while (ritr != reverse_end)
{
result += *ritr++;
groupingSize++;
// If a group is complete, add a separator
// Do not add a separator if:
// - grouping size is 0
// - we are at the end of the digit string
if (currGrouping != 0 && (groupingSize % currGrouping) == 0 && ritr != reverse_end)
{
result += delimiter;
groupingSize = 0; // reset for a new group
// Shift the grouping to next values if they exist
if (groupItr != grouping.end())
{
++groupItr;
// Loop through grouping vector until we find a non-zero value.
// "0" values may appear in a form of either e.g. "3;0" or "3;0;0".
// A 0 in the last position means repeat the previous grouping.
// A 0 in another position is a group. So, "3;0;0" means "group 3, then group 0 repeatedly"
// This could be expressed as just "3" but GetLocaleInfo is returning 3;0;0 in some cases instead.
for (currGrouping = 0; groupItr != grouping.end(); ++groupItr)
{
// If it's a non-zero value, that's our new group
if (*groupItr != 0)
{
currGrouping = *groupItr;
break;
}
// Otherwise, save the previous grouping in case we need to repeat it
currGrouping = *(groupItr - 1);
}
}
}
}
// now copy the negative sign if it is there
if (isNumNegative)
{
result += displayString[0];
}
reverse(result.begin(), result.end());
// Add the right (fractional or exponential) part of the number to the final string.
if (hasDecimal)
{
result += displayString.substr(dec);
}
else if (hasExponent)
{
result += displayString.substr(exp);
}
return result;
}