Arithmetic operations with NamedTuples

I often use Julia to calculate the nutrition of my meals. I know there are calculators for this online, but I prefer doing this myself in Julia. I normally do this by first defining NamedTuples with the nutritional information per 100g of each ingredient of the meal, then creating a NamedTuple with the quantity (in units of 100g) of each ingredient and then multiplying the elements of these tuples together and adding them up for each bit of nutritional information I want to calculate. For instance, here’s a program I created to calculate the nutritional information of a vegan chocolate banana bread I considered making:

olive_oil = (cost=43/40, energy=3390, protein=0, fat=91.0, saturated_fat=13, carbohydrate=0, sugars=0, dietary_fibre=0, sodium=0)
wholemeal_spelt_flour = (cost=7.5/7.5, energy=1470, protein=13.5, fat=3.1, saturated_fat=0.5, carbohydrate=62.1, sugars=0.8, dietary_fibre=8.1, sodium=13)
lakanto_monkfruit_sweetener = (cost=16/5, energy=115, protein=0, fat=0, saturated_fat = 0, carbohydrate = 0, sugars = 0, dietary_fibre = 0, sodium=0)
banana = (cost=0.72/0.98, energy=359, protein=1.4, fat=0.2, saturated_fat=0, dietary_fibre=2.2, carbohydrate=19.6, sugars=12.8, sodium=0)
vanilla_soy_milk = (cost=3/10, energy=247, protein=3.2, fat=1.5, saturated_fat=0.2, carbohydrate=8, sugars=4.9, dietary_fibre=0.1, sodium=45)
baking_powder = (cost=5.56/3, energy=0, protein=0, fat=0, saturated_fat=0, carbohydrate=0, sugars=0, dietary_fibre=0.1, sodium=0)
cocoa_powder = (cost=4/3.75, energy=1500, protein=17.7, fat=22.3, saturated_fat=14.1, carbohydrate=9.4, sugars=0.7, dietary_fibre=27, sodium=18)
cinnamon = (cost=1.8/0.32, energy=1210, protein=11.1, fat=9, saturated_fat=4.33, carbohydrate=22, sugars=5.2, dietary_fibre=39.7, sodium=59)

quantities = (olive_oil = 0.73/8, wholemeal_spelt_flour = 3.1/8, lakanto_monkfruit_sweetener = 1.6/8, banana=1.96*3/8, vanilla_soy_milk = 1.29/8, baking_powder = 0.1/8, cocoa_powder = 0.3/8, cinnamon=0.05/8)

cost = olive_oil.cost * quantities.olive_oil + wholemeal_spelt_flour.cost * quantities.wholemeal_spelt_flour + lakanto_monkfruit_sweetener.cost * quantities.lakanto_monkfruit_sweetener + banana.cost * quantities.banana + vanilla_soy_milk.cost * quantities.vanilla_soy_milk + baking_powder.cost * quantities.baking_powder + cocoa_powder.cost * quantities.cocoa_powder + cinnamon.cost * quantities.cinnamon

energy = olive_oil.energy * quantities.olive_oil + wholemeal_spelt_flour.energy * quantities.wholemeal_spelt_flour + lakanto_monkfruit_sweetener.energy * quantities.lakanto_monkfruit_sweetener + banana.energy * quantities.banana + vanilla_soy_milk.energy * quantities.vanilla_soy_milk + baking_powder.energy * quantities.baking_powder + cocoa_powder.energy * quantities.cocoa_powder + cinnamon.energy * quantities.cinnamon

protein = olive_oil.protein * quantities.olive_oil + wholemeal_spelt_flour.protein * quantities.wholemeal_spelt_flour + lakanto_monkfruit_sweetener.protein * quantities.lakanto_monkfruit_sweetener + banana.protein * quantities.banana + vanilla_soy_milk.protein * quantities.vanilla_soy_milk + baking_powder.protein * quantities.baking_powder + cocoa_powder.protein * quantities.cocoa_powder + cinnamon.protein * quantities.cinnamon

fat = olive_oil.fat * quantities.olive_oil + wholemeal_spelt_flour.fat * quantities.wholemeal_spelt_flour + lakanto_monkfruit_sweetener.fat * quantities.lakanto_monkfruit_sweetener + banana.fat * quantities.banana + vanilla_soy_milk.fat * quantities.vanilla_soy_milk + baking_powder.fat * quantities.baking_powder + cocoa_powder.fat * quantities.cocoa_powder + cinnamon.fat * quantities.cinnamon

saturated_fat = olive_oil.saturated_fat * quantities.olive_oil + wholemeal_spelt_flour.saturated_fat * quantities.wholemeal_spelt_flour + lakanto_monkfruit_sweetener.saturated_fat * quantities.lakanto_monkfruit_sweetener + banana.saturated_fat * quantities.banana + vanilla_soy_milk.saturated_fat * quantities.vanilla_soy_milk + baking_powder.saturated_fat * quantities.baking_powder + cocoa_powder.saturated_fat * quantities.cocoa_powder + cinnamon.saturated_fat * quantities.cinnamon

carbohydrate = olive_oil.carbohydrate * quantities.olive_oil + wholemeal_spelt_flour.carbohydrate * quantities.wholemeal_spelt_flour + lakanto_monkfruit_sweetener.carbohydrate * quantities.lakanto_monkfruit_sweetener + banana.carbohydrate * quantities.banana + vanilla_soy_milk.carbohydrate * quantities.vanilla_soy_milk + baking_powder.carbohydrate * quantities.baking_powder + cocoa_powder.carbohydrate * quantities.cocoa_powder + cinnamon.carbohydrate * quantities.cinnamon

