Testing solar calculator in Elm?

I have written a solar calculator using info provided by NOAA. There are spreadsheets (both Excel and LibreOffice) containing all the necessary functions needed in the calculations. The functions deliver mostly float-type results.
I read recently about TDD first time and was interested to try it, as a total newbie (in Elm and elm-test)
I wonder now, is there any reasonable way to do some fuzz elm-tests on the float-type functions?
It’s lots easier to write some unit tests comparing the calculated values to expected ones using Expect.atMost with the given tolerances.
The very first unit test, I wrote for Julian Day Number JDN (an Int function for the given date) and Julian Date JD (a Float function for the given date and time) as described in the Wikipedia article.

Any comments and suggestions are welcome.

Look at my github files:
Main.elm
Jdn3Test.elm
GregorJDN.elm

5 Likes

As the next step, I have made kind of unit tests on the calculator functions with elm repl, taking the expect values of the results from NOAA Solar Calculator.
Below is a short example. First are the required functions imported from the file Main.elm:
> import Main exposing (Model, getCent, getNoon, sunRise, sunSet, getDayLength, mnToHrMn, sunDeclination, solAzimuth)
Next is the input data set into model structure for the city center Helsinki:
> helsinki = Model β€œ2022” β€œ9” β€œ4” β€œ10” β€œ19” β€œ29” β€œ60.17” β€œ24.94” β€œ3”
{ day = β€œ4”, hour = β€œ10”, latitude = β€œ60.17”, longitude = β€œ24.94”, minute = β€œ19”, month = β€œ9”, second = β€œ29”, timezone = β€œ3”, year = β€œ2022” }
: Model

Calculations are performed for the expected noon time according to the NOAA calculator, 10:19:29 UTC which should result the solar azimuth 180.0Β°.
> helsinki.latitude
β€œ60.17” : String
> mnToHrMn <| 60*(getDayLength helsinki) – expect 13:55
β€œ13:54:21” : String

> mnToHrMn <| getNoon helsinki – expect 13:19:29
β€œ13:19:18” : String

> mnToHrMn <| sunRise helsinki – expect 06:21
β€œ06:22:08” : String

> mnToHrMn <| sunSet helsinki – expect 20:16
β€œ20:16:29” : String

> 90 - Main.solZenith helsinki – Altitude without refraction corr.
36.93729566691127 : Float

> Main.refractCorrectAltitude helsinki – expect 36.96Β°
36.958715879793985 : Float

> solAzimuth helsinki – expect 180.05Β°
180.0521205431728 : Float

> sunDeclination helsinki – expect 7.11Β°
7.107305165343985 : Float

Generally, it can be concluded, the elm version is about as accurate as NOAA calculator. However, NOAA gives the the noon time sligthly deviating from the right value for azimuth 180 degrees and my calculator repeats that for the same given time of course.

I have made the next example during the same session for the German City Hamburg.
The accuracy is here also pretty good. This time, I have adjusted the time of NOAA so that it results as near as possible the right azimuth 180 and now, both calculators give exactly the same values.

> hamburg = Model β€œ2022” β€œ9” β€œ4” β€œ11” β€œ19” β€œ9” β€œ53.57” β€œ9.98” β€œ2”
{ day = β€œ4”, hour = β€œ11”, latitude = β€œ53.57”, longitude = β€œ9.98”, minute = β€œ19”, month = β€œ9”, second = β€œ9”, timezone = β€œ2”, year = β€œ2022” }
: Model

hamburg.latitude
β€œ53.57” : String

mnToHrMn <| 60*(getDayLength hamburg) – expect 13:29
β€œ13:29:08” : String
mnToHrMn <| getNoon hamburg – expect 13:19:18
β€œ13:19:08” : String
mnToHrMn <| sunRise hamburg – expect 06:34
β€œ06:34:34” : String
mnToHrMn <| sunSet hamburg – expect 20:03
β€œ20:03:42” : String

90 - Main.solZenith hamburg – Altitude without refraction correction
43.522008630661276 : Float
Main.refractCorrectAltitude hamburg – Altitude with refr. corr., expect 43.54Β°
43.538979722952796 : Float
solAzimuth hamburg – expect 180.00Β°
180.00283296569495 : Float
sunDeclination hamburg – expect 7.09Β°
7.0920086610518585 : Float

As the next thing, I could calculate the Sun altitude, just at the calculated sunrise or sunset time, for any location. How ever, I leave that for you interested in the topic :slight_smile:

I’m not sure - do you want to test a bunch of hardcoded input-output pairs, or do you rather want to generate random inputs, run the tested function on those and assert something about the result? Both are possible

Thank you Janiczek for your reply.
I have a plan how to do it, something like the first one you mentioned, comparing value-pairs [(calculated in elm expression, respective values of picked up from NOAA functions in Excel)] for a given argument within the list of the given range.
NOAA Solar Calculator is based on the well known Meeus algorithm. However, translating some of the lengthy Excel functions to Elm is quit prone to errors, so it needs some testing as much as possible.

Then you can use this approach to test large numbers of input-output pairs:

toTest : (Int, Int) -> Test
toTest ((input, expectedOutput) as case) =
  Test.test (Debug.toString case) <|
    \() ->
      input
        |> Foo.doSomething
        |> Expect.equal expectedOutput

tests : Test
tests =
  [(123,999)
  ,(159,438)
  ,...
  ]
  |> List.map toTest
  |> Test.describe "Foo tests"
1 Like

Thank you Martin @Janiczek for your code!
It’s interesting and suitable for further developing ideas.
I wanted to modify it so that especially Float variables and functions could be tested. Here is the new example:
(Sorry, indentations are lost in the code below, I cannot fix it here but you can look at Ellie to see it better.)

All 8 tests are passed!

One question: The compiler claims in your code about
toTest ((input, expectedOutput) as case).
As I don’t know, why in fact, I changed the keyword case to testPoint.
Should there be something else instead?

module DecimalTests exposing (..)

import Expect exposing (Expectation)
import Test exposing (…)

degToRad = \x β†’ pi * x / 180.0

toTest : (Float, Float) β†’ Test
toTest ((input, expectedOutput) as testPoint) =
Test.test (Debug.toString testPoint) <|

() β†’
input
|> (-) expectedOutput |> abs
|> Expect.lessThan 1.00e-5

tests : Test
tests =
[(pi, 3.14159)
,(degToRad 180, pi)
,(degToRad 90, pi / 2)
,(degToRad 60, pi / 3)
,(degToRad 45, pi / 4)
,(cos (pi/3), 0.50)
,( sin (pi/6), 0.50)
,(1111 / 1000, 1.111)]
|> List.map toTest
|> Test.describe β€œTrigonometric Functions Tests”

Ah, good point about the case - I wrote the code on my phone and didn’t try to run it. case is a reserved word so indeed you’d need to change it to something like testPoint as you did :+1:

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.