0

I'm new to Go and can't figure out how to simply read and average the values of an array of JSONs. I also want to round my result to 1 decimal point, but Go has no Round() function. Here's the data:

[
    {"millisUTC":"1496424000000","price":"7.6"},
    {"millisUTC":"1496423700000","price":"7.5"},
    {"millisUTC":"1496423400000","price":"9.1"},
    {"millisUTC":"1496423100000","price":"9.2"},
    {"millisUTC":"1496422800000","price":"10.0"}
]

I want to get the prices and average them, rounding to 1 decimal point. Yet it took me over 30 lines of code, when (as a Ruby developer) it would usually take me 3 lines. How do I simplify this? My code takes in 2 parameters, starttime and endtime, and calls an API: https://github.com/rayning0/griddy/blob/master/controllers/default.go

type arrayOfMaps []map[string]string

func getAvgPrice(starttime, endtime string) float64 {
    response, err := http.Get("https://hourlypricing.comed.com/api?type=5minutefeed&datestart=" + starttime + "&dateend=" + endtime)

   if err != nil {
        fmt.Println(err)
    }
    defer response.Body.Close()

    energyJSON, err := ioutil.ReadAll(response.Body)
    if err != nil {
        fmt.Println(err)
    }

    var energyPrices arrayOfMaps
    err = json.Unmarshal(energyJSON, &energyPrices)

    fmt.Println("Energy prices between", starttime, "and", endtime)
    fmt.Println(energyPrices)

    var sum float64
    var size int
    for _, p := range energyPrices {
        price, _ := strconv.ParseFloat(p["price"], 64)
        sum += price
        size++
    }
    avg := Truncate(sum / float64(size))
    fmt.Println("Average price:", avg)
    return avg
}

//Truncate a float to 1 level of precision
func Truncate(some float64) float64 {
    return float64(int(some*10)) / 10
}

Edited, thanks to excellent help from @icza!

This applies to my question: https://golang.org/pkg/encoding/json/#Decoder.Decode

See my revised solution, with detailed comments: https://github.com/rayning0/griddy/blob/master/controllers/default.go

Raymond Gan
  • 4,612
  • 3
  • 24
  • 19
  • 3
    "how to easily read and average the values of an array of JSONs" "I also want to round my result to 1 decimal point," --- these are *3 independent* tasks. Which one you have issues with? – zerkms Jun 03 '17 at 11:22
  • All 3. My code above does those 3 tasks, but it took over 30 lines. How do I simplify this? I'm a Ruby developer who could have probably done all this in 3 lines of code. This was painful. – Raymond Gan Jun 03 '17 at 11:29
  • Go is not as concise as some other languages, get used to it. – zerkms Jun 03 '17 at 12:00
  • Your comment doesn't help. Can you simplify my code? I'm sure my way is longer and clumsier than it should be. – Raymond Gan Jun 03 '17 at 12:38
  • I'm sure you cannot make it shorter without losing readability. Go chooses verbosity over expressiveness. – zerkms Jun 03 '17 at 12:40
  • 1
    Ask one question at a time. – Jonathan Hall Jun 03 '17 at 13:17

1 Answers1

2

Your code can be simplified in several points, and while you said "rounding" you did "truncating" in the end which is not the same.

One important thing: if an error is encountered, you should return early and not continue, as that will only be the source of additional errors or even runtime panics. See at the end.

Unmarshaling JSON

Easier would be to use json.Decoder, decoding right from the response body (which implements io.Reader).

Also note to simplify parsing floats given as string values in JSON, a better option would be to use json.Number.

Parsing can be as simple as this:

var prices []map[string]json.Number
if err := json.NewDecoder(response.Body).Decode(&prices); err != nil {
    fmt.Println(err)
    return
}

Calculating sum

Calculating sum can also be simplified: there is no need to keep track of size, as that is simply the length of the map:

sum := 0.0
for _, p := range prices {
    f, _ := p["price"].Float64()
    sum += f
}

Note that if you want to handle errors in a way to simply exclude it from the sum (and not return with an error), only then would you need to count valid numbers.

Rounding

Multiplying by 10 and then dividing by 10 is truncating and not rounding. For rounding, you should add 0.5 between those 2 operations. For details, see this answer: Golang Round to Nearest 0.05

So a correct rounding function that properly rounds both positive and negative numbers to arbitrary unit:

