From cced4f04b288f0781104fb7333595058d95759c4 Mon Sep 17 00:00:00 2001
From: Sacha Ligthert <sacha@ligthert.net>
Date: Tue, 28 Jan 2025 21:02:12 +0100
Subject: [PATCH] Allow to set verbosity of output (Closes #26)

---
 controller/types.go      |  6 ++++++
 export/renderJSON.go     |  5 ++---
 flags/ParseFlags.go      | 21 +++++++++++++++------
 flags/validateRow.go     |  8 ++++----
 main.go                  | 15 ++++++++++-----
 outputter/Printf.go      | 17 +++++++++++++++++
 outputter/Println.go     | 17 +++++++++++++++++
 outputter/types.go       |  5 +++++
 solver/LoadBlocks.go     |  5 ++---
 solver/PopulateBlocks.go |  5 ++---
 solver/SelectWorkload.go |  7 +++----
 solver/Tracker.go        |  5 ++---
 solver/timeTrack.go      |  3 +--
 solver/types.go          |  3 +++
 14 files changed, 89 insertions(+), 33 deletions(-)
 create mode 100644 outputter/Printf.go
 create mode 100644 outputter/Println.go
 create mode 100644 outputter/types.go

diff --git a/controller/types.go b/controller/types.go
index b6889a3..05fc6b5 100644
--- a/controller/types.go
+++ b/controller/types.go
@@ -1,5 +1,7 @@
 package controller
 
+import "gitea.ligthert.net/golang/sudoku-funpark/outputter"
+
 // Simple interface to store values shared amongst packages.
 type Controller struct {
 	// All possible blocks/rows available
@@ -32,4 +34,8 @@ type Controller struct {
 	Part int
 	// Type of output requested
 	Output string
+	// Select printing method
+	Print string
+	// Outputter package
+	Outputter *outputter.Outputter
 }
diff --git a/export/renderJSON.go b/export/renderJSON.go
index 101f497..d9eb58d 100644
--- a/export/renderJSON.go
+++ b/export/renderJSON.go
@@ -2,7 +2,6 @@ package export
 
 import (
 	"encoding/json"
-	"log"
 )
 
 // Render JSON output.
@@ -27,8 +26,8 @@ func (export *Export) renderJSON() (render string) {
 	}
 	renderBytes, err := json.Marshal(solutions)
 	if err != nil {
-		log.Println("ERROR: json.Marshal error:", err)
-		log.Println("Printing solution as-is:", solutions)
+		export.Controller.Outputter.Println("ERROR: json.Marshal error:", err)
+		export.Controller.Outputter.Println("Printing solution as-is:", solutions)
 		return ""
 	}
 	render = string(renderBytes)
diff --git a/flags/ParseFlags.go b/flags/ParseFlags.go
index 9759851..cde9de9 100644
--- a/flags/ParseFlags.go
+++ b/flags/ParseFlags.go
@@ -2,7 +2,7 @@ package flags
 
 import (
 	"flag"
-	"log"
+	"fmt"
 	"os"
 	"runtime"
 	"strings"
@@ -25,13 +25,14 @@ func (flags *Flags) ParseFlags() {
 	flag.IntVar(&flags.Controller.Split, "split", 1, "Split the tasks in n parts. This depends on the availability of the first row.")
 	flag.IntVar(&flags.Controller.Part, "part", 1, "Process part x in n parts. Cannot be lower than 1, or higher than specified in split.")
 	flag.StringVar(&flags.Controller.Output, "output", "human", "Type of output. 'human' for human readable. 'flat' for flat as stored in memory output. 'json' for JSON output.")
+	flag.StringVar(&flags.Controller.Print, "print", "short", "'short': normal output;'long': normal output with timestamps; 'silent': Only print the results;")
 
 	// Parse the flags
 	flag.Parse()
 
 	// Process any changes to the CPU usage.
 	if flags.Controller.NumCPUs <= 0 {
-		log.Printf("ERROR: Number of CPU cores must be 1 or higher.\n\n")
+		fmt.Printf("ERROR: Number of CPU cores must be 1 or higher.\n\n")
 		flags.printUsage()
 		os.Exit(1)
 	}
@@ -42,7 +43,7 @@ func (flags *Flags) ParseFlags() {
 
 	// Process rows
 	if flags.Controller.Row1 == "000000000" || flags.Controller.Row2 == "000000000" || flags.Controller.Row3 == "000000000" || flags.Controller.Row4 == "000000000" || flags.Controller.Row5 == "000000000" || flags.Controller.Row6 == "000000000" || flags.Controller.Row7 == "000000000" || flags.Controller.Row8 == "000000000" || flags.Controller.Row9 == "000000000" {
-		log.Printf("ERROR: All parameters must be entered.\n\n")
+		fmt.Printf("ERROR: All parameters must be entered.\n\n")
 		flags.printUsage()
 		os.Exit(1)
 	}
@@ -61,14 +62,14 @@ func (flags *Flags) ParseFlags() {
 	// Process workload splitting
 	// Ensure split and part are 1 or higher
 	if flags.Controller.Split <= 0 || flags.Controller.Part <= 0 {
-		log.Printf("ERROR: '-split' and '-part' need to be 1 or higher.\n")
+		fmt.Printf("ERROR: '-split' and '-part' need to be 1 or higher.\n")
 		flags.printUsage()
 		os.Exit(1)
 	}
 
 	// Ensure part is between 1 and split
 	if flags.Controller.Part > flags.Controller.Split {
-		log.Printf("ERROR: '-part' cannot be bigger than `-split`.\n")
+		fmt.Printf("ERROR: '-part' cannot be bigger than `-split`.\n")
 		flags.printUsage()
 		os.Exit(1)
 	}
@@ -76,7 +77,15 @@ func (flags *Flags) ParseFlags() {
 	// Process output selection
 	flags.Controller.Output = strings.ToLower(flags.Controller.Output)
 	if flags.Controller.Output != "human" && flags.Controller.Output != "flat" && flags.Controller.Output != "json" {
-		log.Printf("ERROR: Invalid output, can only be 'human' or 'flat'.\n")
+		fmt.Printf("ERROR: Invalid output, can only be 'human' or 'flat' or 'json'.\n")
+		flags.printUsage()
+		os.Exit(1)
+	}
+
+	// Process print selection
+	flags.Controller.Print = strings.ToLower(flags.Controller.Print)
+	if flags.Controller.Print != "short" && flags.Controller.Print != "long" && flags.Controller.Print != "silent" {
+		fmt.Printf("ERROR: Invalid Print, can only be 'short' or 'long' or 'silent'.\n")
 		flags.printUsage()
 		os.Exit(1)
 	}
diff --git a/flags/validateRow.go b/flags/validateRow.go
index cdb2b2a..6f70514 100644
--- a/flags/validateRow.go
+++ b/flags/validateRow.go
@@ -1,7 +1,7 @@
 package flags
 
 import (
-	"log"
+	"fmt"
 	"os"
 )
 
@@ -18,7 +18,7 @@ func (flags *Flags) validateRow(name string, row string) {
 
 	// 1. Make sure the row is 9 in length
 	if len(row) != 9 {
-		log.Printf("ERROR: Invalid length of %s (%s), must be 9 numbers\n\n", name, row)
+		fmt.Printf("ERROR: Invalid length of %s (%s), must be 9 numbers\n\n", name, row)
 		flags.printUsage()
 		os.Exit(1)
 	}
@@ -29,7 +29,7 @@ func (flags *Flags) validateRow(name string, row string) {
 	}
 
 	if !found {
-		log.Printf("ERROR: Invalid character of %s (%s), must be 9 numbers\n\n", name, row)
+		fmt.Printf("ERROR: Invalid character of %s (%s), must be 9 numbers\n\n", name, row)
 		flags.printUsage()
 		os.Exit(1)
 	}
@@ -46,7 +46,7 @@ func (flags *Flags) validateRow(name string, row string) {
 	}
 
 	if double {
-		log.Printf("ERROR: Double character of %s (%s), numbers between 1 and 9 may only be entered once\n\n", name, row)
+		fmt.Printf("ERROR: Double character of %s (%s), numbers between 1 and 9 may only be entered once\n\n", name, row)
 		flags.printUsage()
 		os.Exit(1)
 	}
diff --git a/main.go b/main.go
index bf71599..b6c77b3 100644
--- a/main.go
+++ b/main.go
@@ -1,29 +1,34 @@
 package main
 
 import (
-	"log"
+	"fmt"
 	"runtime"
 	"strconv"
 
 	"gitea.ligthert.net/golang/sudoku-funpark/controller"
 	"gitea.ligthert.net/golang/sudoku-funpark/export"
 	"gitea.ligthert.net/golang/sudoku-funpark/flags"
+	"gitea.ligthert.net/golang/sudoku-funpark/outputter"
 	"gitea.ligthert.net/golang/sudoku-funpark/solver"
 )
 
 func main() {
 	// Instantiate the interfaces
 	controller := controller.Controller{}
+	outp := outputter.Outputter{}
 	export := export.Export{Controller: &controller}
 	flags := flags.Flags{Controller: &controller}
-	solver := solver.Solver{Controller: &controller}
+	solver := solver.Solver{Controller: &controller, Outp: &outp}
 
 	// Parse and handle flags
 	flags.ParseFlags()
 
+	// Tell outp what kind of output is expected.
+	outp.OutputType = controller.Print
+
 	// Report number of CPUs being used, if set.
 	if runtime.NumCPU() != controller.NumCPUs {
-		log.Println("Using " + strconv.Itoa(controller.NumCPUs) + " CPUs, (was " + strconv.Itoa(runtime.NumCPU()) + ")")
+		outp.Println("Using " + strconv.Itoa(controller.NumCPUs) + " CPUs, (was " + strconv.Itoa(runtime.NumCPU()) + ")")
 	}
 
 	// Load blocks from CSV file
@@ -39,12 +44,12 @@ func main() {
 	}
 
 	// Print the total number of solutions to validate
-	log.Println("Number of (potential) solutions:", solver.Iter)
+	outp.Println("Number of (potential) solutions:", solver.Iter)
 
 	// Check the number of solutions
 	go solver.CheckCombinations()
 	solver.Tracker()
 
-	log.Println(export.Export())
+	fmt.Println(export.Export())
 
 }
diff --git a/outputter/Printf.go b/outputter/Printf.go
new file mode 100644
index 0000000..ad840ee
--- /dev/null
+++ b/outputter/Printf.go
@@ -0,0 +1,17 @@
+package outputter
+
+import (
+	"fmt"
+	"log"
+)
+
+func (outputter *Outputter) Printf(format string, args ...any) {
+	switch outputter.OutputType {
+	case "short":
+		fmt.Printf(format, args...)
+	case "long":
+		log.Printf(format, args...)
+	case "silent":
+		// Do nothing
+	}
+}
diff --git a/outputter/Println.go b/outputter/Println.go
new file mode 100644
index 0000000..b65913c
--- /dev/null
+++ b/outputter/Println.go
@@ -0,0 +1,17 @@
+package outputter
+
+import (
+	"fmt"
+	"log"
+)
+
+func (outputter *Outputter) Println(msg ...any) {
+	switch outputter.OutputType {
+	case "short":
+		fmt.Println(msg...)
+	case "long":
+		log.Println(msg...)
+	case "silent":
+		// Do nothing
+	}
+}
diff --git a/outputter/types.go b/outputter/types.go
new file mode 100644
index 0000000..86555eb
--- /dev/null
+++ b/outputter/types.go
@@ -0,0 +1,5 @@
+package outputter
+
+type Outputter struct {
+	OutputType string
+}
diff --git a/solver/LoadBlocks.go b/solver/LoadBlocks.go
index 4a84571..7b11b66 100644
--- a/solver/LoadBlocks.go
+++ b/solver/LoadBlocks.go
@@ -2,7 +2,6 @@ package solver
 
 import (
 	"embed"
-	"log"
 	"strings"
 	"time"
 )
@@ -15,8 +14,8 @@ var f embed.FS
 // Load all possible blocks from CSV in to memory
 func (solver *Solver) LoadBlocks() {
 
-	defer solver.timeTrack(time.Now(), "Loaded blocks")
-	log.Println("Loading blocks")
+	defer solver.timeTrack(time.Now(), "Done!")
+	solver.Outp.Printf("Loading blocks... ")
 
 	var blocks []string
 
diff --git a/solver/PopulateBlocks.go b/solver/PopulateBlocks.go
index 8c54287..0897ac7 100644
--- a/solver/PopulateBlocks.go
+++ b/solver/PopulateBlocks.go
@@ -1,15 +1,14 @@
 package solver
 
 import (
-	"log"
 	"time"
 )
 
 // Find all possible blocks that can be used to find a solution.
 func (solver *Solver) PopulateBlocks() {
 
-	defer solver.timeTrack(time.Now(), "Populated blocks")
-	log.Println("Populating blocks")
+	defer solver.timeTrack(time.Now(), "Done!")
+	solver.Outp.Printf("Populating blocks... ")
 
 	solver.findBlocks(&solver.Controller.Row1, &solver.row1s)
 	solver.findBlocks(&solver.Controller.Row2, &solver.row2s)
diff --git a/solver/SelectWorkload.go b/solver/SelectWorkload.go
index 40a2692..92af577 100644
--- a/solver/SelectWorkload.go
+++ b/solver/SelectWorkload.go
@@ -1,7 +1,6 @@
 package solver
 
 import (
-	"log"
 	"os"
 	"strconv"
 	"time"
@@ -12,12 +11,12 @@ import (
 // Modify solver.row1s so it limits the workload to what is only desired
 func (solver *Solver) SelectWorkload() {
 	if solver.Controller.Split > len(solver.row1s) {
-		log.Println("ERROR: Unable to divide the workload in " + strconv.Itoa(solver.Controller.Split) + " parts, when only " + strconv.Itoa(len(solver.row1s)) + " are available.\n\n")
+		solver.Outp.Println("ERROR: Unable to divide the workload in " + strconv.Itoa(solver.Controller.Split) + " parts, when only " + strconv.Itoa(len(solver.row1s)) + " are available.\n\n")
 		os.Exit(1)
 	}
 	defer solver.timeTrack(time.Now(), "Workload set")
-	log.Println("Setting workload")
-	log.Println("We are agent " + strconv.Itoa(solver.Controller.Part) + " of " + strconv.Itoa(solver.Controller.Split))
+	solver.Outp.Println("Setting workload")
+	solver.Outp.Println("We are agent " + strconv.Itoa(solver.Controller.Part) + " of " + strconv.Itoa(solver.Controller.Split))
 	workloads := solver.splitWorkload()
 	solver.setWorkload(workloads)
 }
diff --git a/solver/Tracker.go b/solver/Tracker.go
index fdeca5a..c9e34c4 100644
--- a/solver/Tracker.go
+++ b/solver/Tracker.go
@@ -1,7 +1,6 @@
 package solver
 
 import (
-	"log"
 	"strconv"
 	"time"
 )
@@ -12,7 +11,7 @@ func (solver *Solver) Tracker() {
 
 	// Add time tracking
 	defer solver.timeTrack(time.Now(), "Validated solutions")
-	log.Println("Validating solutions")
+	solver.Outp.Println("Validating solutions")
 
 	// Determine if the main-loop is done
 	var done bool
@@ -73,7 +72,7 @@ func (solver *Solver) Tracker() {
 			}
 
 			// Printing the progress
-			log.Println("Processing: " + strconv.Itoa(int(percentage)) + "% (" + strconv.FormatUint(solver.counter.Load(), 10) + "/" + strconv.Itoa(int(solver.Iter)) + "); Rate: " + strconv.FormatUint(rateDiff, 10) + "/sec for " + timer_elapsed.String() + "; Time left (est.): " + est_fin)
+			solver.Outp.Println("Processing: " + strconv.Itoa(int(percentage)) + "% (" + strconv.FormatUint(solver.counter.Load(), 10) + "/" + strconv.Itoa(int(solver.Iter)) + "); Rate: " + strconv.FormatUint(rateDiff, 10) + "/sec for " + timer_elapsed.String() + "; Time left (est.): " + est_fin)
 
 			// After we are done printing, exit this for-loop
 			if percentage == 100 {
diff --git a/solver/timeTrack.go b/solver/timeTrack.go
index bc77f7e..ede9d04 100644
--- a/solver/timeTrack.go
+++ b/solver/timeTrack.go
@@ -1,7 +1,6 @@
 package solver
 
 import (
-	"log"
 	"time"
 )
 
@@ -9,5 +8,5 @@ import (
 // Use with `defer`
 func (solver *Solver) timeTrack(start time.Time, msg string) {
 	elapsed := time.Since(start)
-	log.Printf("%s (%s)", msg, elapsed)
+	solver.Outp.Printf("%s (%s)\n", msg, elapsed)
 }
diff --git a/solver/types.go b/solver/types.go
index bb428fa..5b0bef3 100644
--- a/solver/types.go
+++ b/solver/types.go
@@ -4,6 +4,7 @@ import (
 	"sync/atomic"
 
 	"gitea.ligthert.net/golang/sudoku-funpark/controller"
+	"gitea.ligthert.net/golang/sudoku-funpark/outputter"
 )
 
 // Solve a given Sudoku puzzle by iterating through all possible solutions.
@@ -33,4 +34,6 @@ type Solver struct {
 	counter atomic.Uint64
 	// Slice of rates for accurate duration estimation.
 	rates []uint64
+	// Reference to Outputter interface
+	Outp *outputter.Outputter
 }