diff --git a/semver/semver.go b/semver/semver.go new file mode 100644 index 00000000..9192327 --- /dev/null +++ b/semver/semver.go @@ -0,0 +1,128 @@ +package semver + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "strings" +) + +var ( + semverRegex = regexp.MustCompile(`^v?(\d+)\.(\d+)\.(\d+)(?:-([\w\.\-]+))?(?:\+([\w\.\-]+))?$`) +) + +type Version struct { + Major int + Minor int + Patch int + PreRelease string + BuildMetadata string +} + +func Parse(version string) (*Version, error) { + matches := semverRegex.FindStringSubmatch(version) + if matches == nil { + return nil, errors.New("invalid semantic version") + } + + major, _ := strconv.Atoi(matches[1]) + minor, _ := strconv.Atoi(matches[2]) + patch, _ := strconv.Atoi(matches[3]) + + return &Version{ + Major: major, + Minor: minor, + Patch: patch, + PreRelease: matches[4], + BuildMetadata: matches[5], + }, nil +} + +func (v *Version) String() string { + ver := fmt.Sprintf("v%d.%d.%d", v.Major, v.Minor, v.Patch) + if v.PreRelease != "" { + ver += "-" + v.PreRelease + } + if v.BuildMetadata != "" { + ver += "+" + v.BuildMetadata + } + return ver +} + +func (v *Version) Compare(other *Version) int { + if v.Major != other.Major { + return compareInt(v.Major, other.Major) + } + if v.Minor != other.Minor { + return compareInt(v.Minor, other.Minor) + } + if v.Patch != other.Patch { + return compareInt(v.Patch, other.Patch) + } + return comparePreRelease(v.PreRelease, other.PreRelease) +} + +func compareInt(a, b int) int { + if a < b { + return -1 + } + if a > b { + return 1 + } + return 0 +} + +func comparePreRelease(a, b string) int { + if a == b { + return 0 + } + if a == "" { + return 1 + } + if b == "" { + return -1 + } + return strings.Compare(a, b) +} + +type Constraint struct { + Operator string + Version *Version +} + +func ParseConstraint(constraint string) (*Constraint, error) { + parts := strings.Fields(constraint) + if len(parts) != 2 { + return nil, errors.New("invalid constraint format") + } + + version, err := Parse(parts[1]) + if err != nil { + return nil, err + } + + return &Constraint{ + Operator: parts[0], + Version: version, + }, nil +} + +func (c *Constraint) Matches(v *Version) bool { + switch c.Operator { + case "=": + return v.Compare(c.Version) == 0 + case "!=": + return v.Compare(c.Version) != 0 + case "<": + return v.Compare(c.Version) < 0 + case "<=": + return v.Compare(c.Version) <= 0 + case ">": + return v.Compare(c.Version) > 0 + case ">=": + return v.Compare(c.Version) >= 0 + default: + return false + } +} diff --git a/semver/semver_test.go b/semver/semver_test.go new file mode 100644 index 00000000..991b633 --- /dev/null +++ b/semver/semver_test.go @@ -0,0 +1,41 @@ +package semver + +import ( + "testing" +) + +func TestParse(t *testing.T) { + version, err := Parse("1.2.3-alpha+001") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if version.Major != 1 || version.Minor != 2 || version.Patch != 3 || version.PreRelease != "alpha" || version.BuildMetadata != "001" { + t.Fatalf("parsed version incorrect: %v", version) + } +} + +func TestString(t *testing.T) { + version, _ := Parse("1.2.3-alpha+001") + if version.String() != "v1.2.3-alpha+001" { + t.Fatalf("expected version string 'v1.2.3-alpha+001', got %s", version.String()) + } +} + +func TestCompare(t *testing.T) { + v1, _ := Parse("1.2.3") + v2, _ := Parse("2.0.0") + if v1.Compare(v2) >= 0 { + t.Fatalf("expected v1 < v2") + } +} + +func TestConstraint(t *testing.T) { + constraint, err := ParseConstraint(">= 1.2.3") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + version, _ := Parse("1.2.3") + if !constraint.Matches(version) { + t.Fatalf("expected version to match constraint") + } +} diff --git a/table/example_test.go b/table/example_test.go index d220d88..ef76001 100644 --- a/table/example_test.go +++ b/table/example_test.go @@ -1,71 +1,71 @@ package table import ( - "fmt" + "fmt" ) // Beispiel-Datenstrukturen type Person struct { - Name string - Age int - Email string + Name string + Age int + Email string } type Product struct { - ID int - Name string - Price float64 + ID int + Name string + Price float64 } // Beispiel: ASCII-Tabelle ohne Rotation func ExampleRenderASCII_noRotation() { - people := []Person{ - {Name: "John", Age: 42, Email: "john@example.com"}, - {Name: "Jane", Age: 32, Email: "jane@example.com"}, - } + people := []Person{ + {Name: "John", Age: 42, Email: "john@example.com"}, + {Name: "Jane", Age: 32, Email: "jane@example.com"}, + } - asciiTable, _ := RenderASCII(people, TableOption{Rotate: false}) - fmt.Println(asciiTable) - // Output: - // +------+-----+------------------+ - // | Name | Age | Email | - // +------+-----+------------------+ - // | John | 42 | john@example.com | - // | Jane | 32 | jane@example.com | - // +------+-----+------------------+ + asciiTable, _ := RenderASCII(people, TableOption{Rotate: false}) + fmt.Println(asciiTable) + // Output: + // +------+-----+------------------+ + // | Name | Age | Email | + // +------+-----+------------------+ + // | John | 42 | john@example.com | + // | Jane | 32 | jane@example.com | + // +------+-----+------------------+ } // Beispiel: ASCII-Tabelle mit Rotation func ExampleRenderASCII_withRotation() { - people := []Person{ - {Name: "John", Age: 42, Email: "john@example.com"}, - {Name: "Jane", Age: 32, Email: "jane@example.com"}, - } + people := []Person{ + {Name: "John", Age: 42, Email: "john@example.com"}, + {Name: "Jane", Age: 32, Email: "jane@example.com"}, + } - rotatedASCIITable, _ := RenderASCII(people, TableOption{Rotate: true}) - fmt.Println(rotatedASCIITable) - // Output: - // +-------+------------------+------------------+ - // | | Row 1 | Row 2 | - // +-------+------------------+------------------+ - // | Name | John | Jane | - // | Age | 42 | 32 | - // | Email | john@example.com | jane@example.com | - // +-------+------------------+------------------+ + rotatedASCIITable, _ := RenderASCII(people, TableOption{Rotate: true}) + fmt.Println(rotatedASCIITable) + // Output: + // +-------+------------------+------------------+ + // | | Row 1 | Row 2 | + // +-------+------------------+------------------+ + // | Name | John | Jane | + // | Age | 42 | 32 | + // | Email | john@example.com | jane@example.com | + // +-------+------------------+------------------+ } // Beispiel: Markdown-Tabelle ohne Rotation func ExampleRenderMarkdown_noRotation() { - people := []Person{ - {Name: "John", Age: 42, Email: "john@example.com"}, - {Name: "Jane", Age: 32, Email: "jane@example.com"}, - } + people := []Person{ + {Name: "John", Age: 42, Email: "john@example.com"}, + {Name: "Jane", Age: 32, Email: "jane@example.com"}, + } - markdownTable, _ := RenderMarkdown(people, TableOption{Rotate: false}) - fmt.Println(markdownTable) - // Output: - // | Name | Age | Email | - // |------|-----|------------------| - // | John | 42 | john@example.com | - // | Jane | 32 | jane@example.com | + markdownTable, _ := RenderMarkdown(people, TableOption{Rotate: false}) + fmt.Println(markdownTable) + // Output: + // | Name | Age | Email | + // |------|-----|------------------| + // | John | 42 | john@example.com | + // | Jane | 32 | jane@example.com | } diff --git a/table/table.go b/table/table.go index 3893797..e5b8173 100644 --- a/table/table.go +++ b/table/table.go @@ -1,163 +1,163 @@ package table import ( - "bytes" - "fmt" - "reflect" - "strings" + "bytes" + "fmt" + "reflect" + "strings" ) // TableOption defines the configuration for table output type TableOption struct { - Rotate bool + Rotate bool } // RenderASCII renders the struct slice as an ASCII table func RenderASCII(data interface{}, opt TableOption) (string, error) { - var buffer bytes.Buffer - headers, rows := parseStruct(data) - - if opt.Rotate { - headers, rows = rotate(headers, rows) - } - - colWidths := calculateColumnWidths(headers, rows) - - // Create header line - buffer.WriteString("+") - for _, width := range colWidths { - buffer.WriteString(strings.Repeat("-", width+2) + "+") - } - buffer.WriteString("\n|") - for i, header := range headers { - buffer.WriteString(fmt.Sprintf(" %-*s |", colWidths[i], header)) - } - buffer.WriteString("\n+") - for _, width := range colWidths { - buffer.WriteString(strings.Repeat("-", width+2) + "+") - } - buffer.WriteString("\n") - - // Create rows - for _, row := range rows { - buffer.WriteString("|") - for i, col := range row { - buffer.WriteString(fmt.Sprintf(" %-*s |", colWidths[i], col)) - } - buffer.WriteString("\n") - } - buffer.WriteString("+") - for _, width := range colWidths { - buffer.WriteString(strings.Repeat("-", width+2) + "+") - } - buffer.WriteString("\n") - - return buffer.String(), nil + var buffer bytes.Buffer + headers, rows := parseStruct(data) + + if opt.Rotate { + headers, rows = rotate(headers, rows) + } + + colWidths := calculateColumnWidths(headers, rows) + + // Create header line + buffer.WriteString("+") + for _, width := range colWidths { + buffer.WriteString(strings.Repeat("-", width+2) + "+") + } + buffer.WriteString("\n|") + for i, header := range headers { + buffer.WriteString(fmt.Sprintf(" %-*s |", colWidths[i], header)) + } + buffer.WriteString("\n+") + for _, width := range colWidths { + buffer.WriteString(strings.Repeat("-", width+2) + "+") + } + buffer.WriteString("\n") + + // Create rows + for _, row := range rows { + buffer.WriteString("|") + for i, col := range row { + buffer.WriteString(fmt.Sprintf(" %-*s |", colWidths[i], col)) + } + buffer.WriteString("\n") + } + buffer.WriteString("+") + for _, width := range colWidths { + buffer.WriteString(strings.Repeat("-", width+2) + "+") + } + buffer.WriteString("\n") + + return buffer.String(), nil } // RenderMarkdown renders the struct slice as a Markdown table func RenderMarkdown(data interface{}, opt TableOption) (string, error) { - var buffer bytes.Buffer - headers, rows := parseStruct(data) - - if opt.Rotate { - headers, rows = rotate(headers, rows) - } - - colWidths := calculateColumnWidths(headers, rows) - - // Create header line - buffer.WriteString("|") - for i, header := range headers { - buffer.WriteString(fmt.Sprintf(" %-*s |", colWidths[i], header)) - } - buffer.WriteString("\n|") - for i := range headers { - buffer.WriteString(strings.Repeat("-", colWidths[i]+2) + "|") - } - buffer.WriteString("\n") - - // Create rows - for _, row := range rows { - buffer.WriteString("|") - for i, col := range row { - buffer.WriteString(fmt.Sprintf(" %-*s |", colWidths[i], col)) - } - buffer.WriteString("\n") - } - - return buffer.String(), nil + var buffer bytes.Buffer + headers, rows := parseStruct(data) + + if opt.Rotate { + headers, rows = rotate(headers, rows) + } + + colWidths := calculateColumnWidths(headers, rows) + + // Create header line + buffer.WriteString("|") + for i, header := range headers { + buffer.WriteString(fmt.Sprintf(" %-*s |", colWidths[i], header)) + } + buffer.WriteString("\n|") + for i := range headers { + buffer.WriteString(strings.Repeat("-", colWidths[i]+2) + "|") + } + buffer.WriteString("\n") + + // Create rows + for _, row := range rows { + buffer.WriteString("|") + for i, col := range row { + buffer.WriteString(fmt.Sprintf(" %-*s |", colWidths[i], col)) + } + buffer.WriteString("\n") + } + + return buffer.String(), nil } func parseStruct(data interface{}) ([]string, [][]string) { - v := reflect.ValueOf(data) - if v.Kind() != reflect.Slice { - return nil, nil - } - - var headers []string - var rows [][]string - - for i := 0; i < v.Len(); i++ { - elem := v.Index(i) - if elem.Kind() != reflect.Struct { - continue - } - - var row []string - elemType := elem.Type() - for j := 0; j < elem.NumField(); j++ { - if i == 0 { - headers = append(headers, elemType.Field(j).Name) - } - row = append(row, fmt.Sprintf("%v", elem.Field(j).Interface())) - } - rows = append(rows, row) - } - - return headers, rows + v := reflect.ValueOf(data) + if v.Kind() != reflect.Slice { + return nil, nil + } + + var headers []string + var rows [][]string + + for i := 0; i < v.Len(); i++ { + elem := v.Index(i) + if elem.Kind() != reflect.Struct { + continue + } + + var row []string + elemType := elem.Type() + for j := 0; j < elem.NumField(); j++ { + if i == 0 { + headers = append(headers, elemType.Field(j).Name) + } + row = append(row, fmt.Sprintf("%v", elem.Field(j).Interface())) + } + rows = append(rows, row) + } + + return headers, rows } func rotate(headers []string, rows [][]string) ([]string, [][]string) { - newHeaders := make([]string, len(rows)) - for i := range rows { - newHeaders[i] = fmt.Sprintf("Row %d", i+1) - } - - newRows := make([][]string, len(headers)) - for i := range headers { - newRow := make([]string, len(rows)) - for j := range rows { - newRow[j] = rows[j][i] - } - newRows[i] = newRow - } - - // Add original headers to newRows - for i, header := range headers { - newRows[i] = append([]string{header}, newRows[i]...) - } - - // Add an empty column header for the row identifiers - newHeaders = append([]string{""}, newHeaders...) - - return newHeaders, newRows + newHeaders := make([]string, len(rows)) + for i := range rows { + newHeaders[i] = fmt.Sprintf("Row %d", i+1) + } + + newRows := make([][]string, len(headers)) + for i := range headers { + newRow := make([]string, len(rows)) + for j := range rows { + newRow[j] = rows[j][i] + } + newRows[i] = newRow + } + + // Add original headers to newRows + for i, header := range headers { + newRows[i] = append([]string{header}, newRows[i]...) + } + + // Add an empty column header for the row identifiers + newHeaders = append([]string{""}, newHeaders...) + + return newHeaders, newRows } func calculateColumnWidths(headers []string, rows [][]string) []int { - colWidths := make([]int, len(headers)) + colWidths := make([]int, len(headers)) - for i, header := range headers { - colWidths[i] = len(header) - } + for i, header := range headers { + colWidths[i] = len(header) + } - for _, row := range rows { - for i, col := range row { - if len(col) > colWidths[i] { - colWidths[i] = len(col) - } - } - } + for _, row := range rows { + for i, col := range row { + if len(col) > colWidths[i] { + colWidths[i] = len(col) + } + } + } - return colWidths + return colWidths }