Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 20 additions & 178 deletions challenge-01/ccwc.go
Original file line number Diff line number Diff line change
@@ -1,201 +1,43 @@
package main

import (
"bytes"
"flag"
"fmt"
"io"
"os"
"strings"
"unicode/utf8"

"github.com/sirupsen/logrus"
)

type FileInfo struct {
// count of bytes
Bytes int
// count of lines
Lines int
// count of words
Words int
// count of unicode chars
Runes int
func formatLogs() {

// remove unwanted things and enforce colors
logrus.SetFormatter(&logrus.TextFormatter{
DisableTimestamp: true,
ForceColors: true,
PadLevelText: true,
})
}

func main() {
// setup logger
formatLogs()

// variables to store flag value
var getBytes bool
var getLines bool
var getWords bool
var getRunes bool

myFile := new(FileInfo)

// define flags
flag.BoolVar(&getBytes, "c", false, "print total bytes")
flag.BoolVar(&getLines, "l", false, "print total lines")
flag.BoolVar(&getWords, "w", false, "print total words")
flag.BoolVar(&getRunes, "m", false, "print total bytes(according to utf-8 encoding)")

// add custom usage
flag.Usage = func() {
fmt.Print("\nUsage: ccwc <flag> <file_path>\n")
fmt.Print("\nDefault flags: -c -w -l\n")
fmt.Print("\nFlags:\n")
flag.PrintDefaults()
fmt.Println()
}
formatLogs()

// parses the flags and fills the variables,
// Should be called before flags are accessed
// by program
flag.Parse()
opts := new(Opts)

// set defaults if no flag provided
if flag.NFlag() == 0 {
logrus.Info("No flags provided, using defaults...")
getBytes = true
getLines = true
getWords = true
if RegisterAndParse(opts); NoFlags() {
SetDefaults(opts)
}

inStatus, err := os.Stdin.Stat()
file, err := ReadInput()
if err != nil {
logrus.Fatal(err)
os.Exit(1)
}

var file *os.File

if (inStatus.Mode() & os.ModeCharDevice) == 0 {
file = os.Stdin
} else {

// stop if no args
if flag.NArg() == 0 {
logrus.Errorln("No argument provided")
flag.Usage()
os.Exit(1)
}

// get the first arg,
// should be a file path
filePath := flag.CommandLine.Arg(0)
// trim unwanted space
trimmedFPath := strings.TrimSpace(filePath)

// empty check
if trimmedFPath != "" {
file, err = os.Open(filePath)
if err != nil {
logrus.Fatal(err)
}
defer file.Close()
} else {
logrus.Fatal("invalid argument: empty file path.")
}
}

buffer := make([]byte, 32*1024) //32kB
leftOver := []byte{}
inWord := false

for {
n, err := file.Read(buffer)
if err != nil {
if err == io.EOF {
break
}
logrus.Fatal(err)
}

if getBytes {
processBytes(buffer[:n], myFile)
}
if getLines {
processLines(buffer[:n], myFile)
}
if getWords {
processWords(buffer[:n], myFile, &inWord)
}
if getRunes {
processRunes(buffer[:n], myFile, &leftOver)
}
}

if getBytes {
logrus.Println(myFile.Bytes)
}
if getLines {
logrus.Println(myFile.Lines)
}
if getWords {
logrus.Println(myFile.Words)
}
if getRunes {
logrus.Println(myFile.Runes)
}
}

func processBytes(buffer []byte, file *FileInfo) {

if len(buffer) > 0 {
file.Bytes += len(buffer)
}
}

func processLines(buffer []byte, file *FileInfo) {
// counts \n in buffer
file.Lines += bytes.Count(buffer, []byte{'\n'})
}

func processWords(buffer []byte, file *FileInfo, inWord *bool) {
for _, b := range buffer {
if isSpace(b) {
*inWord = false
} else {
if !*inWord {
file.Words++
*inWord = true
}
}
}
}

func processRunes(buffer []byte, file *FileInfo, leftOver *[]byte) {

buffer = append(*leftOver, buffer...)
*leftOver = (*leftOver)[:0]

i := 0
for i < len(buffer) {
if !utf8.FullRune(buffer[i:]) {
*leftOver = append(*leftOver, buffer[i:]...)
break
}
// no need to err check
// as we are sure there is atleast 1 rune ahed
_, size := utf8.DecodeRune(buffer[i:])
fstat, err := Compute(file, opts)
if err != nil {

file.Runes++
i += size
logrus.Errorf("Unable to compute file stats: %v", err)
os.Exit(1)
}
}

func isSpace(b byte) bool {
// simple ASCII check
return b == ' ' || b == '\n' || b == '\t' || b == '\r'
}

func formatLogs() {

// remove unwanted things and enforce colors
logrus.SetFormatter(&logrus.TextFormatter{
DisableTimestamp: true,
ForceColors: true,
PadLevelText: true,
})
flags, stat := Format(fstat, opts)
PrintCol(flags, stat)
}
108 changes: 108 additions & 0 deletions challenge-01/compute.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// handles the main business logic

package main

import (
"bytes"
"io"
"os"
"unicode/utf8"
)

// FileInfo holds stats of a file.
type FileInfo struct {
// count of bytes
Bytes int
// count of lines
Lines int
// count of words
Words int
// count of unicode chars
Runes int
}

func isSpace(b byte) bool {
// simple ASCII check
return b == ' ' || b == '\n' || b == '\t' || b == '\r'
}

func processBytes(buffer []byte, file *FileInfo) {

if len(buffer) > 0 {
file.Bytes += len(buffer)
}
}

func processLines(buffer []byte, file *FileInfo) {
// counts \n in buffer
file.Lines += bytes.Count(buffer, []byte{'\n'})
}

func processWords(buffer []byte, file *FileInfo, inWord *bool) {

for _, b := range buffer {
if isSpace(b) {
*inWord = false
} else {

if !*inWord {
file.Words++
*inWord = true
}
}
}
}

func processRunes(buffer []byte, file *FileInfo, leftOver *[]byte) {

buffer = append(*leftOver, buffer...)
*leftOver = (*leftOver)[:0]

i := 0
for i < len(buffer) {
if !utf8.FullRune(buffer[i:]) {
*leftOver = append(*leftOver, buffer[i:]...)
break
}
// no need to err check
// as we are sure there is atleast 1 rune ahed
_, size := utf8.DecodeRune(buffer[i:])

file.Runes++
i += size
}
}

// Compute computes the file stats according to the flags provided.
func Compute(f *os.File, opts *Opts) (*FileInfo, error) {

fstat := new(FileInfo)
buffer := make([]byte, 32*1024) //32kB
leftOver := []byte{}
inWord := false

for {
n, err := f.Read(buffer)
if err != nil {
if err == io.EOF {
break
}
return nil, err
}

if opts.getByte {
processBytes(buffer[:n], fstat)
}
if opts.getLine {
processLines(buffer[:n], fstat)
}
if opts.getWord {
processWords(buffer[:n], fstat, &inWord)
}
if opts.getRune {
processRunes(buffer[:n], fstat, &leftOver)
}
}

return fstat, nil
}
18 changes: 9 additions & 9 deletions challenge-01/ccwc_test.go → challenge-01/compute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,49 +139,49 @@ func Test_ProcessWords(t *testing.T) {
}

func Test_ProcessRunes(t *testing.T) {

r := []byte("😍")

// This is an invalid utf-8 character
invalidUTF8 := []byte{0xFF}

type step struct {
input []byte
input []byte
leftOver []byte
}

tests := []struct {
name string
steps []step
expected int
name string
steps []step
expected int
}{
{
name: "incomplete rune",
steps: []step{
{input: r[:2], leftOver: r[:2]},
{input: r[2:], leftOver: []byte{}},
},
},
expected: 1,
},
{
name: "ascii characters",
steps: []step{
{input: []byte("abcd"), leftOver: nil},
},
},
expected: 4,
},
{
name: "incomplete end",
steps: []step{
{input: r[:2], leftOver: r[:2]},
},
},
expected: 0,
},
{
name: "invalid utf-8",
steps: []step{
{input: invalidUTF8, leftOver: nil},
},
},
expected: 1,
},
}
Expand Down
Loading
Loading