func Round(x, unit float64) float64 {
    if x > 0 {
        return float64(int64(x/unit+0.5)) * unit
    }
    return float64(int64(x/unit-0.5)) * unit
}

So the result:

avg := Round(sum/float64(len(prices)), 0.1)

The complete solution with error handling

Since your getAvgPrice() can fail at multiple points, you should definitely add an error return value too.

This is the complete solution with proper error handling:

func getAvgPrice(starttime, endtime string) (float64, error) {
    response, err := http.Get("https://hourlypricing.comed.com/api?type=5minutefeed&datestart=" + starttime + "&dateend=" + endtime)
    if err != nil {
        return 0, err
    }
    defer response.Body.Close()

    var prices []map[string]json.Number
    if err := json.NewDecoder(response.Body).Decode(&prices); err != nil {
        return 0, err
    }

    sum := 0.0
    for _, p := range prices {
        f, err := p["price"].Float64()
        if err != nil {
            return 0, err
        }
        sum += f
    }

    return Round(sum/float64(len(prices)), 0.1), nil
}
icza
  • 389,944
  • 63
  • 907
  • 827
  • Oh my God, you are the best! Thank you thank you thank you, sir! – Raymond Gan Jun 03 '17 at 14:38
  • @peterSO You're right, forgot about it. Fixed. Thanks. – icza Jun 03 '17 at 17:11
  • Unfortunately, your `Round()` function has a problem. When I try starttime = "201706031105" and endtime = "201706031200", I get Average Price = 2.9000000000000004, not 2.9. When I do starttime = "201706021300" and endtime = "201706031400", I get Average Price = 3.9000000000000004, not 3.9. – Raymond Gan Jun 03 '17 at 18:01
  • 1
    @RaymondGan The `Round()` function has no problems, this is because the decimal numbers `2.9` and `3.9` cannot be represented exactly in binary system with finite number of bits (`float64` has "only" 64 bits). This exact issue is mentioned and described in the linked answer: [Golang Round to Nearest 0.05](https://stackoverflow.com/questions/39544571/golang-round-to-nearest-0-05/39544897#39544897). Please read through the complete answer. Solution is simple: print the result like `fmt.Printf("%.1f\n", someFloatValue)`. – icza Jun 03 '17 at 18:15
  • What if I want to save it in the database as 2.9 and 3.9, float64 type? Yes, Printf can easily show a certain number of decimals, but I want the actual saved numbers to be the right format. – Raymond Gan Jun 03 '17 at 18:18
  • 1
    @RaymondGan The IEEE-754 standard is not capable of storing certain decimal numbers precisely. There's nothing you can do about it. If you want to store exact numbers, multiply them by 10 (as you rounded to 1 decimal digit) and store it as an `int`, or store the decimal number as a `string`. – icza Jun 03 '17 at 18:30
  • @RaymondGan That answer you linked and claim it's too much work: that answer shows like 4 or 5 different ways how to do it. Obviously you only need to pick 1 (out of that 5), which will be a "tiny" (acceptable) amount of work. – icza Jun 03 '17 at 18:32
  • 1
    @icza: You give a "rounding function for positive numbers". What about negative prices? "Negative Prices: With real-time hourly market prices, it is possible for the price of electricity to be negative for short periods of time." https://hourlypricing.comed.com/live-prices/ – peterSO Jun 03 '17 at 18:33
  • @peterSO The version that handles both positive and negative numbers is in my linked answer: [Golang Round to Nearest 0.05](https://stackoverflow.com/questions/39544571/golang-round-to-nearest-0-05/39544897#39544897). Maybe I should include that version. Doing it now. – icza Jun 03 '17 at 18:34
  • Ah, I see! Thank you for your answer: https://stackoverflow.com/questions/41159492/format-float-in-golang-html-template I did `{{printf "%.1f" $record.Avgprice}}` on https://github.com/rayning0/griddy/blob/master/views/prices.tpl and it correctly changed all my answers on the page to 1 decimal point. – Raymond Gan Jun 03 '17 at 18:35
  • Thank you @icza, for your excellent answers! This applies to my question: https://golang.org/pkg/encoding/json/#Decoder.Decode See my revised solution, with detailed comments: https://github.com/rayning0/griddy/blob/master/controllers/default.go – Raymond Gan Jun 03 '17 at 19:28