sugars = olive_oil.sugars * quantities.olive_oil + wholemeal_spelt_flour.sugars * quantities.wholemeal_spelt_flour + lakanto_monkfruit_sweetener.sugars * quantities.lakanto_monkfruit_sweetener + banana.sugars * quantities.banana + vanilla_soy_milk.sugars * quantities.vanilla_soy_milk + baking_powder.sugars * quantities.baking_powder + cocoa_powder.sugars * quantities.cocoa_powder + cinnamon.sugars * quantities.cinnamon

dietary_fibre = olive_oil.dietary_fibre * quantities.olive_oil + wholemeal_spelt_flour.dietary_fibre * quantities.wholemeal_spelt_flour + lakanto_monkfruit_sweetener.dietary_fibre * quantities.lakanto_monkfruit_sweetener + banana.dietary_fibre * quantities.banana + vanilla_soy_milk.dietary_fibre * quantities.vanilla_soy_milk + baking_powder.dietary_fibre * quantities.baking_powder + cocoa_powder.dietary_fibre * quantities.cocoa_powder + cinnamon.dietary_fibre * quantities.cinnamon

sodium = olive_oil.sodium * quantities.olive_oil + wholemeal_spelt_flour.sodium * quantities.wholemeal_spelt_flour + lakanto_monkfruit_sweetener.sodium * quantities.lakanto_monkfruit_sweetener + banana.sodium * quantities.banana + vanilla_soy_milk.sodium * quantities.vanilla_soy_milk + baking_powder.sodium * quantities.baking_powder + cocoa_powder.sodium * quantities.cocoa_powder + cinnamon.sodium * quantities.cinnamon

I am wondering whether there may be a more efficient way. Like I would prefer to be able to calculate all the nutritional information in one line with olive_oil * quantities.olive_oil + wholemeal_spelt_flour * quantities.wholemeal_spelt_flour + lakanto_monkfruit_sweetener * quantities.lakanto_monkfruit_sweetener + ..., but this isn’t possible it seems as I get errors that such an operation is not permitted. It seems that arithmetic operations are not defined on NamedTuples.

I think the best way is to defined a struct and create a method for addition and multiplication.

Also if the names are always in the same order you can convert to a tuple, broadcast operation, then convert back to the original named tuple.

Item = typeof(olive_oil)
Item(3.5 .* Tuple(olive_oil) .+ 0.5 .* Tuple(banana))
1 Like

The abstract type FieldVector from StaticArrays.jl was designed for this. Subtyping it adds array methods to your struct, such as addition and subtraction, scaling, matrix multiplication, and so on. Combine it with Base.@kwdef for the keyword constructor and you’re good to go:

using StaticArrays

Base.@kwdef struct NutritionFacts <: FieldVector{9,Float64}
	cost::Float64
	energy::Float64
	protein::Float64
	fat::Float64
	saturated_fat::Float64
	carbohydrate::Float64
	sugars::Float64
	dietary_fibre::Float64
	sodium::Float64
end
julia> olive_oil = NutritionFacts(
           cost=43/40,
           energy=3390,
           protein=0,
           fat=91.0,
           saturated_fat=13,
           carbohydrate=0,
           sugars=0,
           dietary_fibre=0,
           sodium=0,
       );

julia> wholemeal_spelt_flour = NutritionFacts(
           cost=7.5/7.5,
           energy=1470,
           protein=13.5,
           fat=3.1,
           saturated_fat=0.5,
           carbohydrate=62.1,
           sugars=0.8,
           dietary_fibre=8.1,
           sodium=13,
       );

julia> 5 * olive_oil + 27 * wholemeal_spelt_flour
9-element NutritionFacts with indices SOneTo(9):
    32.375
 56640.0
   364.5
   538.7
    78.5
  1676.7
    21.6
   218.7
   351.0
5 Likes

LabelledArrays.jl may be useful for this

1 Like

Is there a simple way to display a NutritionFacts struct with its fieldnames or would one have to write ones own display function?

I agree the display is unfortunate. I think it’s just a consequence of subtyping AbstractVector. Can’t find a workaround in the documentation. Seems like LabelledArrays does better in this respect:

julia> using LabelledArrays

julia> SLVector(foo=3.0, bar=5.0)
2-element SLArray{Tuple{2}, Float64, 1, 2, (:foo, :bar)} with indices SOneTo(2):
 :foo => 3.0
 :bar => 5.0
1 Like
1 Like

Alternatively, here’s a clean way to convert between FieldVector (good for computations) and NamedTuple (good for display) – the getproperties() function:

julia> using StaticArrays, ConstructionBase

julia> Base.@kwdef struct NutritionFacts <: FieldVector{9,Float64} ...

julia> olive_oil = NutritionFacts(...);

julia> olive_oil |> getproperties
(cost = 1.075, energy = 3390.0, protein = 0.0, fat = 91.0, saturated_fat = 13.0, carbohydrate = 0.0, sugars = 0.0, dietary_fibre = 0.0, sodium = 0.0)
4 Likes

Without using the ConstructionBase package, in this case:

getprop(x) = NamedTuple{propertynames(x)}(x)

olive_oil |> getprop

(cost = 1.075, energy = 3390.0, protein = 0.0, fat = 91.0, saturated_fat = 13.0, carbohydrate = 0.0, sugars = 0.0, dietary_fibre = 0.0, sodium = 0.0)
And an alternative using pairs:
getprop2(x) = propertynames(x) .=> x

olive_oil |> getprop2

9-element SVector{9, Pair{Symbol, Float64}} with indices SOneTo(9):
          :cost => 1.075
        :energy => 3390.0
       :protein => 0.0
           :fat => 91.0
 :saturated_fat => 13.0
  :carbohydrate => 0.0
        :sugars => 0.0
 :dietary_fibre => 0.0
        :sodium => 0.0
2 Likes