add til::math, use it for float conversions to point, size (#5150)

This pull request introduces the `til::math` namespace, which provides some casting functions to be used in support of `til::point` and `til::size`. When point/size want to ingest a floating-point structure, they _must_ be instructed on how to convert those floating-point values into integers.

This enables:

```
Windows::Foundation::Point wfPoint = /* ... */;
til::point tp{ til::math::rounding, wfPoint };
```

Future thoughts: should the TilMath types be stackable? Right now, you cannot get "checked + rounding" behavior (where it throws if it doesn't fit) so everything is saturating.

## PR Checklist
* [x] Closes a request by Michael
* [x] I've discussed this with core contributors already
This commit is contained in:
Dustin L. Howett (MSFT) 2020-03-27 15:48:49 -07:00 committed by GitHub
parent ef80f665d3
commit dfc15780c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 490 additions and 0 deletions

View file

@ -1,2 +1,3 @@
powf
sqrtf
isnan

View file

@ -482,6 +482,7 @@ cwchar
cwctype
cwd
cx
cxcy
CXFRAME
CXFULLSCREEN
CXHSCROLL

View file

@ -7,6 +7,7 @@
#include "til/at.h"
#include "til/color.h"
#include "til/math.h"
#include "til/some.h"
#include "til/size.h"
#include "til/point.h"

85
src/inc/til/math.h Normal file
View file

@ -0,0 +1,85 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
namespace til
{
// The til::math namespace contains TIL math guidance casts;
// they are intended to be used as the first argument to
// floating-point universal converters elsewhere in the til namespace.
namespace math
{
namespace details
{
struct ceiling_t
{
template<typename O, typename T>
static O cast(T val)
{
if constexpr (std::is_floating_point_v<T>)
{
THROW_HR_IF(E_ABORT, ::std::isnan(val));
return ::base::saturated_cast<O>(::std::ceil(val));
}
else
{
return ::base::saturated_cast<O>(val);
}
}
};
struct flooring_t
{
template<typename O, typename T>
static O cast(T val)
{
if constexpr (std::is_floating_point_v<T>)
{
THROW_HR_IF(E_ABORT, ::std::isnan(val));
return ::base::saturated_cast<O>(::std::floor(val));
}
else
{
return ::base::saturated_cast<O>(val);
}
}
};
struct rounding_t
{
template<typename O, typename T>
static O cast(T val)
{
if constexpr (std::is_floating_point_v<T>)
{
THROW_HR_IF(E_ABORT, ::std::isnan(val));
return ::base::saturated_cast<O>(::std::round(val));
}
else
{
return ::base::saturated_cast<O>(val);
}
}
};
struct truncating_t
{
template<typename O, typename T>
static O cast(T val)
{
if constexpr (std::is_floating_point_v<T>)
{
THROW_HR_IF(E_ABORT, ::std::isnan(val));
}
return ::base::saturated_cast<O>(val);
}
};
}
static constexpr details::ceiling_t ceiling; // positives become more positive, negatives become less negative
static constexpr details::flooring_t flooring; // positives become less positive, negatives become more negative
static constexpr details::rounding_t rounding; // it's rounding, from math class
static constexpr details::truncating_t truncating; // drop the decimal point, regardless of how close it is to the next value
}
}

View file

@ -53,6 +53,22 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
{
}
// This template will convert to size from anything that has a X and a Y field that are floating-point;
// a math type is required.
template<typename TilMath, typename TOther>
constexpr point(TilMath, const TOther& other, std::enable_if_t<std::is_floating_point_v<decltype(std::declval<TOther>().X)> && std::is_floating_point_v<decltype(std::declval<TOther>().Y)>, int> /*sentinel*/ = 0) :
point(TilMath::template cast<ptrdiff_t>(other.X), TilMath::template cast<ptrdiff_t>(other.Y))
{
}
// This template will convert to size from anything that has a x and a y field that are floating-point;
// a math type is required.
template<typename TilMath, typename TOther>
constexpr point(TilMath, const TOther& other, std::enable_if_t<std::is_floating_point_v<decltype(std::declval<TOther>().x)> && std::is_floating_point_v<decltype(std::declval<TOther>().y)>, int> /*sentinel*/ = 0) :
point(TilMath::template cast<ptrdiff_t>(other.x), TilMath::template cast<ptrdiff_t>(other.y))
{
}
constexpr bool operator==(const point& other) const noexcept
{
return _x == other._x &&

View file

@ -53,6 +53,30 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
{
}
// This template will convert to size from anything that has a X and a Y field that are floating-point;
// a math type is required.
template<typename TilMath, typename TOther>
constexpr size(TilMath, const TOther& other, std::enable_if_t<std::is_floating_point_v<decltype(std::declval<TOther>().X)> && std::is_floating_point_v<decltype(std::declval<TOther>().Y)>, int> /*sentinel*/ = 0) :
size(TilMath::template cast<ptrdiff_t>(other.X), TilMath::template cast<ptrdiff_t>(other.Y))
{
}
// This template will convert to size from anything that has a cx and a cy field that are floating-point;
// a math type is required.
template<typename TilMath, typename TOther>
constexpr size(TilMath, const TOther& other, std::enable_if_t<std::is_floating_point_v<decltype(std::declval<TOther>().cx)> && std::is_floating_point_v<decltype(std::declval<TOther>().cy)>, int> /*sentinel*/ = 0) :
size(TilMath::template cast<ptrdiff_t>(other.cx), TilMath::template cast<ptrdiff_t>(other.cy))
{
}
// This template will convert to size from anything that has a Width and a Height field that are floating-point;
// a math type is required.
template<typename TilMath, typename TOther>
constexpr size(TilMath, const TOther& other, std::enable_if_t<std::is_floating_point_v<decltype(std::declval<TOther>().Width)> && std::is_floating_point_v<decltype(std::declval<TOther>().Height)>, int> /*sentinel*/ = 0) :
size(TilMath::template cast<ptrdiff_t>(other.Width), TilMath::template cast<ptrdiff_t>(other.Height))
{
}
constexpr bool operator==(const size& other) const noexcept
{
return _width == other._width &&

View file

@ -0,0 +1,127 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "til/math.h"
using namespace WEX::Common;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
class MathTests
{
TEST_CLASS(MathTests);
template<typename TG, typename TX = ptrdiff_t>
struct TestCase
{
TG given;
TX expected;
};
template<class TilMath, typename TG, typename TX, int N>
static void _RunCases(TilMath, const std::array<TestCase<TG, TX>, N>& cases)
{
for (const auto& tc : cases)
{
VERIFY_ARE_EQUAL(tc.expected, TilMath::template cast<decltype(tc.expected)>(tc.given));
}
}
TEST_METHOD(Truncating)
{
std::array<TestCase<long double, ptrdiff_t>, 8> cases{
TestCase<long double, ptrdiff_t>{ 1., 1 },
{ 1.9, 1 },
{ -7.1, -7 },
{ -8.5, -8 },
{ PTRDIFF_MAX + 0.5, PTRDIFF_MAX },
{ PTRDIFF_MIN - 0.5, PTRDIFF_MIN },
{ INFINITY, PTRDIFF_MAX },
{ -INFINITY, PTRDIFF_MIN },
};
_RunCases(til::math::truncating, cases);
const auto fn = []() {
const auto v = til::math::details::truncating_t::cast<ptrdiff_t>(NAN);
};
VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; });
}
TEST_METHOD(Ceiling)
{
std::array<TestCase<long double, ptrdiff_t>, 8> cases{
TestCase<long double, ptrdiff_t>{ 1., 1 },
{ 1.9, 2 },
{ -7.1, -7 },
{ -8.5, -8 },
{ PTRDIFF_MAX + 0.5, PTRDIFF_MAX },
{ PTRDIFF_MIN - 0.5, PTRDIFF_MIN },
{ INFINITY, PTRDIFF_MAX },
{ -INFINITY, PTRDIFF_MIN },
};
_RunCases(til::math::ceiling, cases);
const auto fn = []() {
const auto v = til::math::details::ceiling_t::cast<ptrdiff_t>(NAN);
};
VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; });
}
TEST_METHOD(Flooring)
{
std::array<TestCase<long double, ptrdiff_t>, 8> cases{
TestCase<long double, ptrdiff_t>{ 1., 1 },
{ 1.9, 1 },
{ -7.1, -8 },
{ -8.5, -9 },
{ PTRDIFF_MAX + 0.5, PTRDIFF_MAX },
{ PTRDIFF_MIN - 0.5, PTRDIFF_MIN },
{ INFINITY, PTRDIFF_MAX },
{ -INFINITY, PTRDIFF_MIN },
};
_RunCases(til::math::flooring, cases);
const auto fn = []() {
const auto v = til::math::details::flooring_t::cast<ptrdiff_t>(NAN);
};
VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; });
}
TEST_METHOD(Rounding)
{
std::array<TestCase<long double, ptrdiff_t>, 8> cases{
TestCase<long double, ptrdiff_t>{ 1., 1 },
{ 1.9, 2 },
{ -7.1, -7 },
{ -8.5, -9 },
{ PTRDIFF_MAX + 0.5, PTRDIFF_MAX },
{ PTRDIFF_MIN - 0.5, PTRDIFF_MIN },
{ INFINITY, PTRDIFF_MAX },
{ -INFINITY, PTRDIFF_MIN },
};
_RunCases(til::math::rounding, cases);
const auto fn = []() {
const auto v = til::math::details::rounding_t::cast<ptrdiff_t>(NAN);
};
VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; });
}
TEST_METHOD(NormalIntegers)
{
std::array<TestCase<ptrdiff_t, int>, 4> cases{
TestCase<ptrdiff_t, int>{ 1, 1 },
{ -1, -1 },
{ PTRDIFF_MAX, INT_MAX },
{ PTRDIFF_MIN, INT_MIN },
};
_RunCases(til::math::rounding, cases);
}
};

