Skip to content

Instantly share code, notes, and snippets.

@greggirwin
Last active July 14, 2023 19:14
Show Gist options
  • Save greggirwin/38883ca5109175a60896d2f406ee49f6 to your computer and use it in GitHub Desktop.
Save greggirwin/38883ca5109175a60896d2f406ee49f6 to your computer and use it in GitHub Desktop.
Reactive BMR calculator in Red
Red [
Title: "BMR (Basal Metabolic Rate) Calculator"
Author: "Gregg Irwin"
File: %bmr-calc.red
Needs: View
Comment: {
An experiment in reactivity and data modeling.
TBD: Caloric calcs based on activity level.
References:
https://doc.red-lang.org/reactivity.html
https://en.wikipedia.org/wiki/Harris%E2%80%93Benedict_equation
https://en.wikipedia.org/wiki/Basal_metabolic_rate
}
]
;-------------------------------------------------------------------------------
; Generic functions
inch-to-cm: func [val][val * 2.54]
cm-to-inch: func [val][val / 2.54]
lb-to-kg: func [val][val / 2.20462]
kg-to-lb: func [val][val * 2.20462]
linear-interpolate: func [
src-min [number!]
src-max [number!]
dest-min [number!]
dest-max [number!]
value [number!]
][
add dest-min ((value - src-min) / (src-max - src-min) * (dest-max - dest-min))
]
;-------------------------------------------------------------------------------
; Data Ranges
; This may seem a bit confusing. It looks like these are dialected code,
; but they're not. They're just data blocks. The words 'from and 'to will
; be used to look up data, but the unit designations, in this case, are
; strictly informational (and optional).
; This was my first approach.
;height-range: [from 100 cm to 250 cm]
;weight-range: [from 45 kg to 160 kg]
;weight-range: [from 45 to 160 kg] ; works the same as the one above
;age-range: [from 5 to 125 years]
;
;map-range: func ["Map slider val to semantic range" range val][
; to integer! linear-interpolate 0% 100% range/from range/to val
;]
; Then I decided to structure it more during a refactoring pass.
data-ranges: [ ; Units are strictly informational
height [from 100 to 250 cm]
weight [from 45 to 160 kg]
age [from 5 to 125 years]
]
map-range: func [
"Map slider val to semantic range"
range [word!] "data-ranges key [height weight age]"
val [percent!]
/local rng
][
; With the extra level in the ranges data, the one-liner is a bit much.
; to integer! linear-interpolate 0% 100% data-ranges/:range/from data-ranges/:range/to val
; Making it 2 lines seems worth it in this case.
rng: data-ranges/:range
; If we did this in multiple places, it would make sense to break
; out the interpolation of 0-100% into a function. Not worth it now.
to integer! linear-interpolate 0% 100% rng/from rng/to val
]
;!! The big difference in the above choice is what the calls look like.
; In one case you use the actual reference (`height-range`) because
; you're passing the range block itself. In the other you use a word
; to select the range by name (`'height`). This matters because it
; affects the calling site. Of course, you could allow either type
; of value and look up by key if they pass a word, but that's really
; overkill for a small app like this.
;
; cm: map-range height-range val
; vs
; cm: map-range data-ranges/height val
; vs
; cm: map-range 'height val
;
; The latter hides more details from the caller, because it doesn't
; have to know anything about how ranges are defined. The second
; approach is what you might see in a standard OOP model. One thing
; to keep in mind, as you design, is how large your app/system is,
; and whether it needs to be composed with other parts.
;-------------------------------------------------------------------------------
; Data Functions
; This is another unusual approach, at a glance. You might expect
; these functions to convert the value to a simple, normalized result
; (e.g. normalize height to cm). Then you would convert that to other
; unit types and format it elsewhere. That matches the Single
; Responsibility Principle, or very granular cohesion. And I do like
; simple functions. But the simpler each function is, remember, the
; more of them you need, and the more combinations there are in how
; you connect them.
;
; This approach is more like knowledge-based programming, in that you
; provide input and get back something like symbolic results. It's
; not quite like the coupling of code and data in OOP, because you
; are just calling a function and getting back a result each time.
;
; An obvious argument against this model is that your functions are
; doing extra work, calculating results that may never be used, and
; allocating and reducing a block, rather than just returning a
; simple numeric result. Suspend judgement, if you can, until you've
; read the entire program to see how it all works together as a
; whole. You may still not like it, but Red gives you the ability
; to structure your solution however you want.
to-height: function [
"Convert a slider value to height data"
val [percent!]
][
cm: map-range 'height val
inches: to integer! cm-to-inch cm
reduce [
'cm cm
'in inches
'ft-in reduce ['ft to integer! inches / 12 'in mod inches 12]
'formed-imperial imp: rejoin [to integer! inches / 12 {'} mod inches 12 {"}]
'formed-metric met: rejoin [cm 'cm]
'formed rejoin [imp " / " met]
]
]
to-weight: function [
"Convert a slider value to weight data"
val [percent!]
][
kg: map-range 'weight val
lb: to integer! kg-to-lb kg
reduce [
'kg kg
'lb lb
'formed-imperial imp: rejoin [lb 'lb]
'formed-metric met: rejoin [kg 'kg]
'formed rejoin [imp " / " met]
]
]
; This func could just return the age in years, since there is no
; other way we want to represent it, as with the metric/imperial
; values. We *could* set our age range in days, and calc years and
; months from that if we wanted. Something to think about.
to-age: function [
"Convert a slider value to age data"
val [percent!]
][
; Not including months in this, though we could. `Map-range` truncates
; results to integers, so there's no decimal component to get the month
; part from without changing that.
yr: map-range 'age val
;mo: round 12 * mod yr 1
reduce [
'yr yr
;'yr-mo reduce ['yr round yr 'mo mo]
'formed rejoin [yr " years"]
]
]
;-------------------------------------------------------------------------------
; BMR Formulae
; The commented formulae are taken directly from Wikipedia. The Red code
; to implement them mimics their format for easy comparison.
; The same technique is used here as with data-ranges. The last element
; in the result is 'kcal/day, which is strictly informative, making the
; data more self-documenting.
bmr-calc-1918: func [
"The original Harris–Benedict equations published in 1918 and 1919"
height [block!]
weight [block!]
age [block!]
][
;Women BMR = 655.1 + ( 9.563 × weight in kg ) + ( 1.850 × height in cm ) – ( 4.676 × age in years )
;Men BMR = 66.5 + ( 13.75 × weight in kg ) + ( 5.003 × height in cm ) – ( 6.755 × age in years )
reduce [
'female to integer! (655.1 + (9.563 * weight/kg) + (1.850 * height/cm) - (4.676 * age/yr)) 'kcal/day
'male to integer! ( 66.5 + (13.75 * weight/kg) + (5.003 * height/cm) - (6.755 * age/yr)) 'kcal/day
]
]
bmr-calc-1984: func [
"The Harris–Benedict equations revised by Roza and Shizgal in 1984"
height [block!]
weight [block!]
age [block!]
][
;Women BMR = 447.593 + (9.247 × weight in kg) + (3.098 × height in cm) - (4.330 × age in years)
;Men BMR = 88.362 + (13.397 × weight in kg) + (4.799 × height in cm) - (5.677 × age in years)
reduce [
'female to integer! (447.593 + ( 9.247 * weight/kg) + (3.098 * height/cm) - (4.330 * age/yr)) 'kcal/day
'male to integer! ( 88.362 + (13.397 * weight/kg) + (4.799 * height/cm) - (5.677 * age/yr)) 'kcal/day
]
]
bmr-calc-1990: func [
"The Harris–Benedict equations revised by Mifflin and St Jeor in 1990"
height [block!]
weight [block!]
age [block!]
/local base-bmr
][
;Women BMR = (10 × weight in kg) + (6,25 × height in cm) - (5 × age in years) - 161
;Men BMR = (10 × weight in kg) + (6,25 × height in cm) - (5 × age in years) + 5
; The formula layout made it easy to see that there's a common
; sub-expression in each.
base-bmr: to integer! (10 * weight/kg) + (6.25 * height/cm) - (5 * age/yr)
reduce [
'female (base-bmr - 161) 'kcal/day
'male (base-bmr + 5) 'kcal/day
]
]
;-------------------------------------------------------------------------------
; Data Structures
; This is where things get more interesting. A little explanation on
; the background. I started the app so I could get more practical
; experience with Red's reactivity system. As is often the case with
; new things, I stumbled a bit. I knew I wanted a global data structure
; that would reactively update from the UI and, in turn, update other
; parts of the UI. e.g., you move a slider, that updates a value in the
; data, which in turn shows up as calculated output data in the UI.
;
; The problem I hit was that defining the reactive relations statically
; is *really* easy, but doesn't work for forward references. That is, if
; you have a field in an object that uses `is` to react to a slider in
; the UI, the UI has to be defined first. If you then have an output
; label that refers to the data in its `react` block, the data hasn't
; been defined yet.
;
; Red lets you define reactive relations dynamically, but I wanted to
; see if I could do it without that. Well, first I headed down that
; path, but it seemed overly complex for this. Maybe someone will look
; at my approach here and show how a dynamic reactivity version is
; better.
;
; To make this work, there is just one little non-reactive cheat in
; place, and one thing I might be able to eliminate with deep reactors
; later if I want. The cheat is that one reaction forces an update to
; a data value to trigger other reactions. This way the data doesn't
; have to refer to anything in the UI. Wait! That makes it a "feature".
;
; This is where reactions are sourced. When any of the height/weight/age
; values change, the bmr-* reactions trigger. In turn, other targets
; can react to those changes.
data: make reactor! [
; Prime the fields from the middle of our value ranges. Empirical
; choices for the defaults. They are magic numbers, duped in the
; UI code. For a larger app I would probably set up a defaults
; structure for both to reference.
height: to-height 50%
weight: to-weight 30%
age: to-age 38%
; Calculated results
; Originally I had these in an external reactor!, and had a reactive
; formula here (an `is` block) that updated it. Then I made the
; results reactor use `is` blocks so this object didn't know anything
; about it. Finally, I just included these fields here, so it's self-
; contained and the code is slightly simpler.
relate bmr-1918: [bmr-calc-1918 height weight age]
relate bmr-1984: [bmr-calc-1984 height weight age]
relate bmr-1990: [bmr-calc-1990 height weight age]
]
;-------------------------------------------------------------------------------
; UI
; You'll note that there is no option to select a target sex. I had one
; initially, started writing the funcs to calc BMR based on that, and
; included it in the data strucuture. Then I decided the sliders made
; the app more exploratory in nature and having all the data calculated
; all the time led to displaying it. All the BMR calc apps I found have
; a field for sex, and fields to enter values directly. We could do that
; as well, but there is value in different approaches.
view [
style label: text 50
style out-lbl: text 75 right
;!! Assigning to the `data` fields in the react blocks triggers calcs.
; Here you can see how the structured values in data are used to
; good effect. Rather than the UI forming and joining values,
; converting units, etc., it just asks for the formed value. Sort of
; how some OO systems have a standard `to-string` method for objects.
label "Height" sld-ht: slider 50% out-lbl react [
data/height: to-height sld-ht/data
face/text: data/height/formed
] return
label "Weight" sld-wt: slider 30% out-lbl react [
data/weight: to-weight sld-wt/data
face/text: data/weight/formed
] return
label "Age" sld-age: slider 38% out-lbl react [
data/age: to-age sld-age/data
face/text: data/age/formed
] return
pad 0x15
; Make the output look something like this:
; 1918 1984 1990
; Female kcal/day
; Male kcal/day
style cell: text 50 center
style hdr: cell bold
; This is where there's a bit of a trick. Red's reactive system is
; new, and may address this in the future, or someone may have a better
; solution with a different architecture. The trick is having the base
; data/bmr-19* refs in each react block. This is needed because the
; reference to the nested value (e.g. data/bmr-1918/female) does *not*
; work by itself. That is, we're telling Red to monitor a field *within*
; a reactive formula source, which doesn't work (currently).
; I'm sure we'll see more capabilities built on top of the base reactive
; system in the future. For example, the ability to define styles that
; contain reaction blocks, and a way to reference dynamic sources. In
; the meantime, if you have a lot of faces, you can also generate your
; View layout specs dynamically, which is often a good solution. Let the
; data drive your GUI.
label bold "Formula" hdr "1918" hdr "1984" hdr "1990" return
label "Female"
cell react [data/bmr-1918 face/text: form data/bmr-1918/female]
cell react [data/bmr-1984 face/text: form data/bmr-1984/female]
cell react [data/bmr-1990 face/text: form data/bmr-1990/female]
label "kcal/day"
return
label "Male"
cell react [data/bmr-1918 face/text: form data/bmr-1918/male]
cell react [data/bmr-1984 face/text: form data/bmr-1984/male]
cell react [data/bmr-1990 face/text: form data/bmr-1990/male]
label "kcal/day"
return
]
;-------------------------------------------------------------------------------
; Conclusion
; Red's reactive system is going to be a lot of fun to experiment with. I'm
; anxious to try new things with it, like the following-balls demo. It's
; impressive how small the implementation is, for the power it gives us.
; See: https://github.com/red/red/blob/master/environment/reactivity.red
;
; It gives us some new tools for thinking, though the concepts have been
; around for a long time.
;
; Happy Reducing!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment