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
30 changes: 15 additions & 15 deletions challenge-01/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import (
"io"
"unicode/utf8"

stdconfig "github.com/priyanshsao/coding-challenges-go/challenge-01/config"
"github.com/priyanshsao/coding-challenges-go/challenge-01/config"
)

// Process computes the value of each option.
func Process(c *stdconfig.Config) error {
func Process(c *config.Config) error {

buffer := make([]byte, 32*1024) //32kB
leftOver := []byte{}
Expand All @@ -30,13 +30,13 @@ func Process(c *stdconfig.Config) error {
if val {

switch opts {
case stdconfig.BYTES:
case config.BYTES:
processBytes(buffer[:n], &c.Result)
case stdconfig.LINES:
case config.LINES:
processLines(buffer[:n], &c.Result)
case stdconfig.WORDS:
case config.WORDS:
processWords(buffer[:n], &c.Result, &inWord)
case stdconfig.RUNES:
case config.RUNES:
processRunes(buffer[:n], &c.Result, &leftOver)
}
}
Expand All @@ -46,34 +46,34 @@ func Process(c *stdconfig.Config) error {
return nil
}

func processBytes(buffer []byte, result *stdconfig.Result) {
func processBytes(buffer []byte, result *config.Result) {

if len(buffer) > 0 {
(*result)[stdconfig.BYTES] += len(buffer)
(*result)[config.BYTES] += len(buffer)
}
}

func processLines(buffer []byte, result *stdconfig.Result) {
func processLines(buffer []byte, result *config.Result) {

(*result)[stdconfig.LINES] += bytes.Count(buffer, []byte{'\n'})
(*result)[config.LINES] += bytes.Count(buffer, []byte{'\n'})
}

func processWords(buffer []byte, result *stdconfig.Result, inWord *bool) {
func processWords(buffer []byte, result *config.Result, inWord *bool) {

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

if !*inWord {
(*result)[stdconfig.WORDS]++
(*result)[config.WORDS]++
*inWord = true
}
}
}
}

func processRunes(buffer []byte, result *stdconfig.Result, leftOver *[]byte) {
func processRunes(buffer []byte, result *config.Result, leftOver *[]byte) {

buffer = append(*leftOver, buffer...)
*leftOver = (*leftOver)[:0]
Expand All @@ -88,12 +88,12 @@ func processRunes(buffer []byte, result *stdconfig.Result, leftOver *[]byte) {
// as we are sure there is atleast 1 rune ahed
_, size := utf8.DecodeRune(buffer[i:])

(*result)[stdconfig.RUNES]++
(*result)[config.RUNES]++
i += size
}
}

func isSpace(b byte) bool {
// simple ASCII check
return b == ' ' || b == '\n' || b == '\t' || b == '\r'
}
}
212 changes: 212 additions & 0 deletions challenge-01/analyzer/analyzer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package analyzer

import (
"reflect"
"testing"

"github.com/priyanshsao/coding-challenges-go/challenge-01/config"
)

func Test_ProcessBytes(t *testing.T) {

tests := []struct {
name string
input string
expected int
}{
{"empty", "", 0},
{"ascii", "hello", 5},

// edge cases
{"escape sequence", "\n\n\t\r", 4},
{"unicode", "😍😍😍", 12}, //😍 = 4 bytes
}

for _, tt := range tests {

t.Run(tt.name, func(t *testing.T) {

result := config.New().Result

processBytes([]byte(tt.input), &result)

if result[config.BYTES] != tt.expected {
t.Errorf("expected %d but got %d", tt.expected, result[config.BYTES])
}
})
}
}

func Test_ProcessLines(t *testing.T) {

// Todo: currently processLine only
// counts \n in future we need to add
// logic and test for counting lines for files
// that dont end with '\n' or '\r\n'.

tests := []struct {
name string
input string
expected int
}{
{"line feed", "Hi,\n I am a dev", 1},
{"CR+LF", "Hi,\r\n I am a dev\r\n", 2},
}

for _, tt := range tests {

t.Run(tt.name, func(t *testing.T) {
result := config.New().Result

processLines([]byte(tt.input), &result)

if result[config.LINES] != tt.expected {
t.Errorf("expected %d but got %d", tt.expected, result[config.LINES])
}
})
}
}

func Test_ProcessWords(t *testing.T) {

tests := []struct {
name string
input []string
expected int
expectedState []bool
}{
// empty []string{} case
// is not present, because
// function's loop doesn't run for that case,
// so we think there is no need to
// test that.
{
"empty string",
[]string{""},
0,
[]bool{false},
},
{
"normally spaced words",
[]string{"Hi ", "I am", " a developer"},
5,
[]bool{false, true, true},
},

// edge cases
{
"seperated by escape characters",
[]string{"Hi,\tI\tam\ta\ndeveloper"},
5,
[]bool{true},
},
{
"rune input",
[]string{"😍", "😍", "😍"},
1,
[]bool{true, true, true},
},
{
"incomplete words across buffer",
[]string{" h", "ey! this i", "s cc", "wc "},
4,
[]bool{true, true, true, false},
},
{
"single character in each buffer",
[]string{"h", "e", "l", "l", "o"},
1,
[]bool{true, true, true, true, true},
},
}

for _, tt := range tests {

t.Run(tt.name, func(t *testing.T) {
inWord := new(bool)
result := config.New().Result

for i, str := range tt.input {
processWords([]byte(str), &result, inWord)

if *inWord != tt.expectedState[i] {
t.Errorf("expected %v but got %v", tt.expectedState[i], *inWord)
}
}

if result[config.WORDS] != tt.expected {
t.Errorf("expected %v but got this %v", tt.expected, result[config.WORDS])
}
})
}
}

func Test_ProcessRunes(t *testing.T) {

r := []byte("😍")

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

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

tests := []struct {
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,
},
}

for _, tt := range tests {

t.Run(tt.name, func(t *testing.T) {
var leftOver []byte
result := config.New().Result

for _, s := range tt.steps {

processRunes(s.input, &result, &leftOver)

if !reflect.DeepEqual(leftOver, s.leftOver) {
t.Errorf("expected %v but got %v", s.leftOver, leftOver)
}
}

if result[config.RUNES] != tt.expected {
t.Errorf("expected %v but got %v", tt.expected, result[config.RUNES])
}
})
}
}
4 changes: 1 addition & 3 deletions challenge-01/cmd/ccwc.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,11 @@ func main() {
}

if err := analyzer.Read(c); err != nil {
// Todo: log error
logrus.Errorf("unable to read input: %v", err)
return
}

if err := analyzer.Process(c); err != nil {
// Todo: log error
logrus.Errorf("unable to process input: %v", err)
return
}
Expand All @@ -72,7 +70,7 @@ func setLogger() {

// remove unwanted things and enforce colors
logrus.SetFormatter(&logrus.TextFormatter{
ForceColors: true,
ForceColors: true,
DisableLevelTruncation: true,
})
}
3 changes: 1 addition & 2 deletions challenge-01/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ type Config struct {
Result Result
}

// New returns a new config for analyzing input.
// New returns a new config for analyzing input.
func New() *Config {

config := &Config{
Expand All @@ -51,7 +51,6 @@ func New() *Config {
return config
}

// String value of an operation.
func (opts FileOpts) String() string {

var str string
Expand Down
Loading