View file

@ -604,4 +604,101 @@ class PointTests
// All ptrdiff_ts fit into a float, so there's no exception tests.
}
template<typename T>
struct PointTypeWith_xy
{
T x, y;
};
template<typename T>
struct PointTypeWith_XY
{
T X, Y;
};
TEST_METHOD(CastFromFloatWithMathTypes)
{
PointTypeWith_xy<float> xyFloatIntegral{ 1.f, 2.f };
PointTypeWith_xy<float> xyFloat{ 1.6f, 2.4f };
PointTypeWith_XY<double> XYDoubleIntegral{ 3., 4. };
PointTypeWith_XY<double> XYDouble{ 3.6, 4.4 };
Log::Comment(L"0.) Ceiling");
{
{
til::point converted{ til::math::ceiling, xyFloatIntegral };
VERIFY_ARE_EQUAL((til::point{ 1, 2 }), converted);
}
{
til::point converted{ til::math::ceiling, xyFloat };
VERIFY_ARE_EQUAL((til::point{ 2, 3 }), converted);
}
{
til::point converted{ til::math::ceiling, XYDoubleIntegral };
VERIFY_ARE_EQUAL((til::point{ 3, 4 }), converted);
}
{
til::point converted{ til::math::ceiling, XYDouble };
VERIFY_ARE_EQUAL((til::point{ 4, 5 }), converted);
}
}
Log::Comment(L"1.) Flooring");
{
{
til::point converted{ til::math::flooring, xyFloatIntegral };
VERIFY_ARE_EQUAL((til::point{ 1, 2 }), converted);
}
{
til::point converted{ til::math::flooring, xyFloat };
VERIFY_ARE_EQUAL((til::point{ 1, 2 }), converted);
}
{
til::point converted{ til::math::flooring, XYDoubleIntegral };
VERIFY_ARE_EQUAL((til::point{ 3, 4 }), converted);
}
{
til::point converted{ til::math::flooring, XYDouble };
VERIFY_ARE_EQUAL((til::point{ 3, 4 }), converted);
}
}
Log::Comment(L"2.) Rounding");
{
{
til::point converted{ til::math::rounding, xyFloatIntegral };
VERIFY_ARE_EQUAL((til::point{ 1, 2 }), converted);
}
{
til::point converted{ til::math::rounding, xyFloat };
VERIFY_ARE_EQUAL((til::point{ 2, 2 }), converted);
}
{
til::point converted{ til::math::rounding, XYDoubleIntegral };
VERIFY_ARE_EQUAL((til::point{ 3, 4 }), converted);
}
{
til::point converted{ til::math::rounding, XYDouble };
VERIFY_ARE_EQUAL((til::point{ 4, 4 }), converted);
}
}
Log::Comment(L"3.) Truncating");
{
{
til::point converted{ til::math::truncating, xyFloatIntegral };
VERIFY_ARE_EQUAL((til::point{ 1, 2 }), converted);
}
{
til::point converted{ til::math::truncating, xyFloat };
VERIFY_ARE_EQUAL((til::point{ 1, 2 }), converted);
}
{
til::point converted{ til::math::truncating, XYDoubleIntegral };
VERIFY_ARE_EQUAL((til::point{ 3, 4 }), converted);
}
{
til::point converted{ til::math::truncating, XYDouble };
VERIFY_ARE_EQUAL((til::point{ 3, 4 }), converted);
}
}
}
};

