4
\$\begingroup\$

I implemented the freq(1) program from Plan 9 in go.

/* print histogram of character frequencies */
package main

import (
    "bufio"
    "flag"
    "fmt"
    "io"
    "log"
    "os"
    "sort"
    "unicode"
)

var dflag = flag.Bool("d", false, "print decimal value")
var xflag = flag.Bool("x", false, "print hex value")
var oflag = flag.Bool("o", false, "print octal value")
var cflag = flag.Bool("c", false, "print unicode character if printable")
var rflag = flag.Bool("r", false, "count UTF8 sequences (runes)")

func main() {
    var freq func(*os.File, map[int32]int) = freqBytes
    counts := make(map[int32]int)
    var codes []int32

    flag.Parse()
    if (!*dflag && !*oflag && !*xflag && !*cflag) {
        *dflag = true
        *oflag = true
        *xflag = true
        *cflag = true
    }
    if (*rflag) {
        freq = freqRunes
    }
    if (len(flag.Args()) == 0) {
        freq(os.Stdin, counts)
    } else {
        for _, arg := range flag.Args() {
            if arg == "-" {
                freq(os.Stdin, counts)
            } else {
                f, err := os.Open(arg)
                if err != nil {
                    log.Fatal(err)
                }
                freq(f, counts)
                f.Close()
            }
        }
    }
    for code := range counts {
        codes = append(codes, code)
    }
    sort.Slice(codes, func(i, j int) bool { return codes[i] < codes[j] })
    for _, code := range codes {
        if (*dflag) {
            fmt.Printf("%3d ", code)
        }
        if (*oflag) {
            fmt.Printf("%03o ", code)
        }
        if (*xflag) {
            fmt.Printf("%02x ", code)
        }
        if (*cflag) {
            c := rune(code)
            if (!unicode.IsPrint(c) || unicode.IsSpace(c)) {
                c = '-'
            }
            fmt.Printf("%c ", c)
        }
        fmt.Printf("%8d\n", counts[code])
    }
}

func freqRunes(f *os.File, counts map[int32]int) {
    in := bufio.NewReader(f)

    for {
        r, n, err := in.ReadRune()
        if err == io.EOF {
            break
        }
        if err != nil {
            log.Fatal(err)
        }
        if r == unicode.ReplacementChar && n == 1 {
            continue
        }
        counts[r]++
    }
}

func freqBytes(f *os.File, counts map[int32]int) {
    in := bufio.NewReader(f)

    for {
        b, err := in.ReadByte()
        if err == io.EOF {
            break
        }
        if err != nil {
            log.Fatal(err)
        }
        counts[int32(b)]++
    }
}

freq(1) counts the occurrences of each byte (or each rune, if the -r option is given) in the standard input or the given files. More information on the manual. Unprintable and space characters are replaced with a -. Here is an example of usage with an Esperanto Lorem Ipsum:

 $ echo "eĥoŝanĝo ĉiuĵaŭde" | ./freq -r
 10 012 0a -        1
 32 040 20 -        1
 97 141 61 a        2
100 144 64 d        1
101 145 65 e        2
105 151 69 i        1
110 156 6e n        1
111 157 6f o        2
117 165 75 u        1
265 411 109 ĉ        1
285 435 11d ĝ        1
293 445 125 ĥ        1
309 465 135 ĵ        1
349 535 15d ŝ        1
365 555 16d ŭ        1

Each line prints the codepoint (in decimal, octal and hex) of the rune and the rune itself followed by the occurrence count.

How can I make the code more elegant and idiomatic?

\$\endgroup\$
3
  • \$\begingroup\$ I only have a small suggestion which doesn't need it's own answer. Use := initialization shortcut instead of var name typeName = value, you can use name := value and go will infer the types for you. \$\endgroup\$ Jun 8, 2021 at 22:07
  • \$\begingroup\$ @RacketeerHaskeller For some reason I thought that variables to functions should be explicitly typed with the function type. \$\endgroup\$
    – phillbush
    Jun 8, 2021 at 22:29
  • \$\begingroup\$ You may want to look at: A closer look at the go type system, The go reference. \$\endgroup\$ Jun 8, 2021 at 22:50

1 Answer 1

2
\$\begingroup\$

Overall looks fairly clean already.

  • Like was already said in the comments, don't need var and the explicit function type, indeed just freq := freqBytes looks much cleaner.
  • The parentheses around !*dflag && ... can be removed.
  • Using os.Stdin respectively the command line arguments looks good, only other thing you could potentially add is to use -- as a delimiter to disable the special handling of -. The error output of Close should maybe be logged too, though not a fatal condition presumably.
  • int is guaranteed to be at least 32 bits wide, rune's an alias for int32, so the same argument applies. Given that, consider using int everywhere, then you can use sort.Ints.
  • In terms of reuse, freqRunes and freqBytes really can operate on any io.Reader. Using the more generic type is better, because you can immediately see that it's actually a more powerful function, operating on any kind of IO input (say, a socket, or a byte buffer), not just files.
  • If there was a concern about performance, profiling would be in order of course, I'm not gonna make any comments about that otherwise.
\$\endgroup\$
1
  • 1
    \$\begingroup\$ "The parentheses around !*dflag && ... can be removed" D'oh! There are parentheses around every if condition! I'm too used to C... \$\endgroup\$
    – phillbush
    Jun 9, 2021 at 11:20

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.