diff --git a/cmd/cdrtool/main.go b/cmd/cdrtool/main.go index c4c5eaa..ba850c8 100644 --- a/cmd/cdrtool/main.go +++ b/cmd/cdrtool/main.go @@ -1,6 +1,7 @@ package main import ( + "encoding/csv" "flag" "fmt" "log" @@ -99,14 +100,20 @@ func main() { } } if *export != "" { + toExport := make([][]string, 0) + toExport = append(toExport, cdr.ExportLineHeader) + sc := cdr.NewSortableCalls(calls) sort.Sort(sc) callCost := 0 callPrice := 0 for _, c := range sc.Calls { - fmt.Printf("%s\n", c.String()) + if *debug { + fmt.Printf("%s\n", c.String()) + } callCost += c.Cost callPrice += c.Price + toExport = append(toExport, cdr.ExportCall(c)) } st := cdr.NewSortableTexts(texts) @@ -114,9 +121,12 @@ func main() { textCost := 0 textPrice := 0 for _, t := range st.Texts { - fmt.Printf("%s\n", t.String()) + if *debug { + fmt.Printf("%s\n", t.String()) + } textCost += t.PricedLine.Cost textPrice += t.PricedLine.Price + toExport = append(toExport, cdr.ExportText(t)) } sd := cdr.NewSortableData(data) @@ -124,14 +134,32 @@ func main() { dataCost := 0 dataPrice := 0 for _, d := range sd.Data { - fmt.Printf("%s\n", d.String()) + if *debug { + fmt.Printf("%s\n", d.String()) + } dataCost += d.PricedLine.Cost dataPrice += d.PricedLine.Price + toExport = append(toExport, cdr.ExportData(d)) } fmt.Printf("callCost: %.4f, callPrice: %.4f\n", float64(callCost)/10000.0, float64(callPrice)/10000.0) fmt.Printf("textCost: %.4f, textPrice: %.4f\n", float64(textCost)/10000.0, float64(textPrice)/10000.0) fmt.Printf("dataCost: %.4f, dataPrice: %.4f\n", float64(dataCost)/10000.0, float64(dataPrice)/10000.0) + + _, err = os.Stat(*export) + if !os.IsNotExist(err) { + log.Fatalf("refusing to write to existing file %q for exporting", *export) + } + csvExportFile, err := os.Create(*export) + if err != nil { + log.Fatalf("cannot create export file %q: %v", *export, err) + } + csvWriter := csv.NewWriter(csvExportFile) + err = csvWriter.WriteAll(toExport) + if err != nil { + log.Fatalf("cannot export csv to %q: %v", *export, err) + } + log.Printf("exported priced CDRs to %q", *export) } } } diff --git a/combine.go b/combine.go index 4b7b912..c50a25f 100644 --- a/combine.go +++ b/combine.go @@ -38,11 +38,12 @@ func CombineTextCDRs(cdrLines []Line, prices []Price) ([]Text, error) { continue } ret[i] = Text{ - PricedLine{ + PricedLine: PricedLine{ Line: l, Cost: TEXT_OUTGOING_BUY, Price: TEXT_OUTGOING_SELL, }, + Account: l.Account, } i++ } @@ -103,7 +104,8 @@ func CombineDataCDRs(cdrLines []Line, prices []Price) ([]Data, error) { Cost: int(dataCost / 1000.0), // compensate for cost specified per MB Price: int(dataPrice / 1000.0), // compensate for price specified per MB }, - Bytes: l.RawCount, + Account: l.Account, + Kilobytes: l.RawCount, } i++ } @@ -193,6 +195,7 @@ func CombineCallCDRs(cdrLines []Line, prices []Price) ([]Call, error) { */ callCost := 0.0 callPrice := 0.0 + rawCost := 0 pricedLegs := make([]PricedLine, len(v)) for i, leg := range v { legPrice := Price{} @@ -247,10 +250,14 @@ func CombineCallCDRs(cdrLines []Line, prices []Price) ([]Call, error) { pricedLeg.Line = leg pricedLeg.Cost = int(math.Round(thisLegCost)) pricedLeg.Price = int(math.Round(thisLegPrice)) + if legPrice.OurRef != "" { + pricedLeg.OurRef = legPrice.OurRef + } pricedLegs[i] = pricedLeg callCost += thisLegCost callPrice += thisLegPrice + rawCost += leg.RawCosts if DEBUG { fmt.Printf("(%12s( %12s)->%12s) leg %d, callCost: %.4f, callPrice: %.4f (dst: %q)\n", leg.CLI, leg.From, leg.To, leg.Leg, callCost/10000.0, callPrice/10000.0, legPrice.OurRef) } @@ -259,11 +266,12 @@ func CombineCallCDRs(cdrLines []Line, prices []Price) ([]Call, error) { //fmt.Printf("(%12s->%12s) leg %d, price: %+v\n", leg.CLI, leg.To, leg.Leg, legPrice) } callDuration, _ := time.ParseDuration(fmt.Sprintf("%ds", v[0].RawCount)) - var callFrom, callTo string + var callFrom, callTo, callDescription string ret[i] = Call{ Duration: callDuration, Time: v[0].Time, + RawCost: rawCost, Cost: int(math.Round(callCost)), Price: int(math.Round(callPrice)), Legs: pricedLegs, @@ -272,11 +280,26 @@ func CombineCallCDRs(cdrLines []Line, prices []Price) ([]Call, error) { // CLI == From, then this leg's CLI is our "From" field, and this leg's "To" likewise callFrom = v[0].CLI callTo = v[0].To + if pricedLegs[0].OurRef == "" { + strippedDestination := strings.TrimSpace(strings.TrimSuffix(strings.TrimSuffix(pricedLegs[0].Line.Destination, " - Proper"), " - Mobile")) + callDescription = strippedDestination + } else { + callDescription = pricedLegs[0].OurRef + } } else { // We capture the "To" from the leg that has no source, we capture the "From" from the leg that has both source and destination for _, leg := range v { if leg.Source == "" && leg.Destination != "" { callTo = leg.To + strippedDestination := strings.TrimSpace(strings.TrimSuffix(strings.TrimSuffix(leg.Destination, " - Proper"), " - Mobile")) + switch strippedDestination { + case "Netherlands - Mobile": + callDescription = "Nederland (mobiel)" + case "Netherlands - Fixed": + callDescription = "Nederland (vast)" + default: + callDescription = strippedDestination + } } else if leg.Source != "" && leg.Destination != "" { callFrom = leg.From } else { @@ -287,6 +310,8 @@ func CombineCallCDRs(cdrLines []Line, prices []Price) ([]Call, error) { } ret[i].From = callFrom ret[i].To = callTo + ret[i].Account = v[0].Account + ret[i].Description = callDescription //fmt.Printf(" ==> %+v\n\n", ret[i]) //ret[i].Legs = make([]PricedLine, len(v)) diff --git a/export_csv.go b/export_csv.go new file mode 100644 index 0000000..ac0d35d --- /dev/null +++ b/export_csv.go @@ -0,0 +1,71 @@ +package cdr + +import ( + "fmt" +) + +type ExportLineIndex int + +var ( + ExportLineSize = 9 + ExportLineTypeCall = "CALL" + ExportLineTypeText = "TEXT" + ExportLineTypeData = "DATA" + ExportLineHeader = []string{ + "Type", "Timestamp", "From", "To", "Units", "Account", "Cost", "Price", "Description", + } +) + +const ( + ExportLineType ExportLineIndex = iota + ExportLineTimestamp + ExportLineFrom + ExportLineTo + ExportLineUnits + ExportLineAccount + ExportLineCost + ExportLinePrice + ExportLineDescription +) + +func ExportCall(c Call) []string { + ret := make([]string, ExportLineSize) + ret[ExportLineType] = ExportLineTypeCall + ret[ExportLineTimestamp] = c.Time.Format(DateTimeFormat) + ret[ExportLineFrom] = c.From + ret[ExportLineTo] = c.To + ret[ExportLineUnits] = fmt.Sprintf("%.0f", c.Duration.Seconds()) + ret[ExportLineAccount] = c.Account + ret[ExportLineCost] = fmt.Sprintf("%d", c.Cost) + ret[ExportLinePrice] = fmt.Sprintf("%d", c.Price) + ret[ExportLineDescription] = c.Description + return ret +} + +func ExportText(t Text) []string { + ret := make([]string, ExportLineSize) + ret[ExportLineType] = ExportLineTypeText + ret[ExportLineTimestamp] = t.Time.Format(DateTimeFormat) + ret[ExportLineFrom] = t.From + ret[ExportLineTo] = t.To + ret[ExportLineUnits] = "1" + ret[ExportLineAccount] = t.Account + ret[ExportLineCost] = fmt.Sprintf("%d", t.Cost) + ret[ExportLinePrice] = fmt.Sprintf("%d", t.Price) + ret[ExportLineDescription] = "SMS" + return ret +} + +func ExportData(d Data) []string { + ret := make([]string, ExportLineSize) + ret[ExportLineType] = ExportLineTypeData + ret[ExportLineTimestamp] = d.Time.Format(DateTimeFormat) + ret[ExportLineFrom] = "" + ret[ExportLineTo] = "" + ret[ExportLineUnits] = fmt.Sprintf("%d", d.Kilobytes) + ret[ExportLineAccount] = d.Account + ret[ExportLineCost] = fmt.Sprintf("%d", d.PricedLine.Cost) + ret[ExportLinePrice] = fmt.Sprintf("%d", d.PricedLine.Price) + ret[ExportLineDescription] = "Data" + return ret +} diff --git a/record.go b/record.go index 642f560..f7a484c 100644 --- a/record.go +++ b/record.go @@ -121,29 +121,38 @@ type Line struct { type PricedLine struct { Line - Cost int - Price int + Cost int + Price int + OurRef string } // Call is a record of one or more PricedLines that ultimately form "a call". type Call struct { - From string - To string - Time time.Time - Duration time.Duration - Cost int - Price int - Kind CallKind - Legs []PricedLine + From string + To string + Time time.Time + Duration time.Duration + RawCost int + Cost int + Price int + Kind CallKind + Account string + Description string + Legs []PricedLine } func (c *Call) String() string { - return fmt.Sprintf("%s (%12s->%12s) callCost: %.4f, callPrice: %.4f (dur: %v)", c.Time.Format(DateTimeFormat), c.From, c.To, float64(c.Cost)/10000.0, float64(c.Price)/10000.0, c.Duration) + diff := float64(c.RawCost-c.Cost) / 10000.0 + return fmt.Sprintf("%s (%d) (%12s->%12s) cost: %.4f (%.4f=>%+.4f), price: %.4f (dur: %v): %v", + c.Time.Format(DateTimeFormat), len(c.Legs), c.From, c.To, + float64(c.Cost)/10000.0, float64(c.RawCost)/10000.0, diff, float64(c.Price)/10000.0, c.Duration, c.Description) } // Text is a specific PricedLine for a text message. type Text struct { PricedLine + + Account string } func (t *Text) String() string { @@ -154,9 +163,10 @@ func (t *Text) String() string { type Data struct { PricedLine - Bytes int + Account string + Kilobytes int } func (d *Data) String() string { - return fmt.Sprintf("%s (%v bytes) dataCost: %.4f, dataPrice: %.4f", d.PricedLine.Line.Time.Format(DateTimeFormat), d.Bytes, float64(d.PricedLine.Cost)/10000.0, float64(d.PricedLine.Price)/10000.0) + return fmt.Sprintf("%s (%s) %v kilobytes, cost: %.4f, price: %.4f", d.PricedLine.Line.Time.Format(DateTimeFormat), d.Account, d.Kilobytes, float64(d.PricedLine.Cost)/10000.0, float64(d.PricedLine.Price)/10000.0) }