View file

@ -515,4 +515,140 @@ class SizeTests
// All ptrdiff_ts fit into a float, so there's no exception tests.
}
template<typename T>
struct SizeTypeWith_XY
{
T X, Y;
};
template<typename T>
struct SizeTypeWith_cxcy
{
T cx, cy;
};
template<typename T>
struct SizeTypeWith_WidthHeight
{
T Width, Height;
};
TEST_METHOD(CastFromFloatWithMathTypes)
{
SizeTypeWith_XY<float> XYFloatIntegral{ 1.f, 2.f };
SizeTypeWith_XY<float> XYFloat{ 1.6f, 2.4f };
SizeTypeWith_cxcy<double> cxcyDoubleIntegral{ 3., 4. };
SizeTypeWith_cxcy<double> cxcyDouble{ 3.6, 4.4 };
SizeTypeWith_WidthHeight<double> WHDoubleIntegral{ 5., 6. };
SizeTypeWith_WidthHeight<double> WHDouble{ 5.6, 6.4 };
Log::Comment(L"0.) Ceiling");
{
{
til::size converted{ til::math::ceiling, XYFloatIntegral };
VERIFY_ARE_EQUAL((til::size{ 1, 2 }), converted);
}
{
til::size converted{ til::math::ceiling, XYFloat };
VERIFY_ARE_EQUAL((til::size{ 2, 3 }), converted);
}
{
til::size converted{ til::math::ceiling, cxcyDoubleIntegral };
VERIFY_ARE_EQUAL((til::size{ 3, 4 }), converted);
}
{
til::size converted{ til::math::ceiling, cxcyDouble };
VERIFY_ARE_EQUAL((til::size{ 4, 5 }), converted);
}
{
til::size converted{ til::math::ceiling, WHDoubleIntegral };
VERIFY_ARE_EQUAL((til::size{ 5, 6 }), converted);
}
{
til::size converted{ til::math::ceiling, WHDouble };
VERIFY_ARE_EQUAL((til::size{ 6, 7 }), converted);
}
}
Log::Comment(L"1.) Flooring");
{
{
til::size converted{ til::math::flooring, XYFloatIntegral };
VERIFY_ARE_EQUAL((til::size{ 1, 2 }), converted);
}
{
til::size converted{ til::math::flooring, XYFloat };
VERIFY_ARE_EQUAL((til::size{ 1, 2 }), converted);
}
{
til::size converted{ til::math::flooring, cxcyDoubleIntegral };
VERIFY_ARE_EQUAL((til::size{ 3, 4 }), converted);
}
{
til::size converted{ til::math::flooring, cxcyDouble };
VERIFY_ARE_EQUAL((til::size{ 3, 4 }), converted);
}
{
til::size converted{ til::math::flooring, WHDoubleIntegral };
VERIFY_ARE_EQUAL((til::size{ 5, 6 }), converted);
}
{
til::size converted{ til::math::flooring, WHDouble };
VERIFY_ARE_EQUAL((til::size{ 5, 6 }), converted);
}
}
Log::Comment(L"2.) Rounding");
{
{
til::size converted{ til::math::rounding, XYFloatIntegral };
VERIFY_ARE_EQUAL((til::size{ 1, 2 }), converted);
}
{
til::size converted{ til::math::rounding, XYFloat };
VERIFY_ARE_EQUAL((til::size{ 2, 2 }), converted);
}
{
til::size converted{ til::math::rounding, cxcyDoubleIntegral };
VERIFY_ARE_EQUAL((til::size{ 3, 4 }), converted);
}
{
til::size converted{ til::math::rounding, cxcyDouble };
VERIFY_ARE_EQUAL((til::size{ 4, 4 }), converted);
}
{
til::size converted{ til::math::rounding, WHDoubleIntegral };
VERIFY_ARE_EQUAL((til::size{ 5, 6 }), converted);
}
{
til::size converted{ til::math::rounding, WHDouble };
VERIFY_ARE_EQUAL((til::size{ 6, 6 }), converted);
}
}
Log::Comment(L"3.) Truncating");
{
{
til::size converted{ til::math::truncating, XYFloatIntegral };
VERIFY_ARE_EQUAL((til::size{ 1, 2 }), converted);
}
{
til::size converted{ til::math::truncating, XYFloat };
VERIFY_ARE_EQUAL((til::size{ 1, 2 }), converted);
}
{
til::size converted{ til::math::truncating, cxcyDoubleIntegral };
VERIFY_ARE_EQUAL((til::size{ 3, 4 }), converted);
}
{
til::size converted{ til::math::truncating, cxcyDouble };
VERIFY_ARE_EQUAL((til::size{ 3, 4 }), converted);
}
{
til::size converted{ til::math::truncating, WHDoubleIntegral };
VERIFY_ARE_EQUAL((til::size{ 5, 6 }), converted);
}
{
til::size converted{ til::math::truncating, WHDouble };
VERIFY_ARE_EQUAL((til::size{ 5, 6 }), converted);
}
}
}
};

View file

@ -18,6 +18,7 @@ SOURCES = \
ColorTests.cpp \
OperatorTests.cpp \
PointTests.cpp \
MathTests.cpp \
RectangleTests.cpp \
SizeTests.cpp \
SomeTests.cpp \

View file

@ -13,6 +13,7 @@
<ClCompile Include="BitmapTests.cpp" />
<ClCompile Include="OperatorTests.cpp" />
<ClCompile Include="PointTests.cpp" />
<ClCompile Include="MathTests.cpp" />
<ClCompile Include="RectangleTests.cpp" />
<ClCompile Include="SizeTests.cpp" />
<ClCompile Include="ColorTests.cpp" />