mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-06-09 15:07:49 +00:00
init add function command for create seed depretitaion standard
This commit is contained in:
@@ -0,0 +1,563 @@
|
||||
// Command seed-house-depreciation-standards membaca kurva depresiasi per-day
|
||||
// dari file Excel untuk SATU project flock LAYING, lalu meng-generate file
|
||||
// migration {up,down}.sql yang menyisipkan baris house_depreciation_standards
|
||||
// khusus flock tersebut.
|
||||
//
|
||||
// Hanya multiplication_percentage yang di-override; house_type & standard_week
|
||||
// diwarisi dari baris global (project_flock_id IS NULL) untuk hari yang sama,
|
||||
// dan depreciation_percent diturunkan = (1 - multiplication_percentage) * 100.
|
||||
//
|
||||
// Jalankan lokal: dry-run mencetak SQL ke stdout, -apply menulis file migration
|
||||
// untuk di-review & di-commit. Tidak ada API yang di-hit di production — data
|
||||
// masuk lewat `make migrate-up` saat deploy.
|
||||
//
|
||||
// go run ./cmd/seed-house-depreciation-standards -project-flock-id=52 -file=curve.xlsx # dry-run
|
||||
// go run ./cmd/seed-house-depreciation-standards -project-flock-id=52 -file=curve.xlsx -apply # tulis migration
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/database"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
dateLayout = "2006-01-02"
|
||||
timestampLayout = "20060102150405"
|
||||
defaultMigrions = "internal/database/migrations"
|
||||
headerDay = "day"
|
||||
headerMultiplier = "multiplication_percentage"
|
||||
)
|
||||
|
||||
type options struct {
|
||||
ProjectFlockID uint
|
||||
FilePath string
|
||||
Sheet string
|
||||
EffectiveDate string
|
||||
HouseType string
|
||||
OutDir string
|
||||
Apply bool
|
||||
}
|
||||
|
||||
type curveRow struct {
|
||||
RowNumber int
|
||||
Day int
|
||||
Mult float64
|
||||
}
|
||||
|
||||
type validationIssue struct {
|
||||
Row int
|
||||
Field string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (i validationIssue) String() string {
|
||||
if i.Row > 0 {
|
||||
return fmt.Sprintf("row=%d field=%s message=%s", i.Row, i.Field, i.Message)
|
||||
}
|
||||
return fmt.Sprintf("field=%s message=%s", i.Field, i.Message)
|
||||
}
|
||||
|
||||
func main() {
|
||||
var opts options
|
||||
flag.UintVar(&opts.ProjectFlockID, "project-flock-id", 0, "Target LAYING project_flock id (required)")
|
||||
flag.StringVar(&opts.FilePath, "file", "", "Path to .xlsx file with columns day, multiplication_percentage (required)")
|
||||
flag.StringVar(&opts.Sheet, "sheet", "", "Sheet name (optional, default: first sheet)")
|
||||
flag.StringVar(&opts.EffectiveDate, "effective-date", "", "effective_date for the seeded rows (YYYY-MM-DD, default: today)")
|
||||
flag.StringVar(&opts.HouseType, "house-type", "", "Override house_type (open_house|close_house). Default: derived from the flock's kandangs")
|
||||
flag.StringVar(&opts.OutDir, "out-dir", defaultMigrions, "Directory to write the migration files")
|
||||
flag.BoolVar(&opts.Apply, "apply", false, "Write the migration files. If false, print SQL to stdout (dry-run)")
|
||||
flag.Parse()
|
||||
|
||||
opts.FilePath = strings.TrimSpace(opts.FilePath)
|
||||
opts.Sheet = strings.TrimSpace(opts.Sheet)
|
||||
opts.OutDir = strings.TrimSpace(opts.OutDir)
|
||||
|
||||
if opts.ProjectFlockID == 0 {
|
||||
log.Fatal("--project-flock-id is required and must be greater than 0")
|
||||
}
|
||||
if opts.FilePath == "" {
|
||||
log.Fatal("--file is required")
|
||||
}
|
||||
|
||||
location, err := time.LoadLocation("Asia/Jakarta")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to load timezone Asia/Jakarta: %v", err)
|
||||
}
|
||||
|
||||
effectiveDate, err := resolveEffectiveDate(opts.EffectiveDate, location)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid --effective-date: %v", err)
|
||||
}
|
||||
opts.EffectiveDate = effectiveDate.Format(dateLayout)
|
||||
|
||||
sheetName, curve, parseIssues, err := parseCurveFile(opts.FilePath, opts.Sheet)
|
||||
if err != nil {
|
||||
log.Fatalf("failed reading excel: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
db := database.Connect(config.DBHost, config.DBName)
|
||||
|
||||
if err := assertActiveLayingFlock(ctx, db, opts.ProjectFlockID); err != nil {
|
||||
log.Fatalf("project_flock validation failed: %v", err)
|
||||
}
|
||||
|
||||
houseType, err := resolveHouseType(ctx, db, opts.ProjectFlockID, opts.HouseType)
|
||||
if err != nil {
|
||||
log.Fatalf("house_type resolution failed: %v", err)
|
||||
}
|
||||
|
||||
issues := append([]validationIssue{}, parseIssues...)
|
||||
if len(curve) > 0 {
|
||||
globalDays, err := globalDaysForHouseType(ctx, db, houseType)
|
||||
if err != nil {
|
||||
log.Fatalf("failed loading global standard days: %v", err)
|
||||
}
|
||||
issues = append(issues, buildDayCoverageIssues(curve, globalDays, houseType)...)
|
||||
}
|
||||
sortValidationIssues(issues)
|
||||
|
||||
fmt.Printf("Mode: %s\n", modeLabel(opts.Apply))
|
||||
fmt.Printf("File: %s\n", opts.FilePath)
|
||||
fmt.Printf("Sheet: %s\n", sheetName)
|
||||
fmt.Printf("Project flock id: %d\n", opts.ProjectFlockID)
|
||||
fmt.Printf("House type: %s\n", houseType)
|
||||
fmt.Printf("Effective date: %s\n", opts.EffectiveDate)
|
||||
fmt.Printf("Curve rows parsed: %d\n", len(curve))
|
||||
if len(curve) > 0 {
|
||||
minDay, maxDay := dayRange(curve)
|
||||
fmt.Printf("Day range: %d..%d\n", minDay, maxDay)
|
||||
}
|
||||
fmt.Printf("Validation errors: %d\n", len(issues))
|
||||
fmt.Println()
|
||||
|
||||
if len(issues) > 0 {
|
||||
fmt.Println("Validation errors:")
|
||||
for _, issue := range issues {
|
||||
fmt.Printf("ERROR %s\n", issue.String())
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
upSQL := buildUpSQL(opts, houseType, curve)
|
||||
downSQL := buildDownSQL(opts)
|
||||
|
||||
prefix := time.Now().In(location).Format(timestampLayout)
|
||||
upName := fmt.Sprintf("%s_seed_house_depreciation_flock_%d.up.sql", prefix, opts.ProjectFlockID)
|
||||
downName := fmt.Sprintf("%s_seed_house_depreciation_flock_%d.down.sql", prefix, opts.ProjectFlockID)
|
||||
|
||||
if !opts.Apply {
|
||||
fmt.Printf("--- %s ---\n%s\n", upName, upSQL)
|
||||
fmt.Printf("--- %s ---\n%s\n", downName, downSQL)
|
||||
fmt.Printf("Dry-run: would write 2 files to %s. Re-run with -apply to create them.\n", opts.OutDir)
|
||||
return
|
||||
}
|
||||
|
||||
upPath := filepath.Join(opts.OutDir, upName)
|
||||
downPath := filepath.Join(opts.OutDir, downName)
|
||||
if err := os.WriteFile(upPath, []byte(upSQL), 0o644); err != nil {
|
||||
log.Fatalf("failed writing %s: %v", upPath, err)
|
||||
}
|
||||
if err := os.WriteFile(downPath, []byte(downSQL), 0o644); err != nil {
|
||||
log.Fatalf("failed writing %s: %v", downPath, err)
|
||||
}
|
||||
|
||||
fmt.Printf("WROTE %s\n", upPath)
|
||||
fmt.Printf("WROTE %s\n", downPath)
|
||||
fmt.Println("Review the SQL, commit it, then deploy runs `make migrate-up`.")
|
||||
}
|
||||
|
||||
func resolveEffectiveDate(raw string, location *time.Location) (time.Time, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
now := time.Now().In(location)
|
||||
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, location), nil
|
||||
}
|
||||
parsed, err := time.ParseInLocation(dateLayout, raw, location)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("must follow format YYYY-MM-DD")
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func parseCurveFile(filePath, requestedSheet string) (string, []curveRow, []validationIssue, error) {
|
||||
workbook, err := excelize.OpenFile(filePath)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
defer func() { _ = workbook.Close() }()
|
||||
|
||||
sheetName, err := resolveSheetName(workbook, requestedSheet)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
allRows, err := workbook.GetRows(sheetName, excelize.Options{RawCellValue: true})
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
if len(allRows) == 0 {
|
||||
return sheetName, nil, []validationIssue{{Field: "header", Message: "sheet is empty"}}, nil
|
||||
}
|
||||
|
||||
dayIdx, multIdx, headerIssues := parseHeaderIndexes(allRows[0])
|
||||
if len(headerIssues) > 0 {
|
||||
return sheetName, nil, headerIssues, nil
|
||||
}
|
||||
|
||||
rows := make([]curveRow, 0, len(allRows)-1)
|
||||
issues := make([]validationIssue, 0)
|
||||
seenDays := make(map[int]int)
|
||||
|
||||
for idx := 1; idx < len(allRows); idx++ {
|
||||
rowNumber := idx + 1
|
||||
rawRow := allRows[idx]
|
||||
if isRowEmpty(rawRow) {
|
||||
continue
|
||||
}
|
||||
|
||||
parsed, rowIssues := parseCurveRow(rawRow, rowNumber, dayIdx, multIdx, seenDays)
|
||||
if len(rowIssues) > 0 {
|
||||
issues = append(issues, rowIssues...)
|
||||
continue
|
||||
}
|
||||
rows = append(rows, *parsed)
|
||||
}
|
||||
|
||||
if len(rows) == 0 && len(issues) == 0 {
|
||||
issues = append(issues, validationIssue{Field: "rows", Message: "no data rows found"})
|
||||
}
|
||||
|
||||
sort.Slice(rows, func(i, j int) bool { return rows[i].Day < rows[j].Day })
|
||||
return sheetName, rows, issues, nil
|
||||
}
|
||||
|
||||
func resolveSheetName(workbook *excelize.File, requestedSheet string) (string, error) {
|
||||
sheets := workbook.GetSheetList()
|
||||
if len(sheets) == 0 {
|
||||
return "", fmt.Errorf("workbook has no sheets")
|
||||
}
|
||||
if strings.TrimSpace(requestedSheet) == "" {
|
||||
return sheets[0], nil
|
||||
}
|
||||
for _, sheet := range sheets {
|
||||
if strings.EqualFold(strings.TrimSpace(sheet), strings.TrimSpace(requestedSheet)) {
|
||||
return sheet, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("sheet %q not found", requestedSheet)
|
||||
}
|
||||
|
||||
func parseHeaderIndexes(headerRow []string) (int, int, []validationIssue) {
|
||||
dayIdx, multIdx := -1, -1
|
||||
issues := make([]validationIssue, 0)
|
||||
|
||||
for idx, raw := range headerRow {
|
||||
switch normalizeHeader(raw) {
|
||||
case headerDay:
|
||||
if dayIdx >= 0 {
|
||||
issues = append(issues, validationIssue{Field: "header", Message: "duplicate header day"})
|
||||
}
|
||||
dayIdx = idx
|
||||
case headerMultiplier:
|
||||
if multIdx >= 0 {
|
||||
issues = append(issues, validationIssue{Field: "header", Message: "duplicate header multiplication_percentage"})
|
||||
}
|
||||
multIdx = idx
|
||||
}
|
||||
}
|
||||
|
||||
if dayIdx < 0 {
|
||||
issues = append(issues, validationIssue{Field: headerDay, Message: "required header is missing"})
|
||||
}
|
||||
if multIdx < 0 {
|
||||
issues = append(issues, validationIssue{Field: headerMultiplier, Message: "required header is missing"})
|
||||
}
|
||||
return dayIdx, multIdx, issues
|
||||
}
|
||||
|
||||
func parseCurveRow(rawRow []string, rowNumber, dayIdx, multIdx int, seenDays map[int]int) (*curveRow, []validationIssue) {
|
||||
issues := make([]validationIssue, 0)
|
||||
|
||||
day, err := parsePositiveInt(strings.TrimSpace(cellValue(rawRow, dayIdx)))
|
||||
if err != nil {
|
||||
issues = append(issues, validationIssue{Row: rowNumber, Field: headerDay, Message: err.Error()})
|
||||
}
|
||||
|
||||
mult, err := parseMultiplication(strings.TrimSpace(cellValue(rawRow, multIdx)))
|
||||
if err != nil {
|
||||
issues = append(issues, validationIssue{Row: rowNumber, Field: headerMultiplier, Message: err.Error()})
|
||||
}
|
||||
|
||||
if day > 0 {
|
||||
if prev, exists := seenDays[day]; exists {
|
||||
issues = append(issues, validationIssue{Row: rowNumber, Field: headerDay, Message: fmt.Sprintf("duplicate day %d (already used in row %d)", day, prev)})
|
||||
} else {
|
||||
seenDays[day] = rowNumber
|
||||
}
|
||||
}
|
||||
|
||||
if len(issues) > 0 {
|
||||
return nil, issues
|
||||
}
|
||||
return &curveRow{RowNumber: rowNumber, Day: day, Mult: mult}, nil
|
||||
}
|
||||
|
||||
func parsePositiveInt(raw string) (int, error) {
|
||||
if raw == "" {
|
||||
return 0, fmt.Errorf("is required")
|
||||
}
|
||||
value, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
floatValue, floatErr := strconv.ParseFloat(raw, 64)
|
||||
if floatErr != nil || floatValue != float64(int(floatValue)) {
|
||||
return 0, fmt.Errorf("must be a positive integer")
|
||||
}
|
||||
value = int(floatValue)
|
||||
}
|
||||
if value < 1 {
|
||||
return 0, fmt.Errorf("must be greater than or equal to 1")
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func parseMultiplication(raw string) (float64, error) {
|
||||
if raw == "" {
|
||||
return 0, fmt.Errorf("is required")
|
||||
}
|
||||
value, err := strconv.ParseFloat(raw, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("must be numeric")
|
||||
}
|
||||
if value <= 0 || value > 1 {
|
||||
return 0, fmt.Errorf("must be greater than 0 and at most 1")
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func buildDayCoverageIssues(curve []curveRow, globalDays map[int]bool, houseType string) []validationIssue {
|
||||
issues := make([]validationIssue, 0)
|
||||
for _, row := range curve {
|
||||
if !globalDays[row.Day] {
|
||||
issues = append(issues, validationIssue{
|
||||
Row: row.RowNumber,
|
||||
Field: headerDay,
|
||||
Message: fmt.Sprintf("day %d has no global standard row for house_type=%s (cannot inherit standard_week)", row.Day, houseType),
|
||||
})
|
||||
}
|
||||
}
|
||||
return issues
|
||||
}
|
||||
|
||||
func buildUpSQL(opts options, houseType string, curve []curveRow) string {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "-- Kurva depresiasi khusus project_flock_id=%d (house_type=%s, effective_date=%s).\n", opts.ProjectFlockID, houseType, opts.EffectiveDate)
|
||||
b.WriteString("-- Override hanya multiplication_percentage; house_type & standard_week diwarisi dari baris global.\n")
|
||||
b.WriteString("-- depreciation_percent diturunkan = (1 - multiplication_percentage) * 100.\n")
|
||||
b.WriteString("INSERT INTO house_depreciation_standards\n")
|
||||
b.WriteString(" (project_flock_id, house_type, day, effective_date,\n")
|
||||
b.WriteString(" multiplication_percentage, depreciation_percent, standard_week, name)\n")
|
||||
b.WriteString("SELECT\n")
|
||||
fmt.Fprintf(&b, " %d, g.house_type, g.day, DATE '%s',\n", opts.ProjectFlockID, opts.EffectiveDate)
|
||||
b.WriteString(" v.mult, (1 - v.mult) * 100, g.standard_week,\n")
|
||||
fmt.Fprintf(&b, " 'Custom depreciation flock %d (eff %s)'\n", opts.ProjectFlockID, opts.EffectiveDate)
|
||||
b.WriteString("FROM (VALUES\n")
|
||||
b.WriteString(formatValuesTuples(curve))
|
||||
b.WriteString("\n) AS v(day, mult)\n")
|
||||
b.WriteString("JOIN LATERAL (\n")
|
||||
b.WriteString(" SELECT DISTINCT ON (day) house_type, day, standard_week\n")
|
||||
b.WriteString(" FROM house_depreciation_standards\n")
|
||||
b.WriteString(" WHERE project_flock_id IS NULL\n")
|
||||
fmt.Fprintf(&b, " AND house_type = '%s'::house_type_enum\n", houseType)
|
||||
b.WriteString(" AND day = v.day\n")
|
||||
b.WriteString(" ORDER BY day, effective_date DESC NULLS LAST\n")
|
||||
b.WriteString(") g ON TRUE;\n\n")
|
||||
b.WriteString("-- Recompute snapshot depresiasi flock ini dengan kurva baru.\n")
|
||||
fmt.Fprintf(&b, "DELETE FROM farm_depreciation_snapshots WHERE project_flock_id = %d;\n", opts.ProjectFlockID)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// formatValuesTuples renders the (day, mult) tuples 5 per line. The first tuple's
|
||||
// multiplier is cast ::numeric so PostgreSQL types the whole VALUES column as
|
||||
// numeric even if every value happens to render without a decimal point.
|
||||
func formatValuesTuples(curve []curveRow) string {
|
||||
tuples := make([]string, len(curve))
|
||||
for i, row := range curve {
|
||||
mult := formatFloat(row.Mult)
|
||||
if i == 0 {
|
||||
mult += "::numeric"
|
||||
}
|
||||
tuples[i] = fmt.Sprintf("(%d, %s)", row.Day, mult)
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
const perLine = 5
|
||||
for i := 0; i < len(tuples); i += perLine {
|
||||
end := i + perLine
|
||||
if end > len(tuples) {
|
||||
end = len(tuples)
|
||||
}
|
||||
b.WriteString(" ")
|
||||
b.WriteString(strings.Join(tuples[i:end], ", "))
|
||||
if end < len(tuples) {
|
||||
b.WriteString(",")
|
||||
}
|
||||
if end < len(tuples) {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func buildDownSQL(opts options) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("DELETE FROM house_depreciation_standards\n")
|
||||
fmt.Fprintf(&b, "WHERE project_flock_id = %d AND effective_date = DATE '%s';\n\n", opts.ProjectFlockID, opts.EffectiveDate)
|
||||
fmt.Fprintf(&b, "DELETE FROM farm_depreciation_snapshots WHERE project_flock_id = %d;\n", opts.ProjectFlockID)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func formatFloat(value float64) string {
|
||||
return strconv.FormatFloat(value, 'g', -1, 64)
|
||||
}
|
||||
|
||||
func assertActiveLayingFlock(ctx context.Context, db *gorm.DB, projectFlockID uint) error {
|
||||
var count int64
|
||||
err := db.WithContext(ctx).
|
||||
Table("project_flocks").
|
||||
Where("id = ?", projectFlockID).
|
||||
Where("deleted_at IS NULL").
|
||||
Where("category = ?", string(utils.ProjectFlockCategoryLaying)).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return fmt.Errorf("project_flock_id %d must reference an active LAYING project_flock", projectFlockID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveHouseType(ctx context.Context, db *gorm.DB, projectFlockID uint, override string) (string, error) {
|
||||
if strings.TrimSpace(override) != "" {
|
||||
normalized, err := normalizeHouseType(override)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
houseTypes := make([]string, 0)
|
||||
err := db.WithContext(ctx).Raw(`
|
||||
SELECT DISTINCT k.house_type::text AS house_type
|
||||
FROM project_flock_kandangs pfk
|
||||
JOIN kandangs k ON k.id = pfk.kandang_id
|
||||
WHERE pfk.project_flock_id = ? AND k.house_type IS NOT NULL
|
||||
ORDER BY house_type
|
||||
`, projectFlockID).Scan(&houseTypes).Error
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch len(houseTypes) {
|
||||
case 0:
|
||||
return "", fmt.Errorf("flock %d has no kandang house_type set; pass -house-type explicitly", projectFlockID)
|
||||
case 1:
|
||||
return houseTypes[0], nil
|
||||
default:
|
||||
return "", fmt.Errorf("flock %d spans multiple house_types %v; pass -house-type explicitly", projectFlockID, houseTypes)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeHouseType(raw string) (string, error) {
|
||||
normalized := strings.ToLower(strings.TrimSpace(raw))
|
||||
switch normalized {
|
||||
case "open_house", "close_house":
|
||||
return normalized, nil
|
||||
default:
|
||||
return "", fmt.Errorf("house_type %q must be open_house or close_house", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func globalDaysForHouseType(ctx context.Context, db *gorm.DB, houseType string) (map[int]bool, error) {
|
||||
days := make([]int, 0)
|
||||
err := db.WithContext(ctx).Raw(`
|
||||
SELECT DISTINCT day
|
||||
FROM house_depreciation_standards
|
||||
WHERE project_flock_id IS NULL AND house_type = ?::house_type_enum
|
||||
`, houseType).Scan(&days).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
set := make(map[int]bool, len(days))
|
||||
for _, day := range days {
|
||||
set[day] = true
|
||||
}
|
||||
return set, nil
|
||||
}
|
||||
|
||||
func dayRange(curve []curveRow) (int, int) {
|
||||
minDay, maxDay := curve[0].Day, curve[0].Day
|
||||
for _, row := range curve {
|
||||
if row.Day < minDay {
|
||||
minDay = row.Day
|
||||
}
|
||||
if row.Day > maxDay {
|
||||
maxDay = row.Day
|
||||
}
|
||||
}
|
||||
return minDay, maxDay
|
||||
}
|
||||
|
||||
func sortValidationIssues(issues []validationIssue) {
|
||||
sort.Slice(issues, func(i, j int) bool {
|
||||
if issues[i].Row == issues[j].Row {
|
||||
if issues[i].Field == issues[j].Field {
|
||||
return issues[i].Message < issues[j].Message
|
||||
}
|
||||
return issues[i].Field < issues[j].Field
|
||||
}
|
||||
return issues[i].Row < issues[j].Row
|
||||
})
|
||||
}
|
||||
|
||||
func isRowEmpty(row []string) bool {
|
||||
for _, cell := range row {
|
||||
if strings.TrimSpace(cell) != "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func normalizeHeader(raw string) string {
|
||||
return strings.ToLower(strings.TrimSpace(raw))
|
||||
}
|
||||
|
||||
func cellValue(row []string, index int) string {
|
||||
if index < 0 || index >= len(row) {
|
||||
return ""
|
||||
}
|
||||
return row[index]
|
||||
}
|
||||
|
||||
func modeLabel(apply bool) string {
|
||||
if apply {
|
||||
return "APPLY"
|
||||
}
|
||||
return "DRY-RUN"
|
||||
}
|
||||
Reference in New Issue
Block a user