6
\$\begingroup\$

Problem:

Classify the user (such as infant, child, teenager, adult) based on user's age.

Solution

Please run the solution here.

The code snippet written below works well as expected. I want to know if there's a better way to do or refine this.

package main

import (
    "errors"
    "fmt"
)

func age(urAge int) (int, string, error) {
    if urAge > 120 || urAge < 0 {
        return -1, "", errors.New("Invalid age")
    } else if urAge > 0 && urAge <= 2 {
        return urAge, "Infant", nil
    } else if urAge > 2 && urAge <= 12 {
        return urAge, "Child", nil
    } else if urAge > 12 && urAge <= 19 {
        return urAge, "Teenager", nil
    } else {
        return urAge, "Adult", nil
    }
}

func main() {
    var input int
    fmt.Println("Enter your age: ")
    fmt.Scanf("%d", &input)

    if a, b, e := age(input); e != nil {
        fmt.Println("Error: ", e)
    } else {
        fmt.Println("Age: ", a)
        fmt.Println("Classification: ", b)
    }
}
\$\endgroup\$

migrated from stackoverflow.com Aug 12 '15 at 12:34

This question came from our site for professional and enthusiast programmers.

16
\$\begingroup\$

Note that your code classifies age = 0 as "Adult" which you probably want to be "Infant". (This is because age = 0 is not included in any of your specified ranges and so the last else branch will be executed.)

Two completely different solutions will be presented:

  1. The clearest and most obvious using switch.
  2. An elegant, very compact and faster way using binary search.

1) Using switch

A much clearer way would be to use switch.

If you write cases in increasing age order, you don't need to explicitly state both boundaries of a classification because cases are evaluated in order and the first one that matches will be executed:

func age(urAge int) (int, string, error) {
    switch {
    case urAge < 0 || urAge > 120:
        return -1, "", errors.New("Invalid age")
    case urAge <= 2:
        return urAge, "Infant", nil
    case urAge <= 12:
        return urAge, "Child", nil
    case urAge <= 19:
        return urAge, "Teenager", nil
    default:
        return urAge, "Adult", nil
    }
}

Also I find it redundant to return 3 values: age, classification and an error. Since the returned age is identical to the input if no error is returned, it is unnecessary. Callers of your function will know not to use the age if an error is returned, so I would further simplify it like this:

func age(urAge int) (string, error) {
    switch {
    case urAge < 0 || urAge > 120:
        return "", errors.New("Invalid age")
    case urAge <= 2:
        return "Infant", nil
    case urAge <= 12:
        return "Child", nil
    case urAge <= 19:
        return "Teenager", nil
    default:
        return "Adult", nil
    }
}

And then of course usage slightly changes (slightly simplifies):

if b, e := age(input); e != nil {
    fmt.Println("Error: ", e)
} else {
    fmt.Println("Age: ", input)
    fmt.Println("Classification: ", b)
}

Alternative error reporting

Although in general it is a good pattern to return a value and an error if a function can fail, in your case it doesn't really serve much purpose. It can only be "Invalid age". Also the empty string "" does not have a meaning as a classification, so you may want to exploit the empty string "" value to signal the error, and then you only need to return one value. If you choose to do this, the minimum is to include this in the documentation of the function like this:

// age returns the age classification.
// Returns empty string "" if the age is invalid.
func age(urAge int) string {
    switch {
    case urAge < 0 || urAge > 120:
        return "" // Empty string means "Invalid age"
    case urAge <= 2:
        return "Infant"
    case urAge <= 12:
        return "Child"
    case urAge <= 19:
        return "Teenager"
    default:
        return "Adult"
    }
}

And using it:

if c := age(input); c == "" {
    fmt.Println("Error: Invalid age")
} else {
    fmt.Println("Age: ", input)
    fmt.Println("Classification: ", c)
}

2) Using binary search

There is another elegant way. This is also faster if there are many cases. This solution is possible because age ranges of different classifications are disjunct (that is a concrete age can only belong to one classification). It also makes our life easier that age ranges of classifications are contiguous (we would need more ranges to model otherwise, but it wouldn't be a show-stopper).

The idea is that we list the age boundaries between classifications in an int slice in ascending order, and we can perform a binary search in this sorted slice. We can just model ranges with their maximum value since the sequence of ranges is without any holes. This will give us the index of the classification. We can store the classification names in a separate slice, and return the classification name at the index we just found as the result of binary search.

Binary search in a sorted slice is implemented in the standard library, we can just use that: sort.SearchInts().

Note that we will put 2 empty string names into the name slice (to the first and last place) which will be for the invalid ages (<0 and >120).

Now let's see the implementation, it is rather short:

var ages = []int{-1, 2, 12, 19, 120}
var classes = []string{"", "Infant", "Child", "Teenager", "Adult", ""}

// age returns the age classification.
// Returns empty string "" if the age is invalid.
func age(urAge int) string {
    return classes[sort.SearchInts(ages, urAge)]
}

That's all! It's a one-line function!

Testing it:

data := []int{-100, -1, 0, 2, 3, 10, 12, 13, 19, 20, 120, 121}
for _, d := range data {
    if c := age(d); c == "" {
        fmt.Printf("Age: %4d, Classification: Invalid age\n", d)
    } else {
        fmt.Printf("Age: %4d, Classification: %s\n", d, age(d))
    }
}

Output (try it on the Go Playground):

Age: -100, Classification: Invalid age
Age:   -1, Classification: Invalid age
Age:    0, Classification: Infant
Age:    2, Classification: Infant
Age:    3, Classification: Child
Age:   10, Classification: Child
Age:   12, Classification: Child
Age:   13, Classification: Teenager
Age:   19, Classification: Teenager
Age:   20, Classification: Adult
Age:  120, Classification: Adult
Age:  121, Classification: Invalid age
\$\endgroup\$
  • 1
    \$\begingroup\$ Implemented your suggestions - brevity at its best - working fine. Thanks. \$\endgroup\$ – Basant Aug 12 '15 at 6:50
  • \$\begingroup\$ @Basant I posted another, one-liner solution. \$\endgroup\$ – icza Aug 12 '15 at 11:55
  • 1
    \$\begingroup\$ Awesome, a completely different approach using binary search! \$\endgroup\$ – Basant Aug 13 '15 at 2:15
  • \$\begingroup\$ Also, I just noticed the bug you've pointed out; age = 0 getting classified as adult. \$\endgroup\$ – Basant Aug 13 '15 at 4:52
  • \$\begingroup\$ I object the point of implicit errors: It breaks a well known idiom. \$\endgroup\$ – Markus W Mahlberg Nov 14 '15 at 15:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.