joshwlewis / unitwise

Physical quantity and units of measure conversion and math for Ruby
unitwise.org
MIT License
281 stars 32 forks source link

Unitwise::Delta #73

Open fabiengagne opened 1 year ago

fabiengagne commented 1 year ago

How about having the ability to deal with delta temperatures, or delta speeds or delta whatever?

Like being able to specify a delta temperature of x degrees (3 C, 4 F or 7 K). This would allow to compute a new temperature by adding or subtracting a delta temperature to a temperature that is not necessarily in the same unit.

It would be useful when dealing with thresholds or safety margins, all while implementing user's unit preferences.

For example :

# current_temperature is a temperature
# safety_margin is a delta-temperature
# threshold_temperature is a temperature
if (current_temperature - safety_margin) > threshold_temperature
  raise_alarm
joshwlewis commented 1 year ago

Hey, not exactly sure what you are getting at. You should be able to add and subtract temperatures without issue. A delta temperature is still a temperature, a delta speed is still a speed.

I think these make sense to me, and maybe for your use case?

# (current_temperature - safety_margin) > threshold_temperature
(Unitwise(100, "°C") - Unitwise(3, "°C")) > Unitwise(95, "°C") # => true
(Unitwise(100, "°C") - Unitwise(3, "°C")) > Unitwise(98, "°C") # => false

Though I might be missing something?

joshwlewis commented 1 year ago

I think I see what you mean. These feel inconsistent:

(Unitwise(100, "°C") - Unitwise(3, "°C")) > Unitwise(98, "°C") # => false
(Unitwise(100, "°C") - Unitwise(3, "K")) > Unitwise(98, "°C") # => true

I think this is mostly a temperature issue, where both °C and °F both have a 0 value that is not actually zero temperature. Most (all?) the other SI and even most of the other units have a zero value that is actually zero (for instance 0 m/s means no velocity).

I don't see a super clear way to implement something like this, without adding more deviation from the UCUM spec. I think your best bet is to force all your temperature calculations to one unit prior to operating on them, unfortunately.

fabiengagne commented 1 year ago

I think you've just uncovered another issue that should be open as a distinct PR

(Unitwise(100, "°C") - Unitwise(3, "K"))
=> #<Unitwise::Measurement value=370.15 unit=°C>

Further,

((Unitwise(100, "°C") - Unitwise(3, "K"))).convert_to("°C")
 #<Unitwise::Measurement value=370.15 unit=°C>
((Unitwise(100, "°C") - Unitwise(3, "K"))).convert_to("K")
 #<Unitwise::Measurement value=643.3 unit=K>

I will post more detail about my original suggestion in a few minutes to better depict my "delta-something" use case.

fabiengagne commented 1 year ago

I will depicts with my actual use case.

Let me first reassure that I already have a workaround implemented in my app, however it's somehow cumbersome.

In user preference, the user selects its preferred temperature units. My app periodically reads an actual sensor temperature about a physical phenomena. My app periodically calculates a threshold temperature for a physical phenomena (some sort of temperature at which damage occurs). All parameters and readings are saved in base unit in the db; for temperature that's in Kelvin.

In each of its "site" configuration, user select the safety margin (delta temperature) he wants to be alerted on. This parameter, means if the sensor temperatures comes this close to the predicted temperature, please alert me. This delta-temperature parameters should be saved in base units too. Since we don't have "delta-kelvin", I save in "kelvin".

For example, if user inputs 3 (°F), it means to alert if the temperature of the sensor comes 3 °F to the predicted damage temperature.

There are two possible approaches.

Approach 1 save this safety_margin user parameter as a pseudo delta temperature in base unit This is the approach I chose.

delta = (Unitwise (3, current_user.pref.temp_unit) - Unitwise(0, current_user.pref.temp_unit) ).convert_to ("K")
site.safety_margin = delta

Then convert back-and-forth in the controller and form and partials, from base unit to the user preference, with the saved parameter being always in the base unit in the configuration. It's a lot of fiddling, but quite doable.

Then the app does its business as follows

Site.all.each do |site|
  site.raise_alarm if site.sensor_temperature > (site.predicted_damage_temperature + site.safety_margin)
end

Approach 2 : Save the user's current unit, along with the parameter value itself But this would require the support for delta-temperature. My business logic would become

Site.all.each do |site|
  safety_margin = Unitwise(site.safety_margin.value, site.safety_margin_unit)
  site.raise_alarm if site.sensor_temperature > (site.predicted_damage_temperature + safety_margin)
end

In the controller actions for saving

site.safety_margin_unit = "delta." + user.pref.temperature_unit
site.satety_margin_value = params[:safety_margin]

In the controller actions for displaying

safety_margin = Unitwise(site.safety_margin_value, site.safety_margin_unit).convert_to("delta."+current_user.pref.temperature_unit)

All in all, and while writing this, I realize that the code might not get all that much cleaner after all. We would get around the complications of zeroing the conversion on the zero of the unit, before converting to base unit. In all honestly, the two approaches I depicted above can't even convince myself of the net value added.

In short, if you find that it's a lot of work for not much value added, I would understand.