InfiniTime icon indicating copy to clipboard operation
InfiniTime copied to clipboard

weather: Fix incorrect rounding for negative temperatures

Open FintasticMan opened this issue 1 year ago • 9 comments

Thanks @tokiwee for reporting this issue! Closes #2185 (if GitHub un-deletes it).

FintasticMan avatar Dec 09 '24 23:12 FintasticMan

Build size and comparison to main:

Section Size Difference
text 380116B 32B
data 944B 0B
bss 22544B 0B

Run in InfiniEmu

github-actions[bot] avatar Dec 09 '24 23:12 github-actions[bot]

Would it be worth extracting out rounding integer division to some shared code? It's somewhat annoying that this isn't part of the C++ stdlib really :(

Maybe an implementation similar to this? https://github.com/lucianpls/Rounding-Integer-Division I haven't tried it though

mark9064 avatar Dec 10 '24 00:12 mark9064

What do you think of this? I've made a RoundedDiv function in the Utility namespace, that uses C++20 concepts so that we can have keep the code type-agnostic, while making sure that it only works for expected values.

Didn't you also use a rounded division somewhere in the AoD code? Would this solution also work there?

FintasticMan avatar Dec 10 '24 02:12 FintasticMan

Wow that looks great. I didn't know that concepts allow this, I should read up on it. Yes I think this would work for AOD, I guess that would go in a separate PR though?

I want to double check correctness properly before approving, but otherwise LGTM

mark9064 avatar Dec 11 '24 00:12 mark9064

Oh yeah of course. Integer promotion rules are really stupid. This conversion to unsigned also only happens when the values are ints or smaller, because that makes sense. Let me think of a good way to solve this then...

FintasticMan avatar Dec 22 '24 15:12 FintasticMan

because that makes sense

of course... 😅 I love the C++ spec too

mark9064 avatar Dec 22 '24 15:12 mark9064

I was curious about this piece of c++ code so I tried to check how it works, but

#include <cstdint>
#include <concepts>
#include <iostream>

    /*
    return dividend / divisor rounded correctly
    */
    static constexpr auto RoundedDiv(std::integral auto dividend, std::unsigned_integral auto divisor) -> decltype(dividend / divisor) {
      return (dividend + (dividend >= 0 ? divisor : -divisor) / 2) / divisor;
    }

int main () {
    std::cout << RoundedDiv((int16_t)250, 100u) << std::endl;
    std::cout << RoundedDiv((int16_t)150, 100u) << std::endl;
    std::cout << RoundedDiv((int16_t)50, 100u) << std::endl;
    std::cout << RoundedDiv((int16_t)20, 100u) << std::endl;
    std::cout << RoundedDiv((int16_t)-50, 100u) << std::endl;
    std::cout << RoundedDiv((int16_t)-150, 100u) << std::endl;
    std::cout << RoundedDiv((int16_t)-250, 100u) << std::endl;
    return 0;
}

It turned out it doesn't worked as I expected:

3
2
1
0
21474835
21474834
21474833

It looks like, you cannot use unsigned (i.e. 100u) as divisor, because -divisor will overflow and you will get incorrect number.

My knowledge of modern c++ is limited. It started to work when I have replaced std::unsigned_integral with std::integral in this line:

static constexpr auto RoundedDiv(std::integral auto dividend, std::integral auto divisor) -> decltype(dividend / divisor) {

and used 100 instead of 100u in those two lines

return Utility::RoundedDiv(PreciseFahrenheit(), 100);
return Utility::RoundedDiv(PreciseCelsius(), 100);

jmlich avatar Oct 21 '25 08:10 jmlich

@vkareh thanks for taking a look at this! The whole expression gets "promoted" to an unsigned integer if any of the terms are unsigned (and an int or smaller), which causes overflow for negative numbers. To solve this I've implemented the workaround you described, which is a bit less expressive in its typing, but at least works haha :laughing:.

FintasticMan avatar Nov 04 '25 09:11 FintasticMan

How about these? https://github.com/lucianpls/rounding_integer_division

(LOL, just realised I proposed this right at the start and then forgot about it, sorry for the double post)

// Round half toward zero integer division
// If T signed, divisor cannot be std::numeric_limits<T>::min()
// Adapted from https://github.com/lucianpls/rounding_integer_division
// Under the MIT license
template<std::integral T> 
T RoundedDiv(T dividend, T divisor) {
    bool neg = divisor < 0;
    if (neg) {
        // overflows if divisor is minimum value for T
        divisor = -divisor;
    }

    T m = dividend % divisor;
    T h = divisor / 2;
    T res = dividend / divisor + (!(dividend < 0) & (m > h)) - ((dividend < 0) & ((m + h) < 0));

    if (neg) {
        res = -res;
    }
    return res;
}

Should work for everything other than dividing with the negative type minimum, which would be a totally fine edge case IMO

mark9064 avatar Dec 07 '25 01:12 mark9064

This implementation looks good and fixes the issue mentioned by @jmlich ! And it comes with documentation for free :+1:

JF002 avatar Dec 13 '25 21:12 JF002