1 // HCLTL converts a formatted XLS file into a timeline 2 package main // import "vimagination.zapto.org/hcltl" 3 4 import ( 5 "errors" 6 "fmt" 7 "math" 8 "os" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/shakinm/xlsReader/xls" 14 ) 15 16 type StringRepo struct { 17 strings []string 18 ids map[string]int64 19 } 20 21 func NewStringRepo() StringRepo { 22 return StringRepo{ 23 ids: make(map[string]int64), 24 } 25 } 26 27 func (s *StringRepo) GetID(str string) int64 { 28 if str == "" { 29 return -1 30 } 31 ustr := strings.ToUpper(str) 32 id, ok := s.ids[ustr] 33 if !ok { 34 id = int64(len(s.ids)) 35 s.ids[ustr] = id 36 s.strings = append(s.strings, str) 37 } 38 return id 39 } 40 41 func (s *StringRepo) WriteTo(w *os.File) (int64, error) { 42 var count int64 43 for n, str := range s.strings { 44 if n > 0 { 45 if _, err := w.WriteString(","); err != nil { 46 return count, err 47 } 48 } 49 m, err := fmt.Fprintf(w, "%q", str) 50 count += int64(m) 51 if err != nil { 52 return count, err 53 } 54 } 55 return count, nil 56 } 57 58 func main() { 59 if err := run(); err != nil { 60 fmt.Fprintln(os.Stderr, err) 61 os.Exit(1) 62 } 63 } 64 65 func run() error { 66 if len(os.Args) < 2 { 67 return fmt.Errorf("usage: %s input", os.Args[0]) 68 } 69 users := NewStringRepo() 70 alarms := NewStringRepo() 71 lines := NewStringRepo() 72 reasons := NewStringRepo() 73 wb, err := xls.OpenFile(os.Args[1]) 74 if err != nil { 75 return fmt.Errorf("error reading xls file: %w", err) 76 } 77 ws, err := wb.GetSheet(0) 78 if err != nil { 79 return fmt.Errorf("error getting first sheet: %w", err) 80 } 81 if ws == nil { 82 return errors.New("no worksheets") 83 } 84 header, err := ws.GetRow(0) 85 if err != nil { 86 return fmt.Errorf("error getting first row: %w", err) 87 } 88 if header == nil { 89 return errors.New("no header row") 90 } 91 cols := map[string]int{ 92 "ACCEPTED": -1, 93 "ENDED": -1, 94 "USER": -1, 95 "DATE/TIME": -1, 96 "IN/OUT": -1, 97 "ALARM DESC.": -1, 98 "LINE": -1, 99 "CALL REASON": -1, 100 } 101 var done int 102 for i := 0; i <= 0x4000; i++ { 103 c, err := header.GetCol(i) 104 if err != nil { 105 return fmt.Errorf("error reading column %d: %w", i+1, err) 106 } 107 col := strings.ToUpper(c.GetString()) 108 if n, ok := cols[col]; ok && n == -1 { 109 cols[col] = i 110 done++ 111 } 112 if len(cols) == done { 113 break 114 } 115 } 116 if len(cols) != done { 117 return errors.New("cannot find all required headers") 118 } 119 f, err := os.Create(os.Args[1] + ".html") 120 if err != nil { 121 return fmt.Errorf("error creating output file: %w", err) 122 } 123 first := true 124 if _, err = f.WriteString(start); err != nil { 125 return fmt.Errorf("error writing JS 'start': %w", err) 126 } 127 maxRows := ws.GetNumberRows() 128 for i := 1; i < maxRows; i++ { 129 row, err := ws.GetRow(int(i)) 130 if err != nil { 131 return fmt.Errorf("error reading row %d: %w", i+1, err) 132 } 133 data := make(map[string]string, len(cols)) 134 for k, col := range cols { 135 cell, err := row.GetCol(col) 136 if err != nil { 137 return fmt.Errorf("error reading cell %d,%d: %w", col+1, i+1, err) 138 } 139 d := strings.TrimSpace(cell.GetString()) 140 data[k] = d 141 } 142 userID := users.GetID(data["USER"]) 143 lineID := lines.GetID(data["LINE"]) 144 reasonID := reasons.GetID(data["CALL REASON"]) 145 startTime := parseTime(data["ACCEPTED"]) 146 endTime := parseTime(data["ENDED"]) 147 logTime := parseTime(data["DATE/TIME"]) 148 if userID < 0 || lineID < 0 || reasonID < 0 || startTime == 0 || endTime == 0 || logTime == 0 || endTime < startTime || startTime < logTime { 149 continue 150 } 151 if io := strings.ToUpper(data["IN/OUT"]); io == "OUT" { 152 logTime = 0 153 } else if io == "NONE" { 154 continue 155 } 156 if first { 157 first = false 158 } else { 159 if _, err := f.WriteString(","); err != nil { 160 return fmt.Errorf("error writing seperator: %w", err) 161 } 162 } 163 if _, err := fmt.Fprintf(f, "[%d,%d,%d,%d,%d,%d", userID, startTime, endTime, logTime, lineID, reasonID); err != nil { 164 return fmt.Errorf("error writing row data: %w", err) 165 } 166 if aid := alarms.GetID(data["ALARM DESC."]); aid < 0 { 167 if _, err := fmt.Fprint(f, "]"); err != nil { 168 return fmt.Errorf("error writing close bracker: %w", err) 169 } 170 } else { 171 if _, err := fmt.Fprintf(f, ",%d]", aid); err != nil { 172 return fmt.Errorf("error writing alarm desc: %w", err) 173 } 174 } 175 } 176 if _, err := f.WriteString(mid); err != nil { 177 return fmt.Errorf("error writing JS 'mid' data: %w", err) 178 } 179 if _, err := users.WriteTo(f); err != nil { 180 return fmt.Errorf("error writing user data: %w", err) 181 } 182 if _, err := f.WriteString(mid2); err != nil { 183 return fmt.Errorf("error writing JS 'mid2': %w", err) 184 } 185 if _, err := alarms.WriteTo(f); err != nil { 186 return fmt.Errorf("error writing alarm data: %w", err) 187 } 188 if _, err := f.WriteString(mid3); err != nil { 189 return fmt.Errorf("error writing JS 'mid3': %w", err) 190 } 191 if _, err := lines.WriteTo(f); err != nil { 192 return fmt.Errorf("error writing lines data: %w", err) 193 } 194 if _, err := f.WriteString(mid4); err != nil { 195 return fmt.Errorf("error writing JS 'mid4': %w", err) 196 } 197 if _, err := reasons.WriteTo(f); err != nil { 198 return fmt.Errorf("error writing reasons data: %w", err) 199 } 200 if _, err := f.WriteString(end); err != nil { 201 return fmt.Errorf("error writing JS 'end': %w", err) 202 } 203 return f.Close() 204 } 205 206 func parseTime(v string) int64 { 207 p, err := strconv.ParseFloat(v, 64) 208 if err != nil { 209 return 0 210 } 211 u := int64(math.Round((p - 25569) * 86400)) 212 t := time.Unix(u, 0) 213 y, mo, d := t.Date() 214 h, mi, s := t.Clock() 215 _, offset := time.Date(y, mo, d, h, mi, s, 0, time.Local).Zone() 216 return u - int64(offset) 217 } 218