Skip to content

Commit

Permalink
Use csv as inventory file
Browse files Browse the repository at this point in the history
  • Loading branch information
mvach committed Nov 3, 2024
1 parent 8f94a74 commit 06512f6
Show file tree
Hide file tree
Showing 21 changed files with 1,137 additions and 262 deletions.
29 changes: 29 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "App Test",
"type": "go",
"request": "launch",
"mode": "test",
"program": "${workspaceFolder}/app"
},
{
"name": "Config Tests",
"type": "go",
"request": "launch",
"mode": "test",
"program": "${workspaceFolder}/config"
},
{
"name": "Launch Specific Test",
"type": "go",
"request": "launch",
"mode": "test",
"program": "${workspaceFolder}/app/inventory_data_test.go",
}
]
}
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,17 @@ Dieses kleine Tool ermöglicht es, erfasstes Equipment mit den THW-Inventurdaten
Das Tool lädt man am einfachsten aus der [Releases-Sektion](https://github.com/mvach/thwInventoryMerge/releases) herunter und legt es in ein beliebiges leeres Verzeichnis (das `working_dir`).

## Konfiguration
Die CSV-Dateien mit den erfassten Barcodes der Scanner sowie die (aus THWin exportierte) Inventur-Excel-Datei legt man am besten ebenfalls in das `working_dir`.
Die CSV-Dateien mit den erfassten Barcodes der Scanner sowie die (aus THWin exportierte) Inventur CSV Datei legt man am besten ebenfalls in das `working_dir`.

Zudem erstellt man eine Konfigurationsdatei (`config.json`), die man am einfachsten auch in das `working_dir` legt.
Zudem erstellt man eine Konfigurationsdatei (`config.json`), die man auch in das `working_dir` legt.

## Hier ist eine Beispielkonfiguration:

```
// config.json
{
"excel_file_name": "20240101_Bestand.xlsx",
"excel_config": {
"worksheet_name": "N",
"inventory_csv_file_name": "20240101_Bestand_FGr_N.csv",
"inventory_csv_config": {
"equipment_id_column_name": "Inventar Nr",
"equipment_available_column_name": "Verfügbar"
}
Expand All @@ -31,7 +30,7 @@ C:/
└── DeinUser/
└── MeinArbeitsverzeichnis/
├── config.json
├── 20240101_Bestand.xlsx
├── 20240101_Bestand_FGr_N.csv
├── scanner1.csv
├── scanner2.csv
├── scanner3.csv
Expand All @@ -54,4 +53,8 @@ C:/

## Ausführung

Liegen alle Dateien gemeinsam im `working_dir`, kann thwInventoryMerge.exe einfach per Doppelklick ausgeführt werden.
Befinden sich alle Dateien gemeinsam im `working_dir`, kann die Datei `thwInventoryMerge.exe` einfach per Doppelklick ausgeführt werden.

Nach der Ausführung wird im `working_dir` ein `result`-Verzeichnis erstellt, in dem sich eine Datei namens `result_<timestamp>.csv` befindet. Diese Datei enthält die zusammengeführten Inventurdaten.

Jede weitere Ausführung erzeugt eine neue Datei `result_<timestamp>.csv`.
13 changes: 13 additions & 0 deletions app/app_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package app_test

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestConfig(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "App Suite")
}
66 changes: 66 additions & 0 deletions app/csv_file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package app

import (
"encoding/csv"
"fmt"
"os"
)

type CSVContent [][]string

type CSVFile interface {
Read(filePath string) (CSVContent, error)

Write(filePath string, content CSVContent) error
}

type csvFile struct {
}

func NewCSVFile() CSVFile {
return &csvFile{}
}

func (c *csvFile) Read(filePath string) (CSVContent, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("failed to open CSV file '%s': %w", filePath, err)
}
defer file.Close()

reader := csv.NewReader(file)
reader.Comma = ';'
reader.LazyQuotes = true
content, err := reader.ReadAll()
if err != nil {
return nil, fmt.Errorf("failed to read CSV file '%s': %w", filePath, err)
}

return content, nil
}

func (c *csvFile) Write(filePath string, content CSVContent) error {
file, err := os.Create(filePath)
if err != nil {
return fmt.Errorf("failed to create CSV file: %w", err)
}
defer file.Close()

// Write the UTF-8 BOM for Excel on Windows compatibility
_, err = file.Write([]byte{0xEF, 0xBB, 0xBF})
if err != nil {
return fmt.Errorf("failed to write UTF-8 BOM to csv file: %v", err)
}

writer := csv.NewWriter(file)
writer.Comma = ';'

err = writer.WriteAll(content)
if err != nil {
return fmt.Errorf("failed to write into CSV file: %w", err)
}

writer.Flush()

return nil
}
174 changes: 174 additions & 0 deletions app/csv_file_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package app_test

import (
"bufio"
"encoding/csv"
"os"
"path/filepath"
"thwInventoryMerge/app"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("CSVFile", func() {

var (
filePath string
)

BeforeEach(func() {
})

AfterEach(func() {
if filePath != "" {
os.Remove(filePath)
}
})

var _ = Describe("Read", func() {
Context("when the CSV file is valid", func() {
BeforeEach(func() {
filePath = filepath.Join(os.TempDir(), "valid.csv")
file, err := os.Create(filePath)
Expect(err).NotTo(HaveOccurred())
defer file.Close()

writer := csv.NewWriter(file)
writer.Comma = ';'
writer.WriteAll([][]string{
{"name", "age", "city"},
{"Alice", "30", "New York"},
{"Bob", "25", "San Francisco"},
})
writer.Flush()
})

It("should read the CSV file successfully", func() {
content, err := app.NewCSVFile().Read(filePath)
Expect(err).NotTo(HaveOccurred())

Expect(content).To(HaveLen(3)) // header + 2 rows
Expect(content[0]).To(Equal([]string{"name", "age", "city"}))
Expect(content[1]).To(Equal([]string{"Alice", "30", "New York"}))
Expect(content[2]).To(Equal([]string{"Bob", "25", "San Francisco"}))
})
})

Context("when the CSV file starts like the original from thw", func() {
BeforeEach(func() {
csvContent := `"Ebene";"OE";"Art";"FB";"Menge";"Menge Ist";"Verfügbar";"Ausstattung | Hersteller | Typ";"Sachnummer";"Inventar Nr";"Gerätenr.";"Status"
;"OV Speyer";"";"";"";"";"";"1. Technischer Zug/Fachgruppe Notversorgung und Notinstandsetzung";"";"";"";"V"
1;"";"";"";"";"";"";"Geringwertiges Material";"";"";"";"V"
2;"";"Gwm";"";"";"1";"1";" Eiskratzer, handelsüblich";"2540T21171";"---------------";"--------------------";"V"`

filePath = filepath.Join(os.TempDir(), "valid.csv")
file, err := os.Create(filePath)
Expect(err).NotTo(HaveOccurred())
defer file.Close()

writer := bufio.NewWriter(file)
_, err = writer.WriteString(csvContent)
Expect(err).NotTo(HaveOccurred())

err = writer.Flush()
Expect(err).NotTo(HaveOccurred())
})

It("should read the CSV file successfully", func() {
_, err := app.NewCSVFile().Read(filePath)
Expect(err).NotTo(HaveOccurred())

// Expect(content).To(HaveLen(3)) // header + 2 rows
// Expect(content[0]).To(Equal([]string{"name", "age", "city"}))
// Expect(content[1]).To(Equal([]string{"Alice", "30", "New York"}))
// Expect(content[2]).To(Equal([]string{"Bob", "25", "San Francisco"}))
})
})



Context("when the file does not exist", func() {
It("should return an error", func() {
_, err := app.NewCSVFile().Read("nonexistent.csv")

Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("failed to open CSV file"))
})
})

Context("when the CSV file is malformed", func() {
BeforeEach(func() {
// Create a temporary CSV file with malformed content
filePath = filepath.Join(os.TempDir(), "malformed.csv")
file, err := os.Create(filePath)
Expect(err).NotTo(HaveOccurred())
defer file.Close()

file.WriteString("name;age;city\nAlice;30\nBob;25;San Francisco") // Missing one field in Alice's row
})

It("should return an error when reading", func() {
_, err := app.NewCSVFile().Read(filePath)

Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("failed to read CSV file"))
})
})
})

var _ = Describe("Write", func() {
var (
content [][]string
)

BeforeEach(func() {
content = [][]string{
{"name", "age", "city"},
{"Alice", "30", "New York"},
{"Bob", "25", "San Francisco"},
}
})

Context("when the CSV file path is valid", func() {
BeforeEach(func() {
filePath = filepath.Join(os.TempDir(), "output.csv")
})

It("should write the content to the CSV file successfully", func() {
err := app.NewCSVFile().Write(filePath, content)
Expect(err).NotTo(HaveOccurred())

file, err := os.Open(filePath)
Expect(err).NotTo(HaveOccurred())
defer file.Close()

scanner := bufio.NewScanner(file)

// \ufeff is the UTF-8 BOM for Excel on Windows compatibility
expectedLines := []string{
"\ufeffname;age;city",
"Alice;30;New York",
"Bob;25;San Francisco",
}

i := 0
for scanner.Scan() {
Expect(scanner.Text()).To(Equal(expectedLines[i]))
i++
}
Expect(scanner.Err()).NotTo(HaveOccurred())
Expect(i).To(Equal(len(expectedLines)))
})
})

Context("when the file path is invalid", func() {
It("should return an error", func() {
invalidPath := "/invalid/output.csv" // Likely to be invalid on most systems
err := app.NewCSVFile().Write(invalidPath, content)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("failed to create CSV file"))
})
})
})
})
Loading

0 comments on commit 06512f6

Please sign in to comment.