Celsius to Fahrenheit revisited - integer averaging considered harmful

Just to share some insights I got this morning about converting Celsius to Fahrenheit. As this conversion is used in many applications it might be worth just that extra attention (especially as it stands model for any measurements).

The formula for converting Celsius to Fahrenheit found in the books always state : F = C * 9/5 + 32
This formula translates easily to:

double Celcius2Fahrenheit(double celsius)
{
	return celsius * 9 / 5 + 32;
}

To speed things up we can remove the division (compiler might do this too I know)

double Celcius2Fahrenheit(double celsius)
{
	return 1.8 * celsius + 32;
}

If we need more speed we can move to the integer domain - many sensors offer only whole degrees

int Celcius2Fahrenheit(int celcius)
{
  return celsius * 9 /5 + 32;
}

Problem with this function is that it has a (truncating) error for 4 out of 5 values. This error is max 0.8F and on average 0.4F.

We can decrease this error by adding rounding to the formula. (sneaked in a factor 2 to keep whole numbers)

int Celcius2Fahrenheit(int celcius)
{
  return (celsius * 18 + 5)/10 + 32;
}

Still 4 out of 5 values have rounding errors but the error has decreased with almost a factor 2. Way better!
The absolute error is now max 0.4F and on average 0.24F. The cost is an extra addition.

Although we cannot improve on the precision, the extra addition introduced can be removed easily.

int Celcius2Fahrenheit(int celcius)
{
  return (celsius * 18 + 325)/10;  // note max celsius == 1820
}

And some people say the optimization of this relative simple conversion stops here. They are of course right and may skip the rest.


(is there anyone still left?)
When modelling the conversion function in Excel (undervalued tool for code analysis) it occurred to me that the integer versions of the function jumped with steps of 2F most of the time. This is because of the factor 9/5 which is almost 2. Then it occurred to me that when the temperature alternated between 20 and 21 C it caused jumps between 68 and 70F which indicates that 69F would be a better temperature in F.

The obvious solution is to let the "display" function take the average of the previous display value and the current reading. However that would lead to the following error:
assume the following data streams:
Celsius: ... 20 20 20 21 21 21 21
F = (F + C2F(t))/2;
Display: ... 68 68 68 69 69 69 !!! it never gets to 70 due to the integer truncating in average...oops!

OK we can try to fix this by using (F + C2F(t) + 1)/2;
Celsius: ... 20 20 20 21 21 21 21 20 20 20
F = (F + C2F(t) + 1)/2;
Display: ... 68 68 68 69 70 70 69 69 69 !!! 70 goes well now but it now it never gets back to 68 due to the +1 ... oops!

... that's not easy as expected....
As averaging after does not work we might consider doing averaging the Celsius part. But that won't work as the difference is already minimal (in integer domain).

So we must do the averaging IN the conversion function! This lead to the following code:

int Celcius2Fahrenheit(int prevCelsius, int celcius)
{
  return ((prevCelsius + celcius) * 9 + 325)/10;
}

Celsius : ... 20 20 20 21 21 21 21 20 20 20 21 20 21 20
Fahrenheit: ... 68 68 69 70 70 70 69 68 68 69 69 69 69 !!!

It now looks like the conversion function remembers where it came from and uses that to calculate a better Fahrenheit temperature.
Of course one can adjust other weights to the two temperatures to reflect e.g. distance in time.

Conclusion: By keeping the previous Celsius value we can calculate a better Fahrenheit value.


update: first call (in setup) should be of course with twice the same initial Celsius value.

So can that be internalised to make it transparent to the caller? eg

int Celcius2Fahrenheit(int celcius)
{
	static int prevCelsius = -50;  // some value that will never occur
	
	if (prevCelsius == -50) prevCelsius = celcius;
	prevCelsius = ((prevCelsius + celcius) * 9 + 325)/10;
	return prevCelsius;
	
}

Rob

That was exactly what I wrote in my original text before posting (OK I used -300 iso -50 as -50 can occur - eg DS18B20 ) and the answer is: it depends

Yes, if you have only one temp sensor you can do it, the programmer does not need to keep track of prev value any more.

No, given the case you have 2 or more temp sensors - let us call them indoor and outdoor - and you want to convert the Celsius values alternately... Think you get some interesting but incorrect values most of the time :wink: Based on this scenario I decided to remove this internalization version from my post as in its current form it is a "recipe for bugsoup".

Still internalization can be used if encapsulated in a sensor class, which keeps track of the prev value per sensor.

appendum: integer division replaced by shift to optimize speed.

int Celcius2Fahrenheit(int celcius)

  return celsius * 115/64 + 32;  // max celsius is about 280    Note: 115/64 = 1.796875  ~~ 0.3% error in theory in practice it will round downwards so it will be worse

  return celsius * 29/16 + 32;  // max celsius is about 1125   Note: 29/16 = 1.8125  ~~ 1.3% error too high in theory but as integer math rounds downwards so it compensates 

  return celsius * 57/32 + 32;

formula 1: 100C = 211F
formula 2: 100C = 213F
formula 3: 100C = 210F

TODO: test in Excel ...

This is an old topic but since it is so complete I did not start a new thread.

I just want to add a couple points. First, because integer division truncates towards zero (floor for positive integers, ceiling for negative integers), you need to reverse the rounding element for negative numbers.

So to support negative numbers, this:

return (celsius * 18 + 5)/10 + 32;

should be

if (celsius < 0) return (celsius * 18 - 5)/10 + 32; else return (celsius * 18 + 5)/10 + 32;

Second, the averaging during conversion is very clever. I like it if your ADC does not have resolution better than 1 degree celsius. Otherwise, if it does have better resolution, I recommend keeping the temperature in tenths degrees celsius and converting from that. Then you don't need the averaging in the conversion. So the above would become, where tdc is tenths degrees celsius:

if (tdc < 0) return (tdc * 18 - 50)/100 + 32; else return (tdc * 18 + 50)/100 + 32;

Roger