Add exclusion support
4 files changed, 151 insertions(+), 14 deletions(-)

M cmd/orgchart/main.go
M cmd/orgchart/main_test.go
M cmd/orgchart/svg_test.go
M todo.txt
M cmd/orgchart/main.go +65 -4
@@ 36,7 36,8 @@ func main() {
 	sup := goopt.Flag([]string{"-s"}, []string{}, "Display superior; only used if results are filtered", "")
 	hil := goopt.Strings([]string{"-c"}, "", "Column,ValueMatch,ClassName -- sets a CSS class based on a regexp match in a column")
 	cssfile := goopt.String([]string{"-y"}, "", "CSS file to embed")
-	//lin := goopt.Strings([]string{"-l"}, "", "Box stroke. Format: <col#>,<regexp>.")
+	ands := goopt.Strings([]string{"--ex-and"}, "", "Column,Value -- Exclude (and -- all ands must match")
+	ors := goopt.Strings([]string{"--ex-or"}, "", "Column,Value -- Exclude (or -- any ors may match")
 	goopt.Version = ""
 	goopt.Summary = "Organization chart generator"
 	goopt.Parse(nil)

          
@@ 81,7 82,12 @@ func main() {
 		fmt.Println(err.Error())
 		os.Exit(1)
 	}
-	ps, err := makePeople(fin, cols, rules)
+	exc, err := makeExcludes(*ands, *ors)
+	if err != nil {
+		fmt.Println(err.Error())
+		os.Exit(1)
+	}
+	ps, err := makePeople(fin, exc, cols, rules)
 	if err != nil {
 		fmt.Println(err.Error())
 		os.Exit(1)

          
@@ 140,7 146,7 @@ func matches(p *Person, re *regexp.Regex
 
 // Parses a CSV data stream, producing a map of people indexed by the person
 // name.  Returns an error if a non-EOF read error is encountered
-func makePeople(f io.Reader, cols []int, rs Ruleset) (map[string]*Person, error) {
+func makePeople(f io.Reader, exc Excludes, cols []int, rs Ruleset) (map[string]*Person, error) {
 	r := csv.NewReader(f)
 	r.LazyQuotes = true
 	r.TrailingComma = true

          
@@ 157,7 163,7 @@ func makePeople(f io.Reader, cols []int,
 			header = false
 			continue
 		}
-		if l[0] == "" {
+		if l[0] == "" || exc.Match(l) {
 			continue
 		}
 		name := l[0]

          
@@ 220,3 226,58 @@ func addRecursively(p *Person, rs Ranks,
 	}
 	return rs
 }
+
+type Exclude struct {
+	idx   int
+	match string
+}
+
+type Excludes struct {
+	ands []Exclude
+	ors  []Exclude
+}
+
+func (ex Excludes) Match(cols []string) bool {
+	for _, e := range ex.ors {
+		if e.idx < len(cols) {
+			if e.match == cols[e.idx] {
+				return true
+			}
+		}
+	}
+	matches := len(ex.ands) > 0
+	for _, e := range ex.ands {
+		if e.idx < len(cols) {
+			if e.match != cols[e.idx] {
+				matches = false
+			}
+		} else {
+			return false
+		}
+	}
+	return matches
+}
+
+func makeExcludes(ands, ors []string) (Excludes, error) {
+	var err error
+	rv := Excludes{}
+	rv.ands, err = breakUp(ands)
+	rv.ors, err = breakUp(ors)
+	return rv, err
+}
+
+func breakUp(input []string) ([]Exclude, error) {
+	output := make([]Exclude, 0)
+	for _, a := range input {
+		cs := strings.Split(a, ",")
+		if len(cs) != 2 {
+			return output, errors.New("Malformed exclude argument " + a)
+		}
+		idx, err := strconv.Atoi(cs[0])
+		if err != nil {
+			return output, errors.New("Malformed exclude argument " + a)
+		}
+		output = append(output, Exclude{idx, cs[1]})
+	}
+	return output, nil
+}

          
M cmd/orgchart/main_test.go +84 -8
@@ 50,8 50,9 @@ func makeData() (map[string]*Person, *Pe
 	return ps, a
 }
 
-func testMakePeople(t *testing.T) {
-	s := `C1,B1
+func TestMakePeople(t *testing.T) {
+	s := `H1,H2
+C1,B1
 E1,D1
 B1,A
 A,

          
@@ 60,7 61,8 @@ D1,B2
 C2,B2`
 	b := bytes.NewBufferString(s)
 	rx := make(Ruleset, 0)
-	ps, er := makePeople(b, []int{}, rx)
+	exc, _ := makeExcludes([]string{}, []string{})
+	ps, er := makePeople(b, exc, []int{}, rx)
 	if er != nil {
 		assert.Fail(t, "failed to parse: %s", er.Error())
 	}

          
@@ 95,7 97,7 @@ C2,B2`
 }
 
 func TestDims(t *testing.T) {
-	s := `
+	s := `H1,H2
 X,
 A,X
 B1,A

          
@@ 111,7 113,8 @@ C2B2,B2
 C3B2,B2`
 	b := bytes.NewBufferString(s)
 	rx := make(Ruleset, 0)
-	ps, er := makePeople(b, []int{}, rx)
+	exc, _ := makeExcludes([]string{}, []string{})
+	ps, er := makePeople(b, exc, []int{}, rx)
 	if er != nil {
 		assert.Fail(t, "failed to parse: %s", er.Error())
 	}

          
@@ 119,7 122,8 @@ C3B2,B2`
 
 	assert.Equal(t, 3, rs.width())
 
-	s = `X,
+	s = `H1,H2
+X,
 A,X
 B1,A
 B2,A

          
@@ 135,7 139,8 @@ D1,C2B3
 D2,C2B3`
 	b = bytes.NewBufferString(s)
 	rx = make(Ruleset, 0)
-	ps, er = makePeople(b, []int{}, rx)
+	exc, _ = makeExcludes([]string{}, []string{})
+	ps, er = makePeople(b, exc, []int{}, rx)
 	if er != nil {
 		assert.Fail(t, "failed to parse: %s", er.Error())
 	}

          
@@ 159,7 164,8 @@ W,,false,false`
 	if er != nil {
 		assert.Fail(t, er.Error())
 	}
-	ps, er := makePeople(b, []int{}, rx)
+	exc, _ := makeExcludes([]string{}, []string{})
+	ps, er := makePeople(b, exc, []int{}, rx)
 	if er != nil {
 		assert.Fail(t, er.Error())
 	}

          
@@ 171,3 177,73 @@ W,,false,false`
 	assert.Equal(t, ps["Z"].Class, "A B")
 	assert.Equal(t, ps["W"].Class, "")
 }
+
+func TestExcludes(t *testing.T) {
+	ex1, er := makeExcludes(
+		[]string{"0,TRUE", "1,TRUE"}, // AND
+		[]string{"2,TRUE", "3,TRUE"}, // OR
+	)
+	if er != nil {
+		assert.Fail(t, "failed to parse: %s", er.Error())
+	}
+	assert.True(t, ex1.Match([]string{"", "", "", "TRUE"}))
+	assert.True(t, ex1.Match([]string{"", "", "TRUE", ""}))
+	assert.True(t, ex1.Match([]string{"TRUE", "TRUE", "", ""}))
+	assert.True(t, ex1.Match([]string{"TRUE", "TRUE", "", "TRUE"}))
+	assert.False(t, ex1.Match([]string{"", "", "", ""}))
+	assert.False(t, ex1.Match([]string{"TRUE", "", "", ""}))
+	assert.False(t, ex1.Match([]string{"", "TRUE", "", ""}))
+}
+
+func TestExclude(t *testing.T) {
+	s := `H1,H2,X1,X2,X3
+X,,,,
+A,X,,,
+B1,A,,,
+B2,A,,,
+B3,A,,,
+C1B1,B1,FALSE,FALSE,TRUE
+C2B1,B1,,,TRUE
+C3B1,B1,TRUE,FALSE,TRUE
+C1B2,B2,TRUE,TRUE,
+C1B3,B3,TRUE,FALSE,
+C2B3,B3,FALSE,TRUE,
+C3B3,B3,FALSE,FALSE,
+D1,C2B3,TRUE,TRUE,TRUE
+D2,C2B3,TRUE,TRUE,FALSE`
+	b := bytes.NewBufferString(s)
+	rx := make(Ruleset, 0)
+	ex1, er := makeExcludes(
+		[]string{"2,TRUE", "3,TRUE"}, // AND
+		[]string{"4,TRUE"},           // OR
+	)
+	if er != nil {
+		assert.Fail(t, "failed to parse: %s", er.Error())
+	}
+	ps, er := makePeople(b, ex1, []int{}, rx)
+	if er != nil {
+		assert.Fail(t, "failed to parse: %s", er.Error())
+	}
+	rs := rank(ps)
+	var foundCount int
+	for _, ps := range rs {
+		for _, p := range ps {
+			assert.NotEqual(t, "C1B1", p.Name)
+			assert.NotEqual(t, "C1B1", p.Name)
+			assert.NotEqual(t, "C3B1", p.Name)
+			assert.NotEqual(t, "C1B2", p.Name)
+			assert.NotEqual(t, "D1", p.Name)
+			switch p.Name {
+			case "B3":
+				foundCount += 1
+			case "C1B3":
+				foundCount += 1
+			case "C2B3":
+				foundCount += 1
+			case "C3B3":
+				foundCount += 1
+			}
+		}
+	}
+	assert.Equal(t, 4, foundCount)
+}

          
M cmd/orgchart/svg_test.go +2 -1
@@ 28,9 28,10 @@ func TestMaxTextWidth(t *testing.T) {
 	if er != nil {
 		assert.Fail(t, er.Error())
 	}
+	exc, _ := makeExcludes([]string{}, []string{})
 	for i, tst := range tests {
 		b := bytes.NewBufferString(h + tst)
-		ps, er = makePeople(b, []int{2, 3, 4}, rx)
+		ps, er = makePeople(b, exc, []int{2, 3, 4}, rx)
 		if er != nil {
 			assert.Fail(t, er.Error())
 		}

          
M todo.txt +0 -1
@@ 1,4 1,3 @@ 
 Dotted-line reports
 Admin associations
-Exclude
 Pictures