comparison pkg/controllers/fwa.go @ 5259:680be197844d

Merged branch new-fwa.
author Sascha Wilde <wilde@intevation.de>
date Wed, 13 May 2020 11:28:34 +0200
parents 256ebbeb1252
children ed62f138528a
comparison
equal deleted inserted replaced
5183:1b5c80ea5582 5259:680be197844d
1 // This is Free Software under GNU Affero General Public License v >= 3.0
2 // without warranty, see README.md and license for details.
3 //
4 // SPDX-License-Identifier: AGPL-3.0-or-later
5 // License-Filename: LICENSES/AGPL-3.0.txt
6 //
7 // Copyright (C) 2018, 2019, 2020 by via donau
8 // – Österreichische Wasserstraßen-Gesellschaft mbH
9 // Software engineering by Intevation GmbH
10 //
11 // Author(s):
12 // * Sascha L. Teichmann <sascha.teichmann@intevation.de>
13
14 package controllers
15
16 import (
17 "context"
18 "database/sql"
19 "encoding/csv"
20 "fmt"
21 "log"
22 "net/http"
23 "sort"
24 "strconv"
25 "strings"
26 "time"
27
28 "github.com/gorilla/mux"
29
30 "gemma.intevation.de/gemma/pkg/common"
31 "gemma.intevation.de/gemma/pkg/middleware"
32 )
33
34 const (
35 selectBottlenecksLimitingSQL = `
36 SELECT
37 lower(validity),
38 upper(validity),
39 limiting
40 FROM
41 waterway.bottlenecks
42 WHERE
43 bottleneck_id = $1 AND
44 validity && tstzrange($2, $3)`
45
46 selectSymbolBottlenecksSQL = `
47 SELECT
48 distinct(b.bottleneck_id)
49 FROM
50 %s s, waterway.bottlenecks b
51 WHERE
52 ST_Intersects(b.area, s.area)
53 AND s.name = $1
54 AND b.validity && tstzrange($2, $3)`
55
56 selectLDCsSQL = `
57 SELECT
58 lower(grwl.validity),
59 upper(grwl.validity),
60 grwl.value
61 FROM
62 waterway.gauges_reference_water_levels grwl
63 JOIN waterway.bottlenecks bns
64 ON grwl.location = bns.gauge_location
65 WHERE
66 grwl.depth_reference like 'LDC%'
67 AND bns.bottleneck_id = $1
68 AND grwl.validity && tstzrange($2, $3)`
69
70 selectMeasurementsSQL = `
71 WITH data AS (
72 SELECT
73 efa.measure_date,
74 efa.available_depth_value,
75 efa.available_width_value,
76 efa.water_level_value
77 FROM waterway.effective_fairway_availability efa
78 JOIN waterway.fairway_availability fa
79 ON efa.fairway_availability_id = fa.id
80 JOIN waterway.bottlenecks bn
81 ON fa.bottleneck_id = bn.bottleneck_id
82 WHERE
83 bn.validity @> efa.measure_date AND
84 bn.bottleneck_id = $1 AND
85 efa.level_of_service = $2 AND
86 efa.measure_type = 'Measured' AND
87 (efa.available_depth_value IS NOT NULL OR
88 efa.available_width_value IS NOT NULL) AND
89 efa.water_level_value IS NOT NULL
90 ),
91 before AS (
92 SELECT * FROM data WHERE measure_date < $3
93 ORDER BY measure_date DESC LIMIT 1
94 ),
95 inside AS (
96 SELECT * FROM data WHERE measure_date BETWEEN $3 AND $4
97 ),
98 after AS (
99 SELECT * FROM data WHERE measure_date > $4
100 ORDER BY measure_date LIMIT 1
101 )
102 SELECT * FROM before
103 UNION ALL
104 SELECT * FROM inside
105 UNION ALL
106 SELECT * FROM after
107 ORDER BY measure_date`
108 )
109
110 type (
111 timeRange struct {
112 lower time.Time
113 upper time.Time
114 }
115
116 ldc struct {
117 timeRange
118 value []float64
119 }
120
121 ldcs []*ldc
122
123 limitingFactor int
124
125 limitingValidity struct {
126 timeRange
127 limiting limitingFactor
128 ldcs ldcs
129 }
130
131 limitingValidities []limitingValidity
132
133 availMeasurement struct {
134 when time.Time
135 depth int16
136 width int16
137 value int16
138 }
139
140 availMeasurements []availMeasurement
141
142 bottleneck struct {
143 id string
144 validities limitingValidities
145 measurements availMeasurements
146 }
147
148 bottlenecks []bottleneck
149
150 fwaMode int
151 )
152
153 const (
154 fwaMonthly fwaMode = iota
155 fwaQuarterly
156 fwaYearly
157 )
158
159 const (
160 limitingDepth limitingFactor = iota
161 limitingWidth
162 )
163
164 var limitingAccess = [...]func(*availMeasurement) float64{
165 limitingDepth: (*availMeasurement).getDepth,
166 limitingWidth: (*availMeasurement).getWidth,
167 }
168
169 // afdRefs are the typical available fairway depth reference values.
170 var afdRefs = []float64{
171 230,
172 250,
173 }
174
175 func (ls ldcs) find(from, to time.Time) *ldc {
176 for _, l := range ls {
177 if l.intersects(from, to) {
178 return l
179 }
180 }
181 return nil
182 }
183
184 func fairwayAvailability(rw http.ResponseWriter, req *http.Request) {
185
186 from, to, ok := parseFromTo(rw, req)
187 if !ok {
188 return
189 }
190
191 vars := mux.Vars(req)
192 name := vars["name"]
193 if name == "" {
194 http.Error(rw, "missing 'name' parameter.", http.StatusBadRequest)
195 return
196 }
197
198 los, ok := parseFormInt(rw, req, "los", 3)
199 if !ok {
200 return
201 }
202
203 ctx := req.Context()
204 conn := middleware.GetDBConn(req)
205
206 var bns bottlenecks
207 var err error
208
209 switch vars["kind"] {
210 case "bottleneck":
211 bns = bottlenecks{{id: name}}
212 case "stretch":
213 bns, err = loadSymbolBottlenecks(ctx, conn, "users.stretches", name, from, to)
214 case "section":
215 bns, err = loadSymbolBottlenecks(ctx, conn, "waterway.sections", name, from, to)
216 default:
217 http.Error(rw, "Invalid kind type.", http.StatusBadRequest)
218 return
219 }
220
221 if err != nil {
222 log.Printf("error: %v\n", err)
223 http.Error(rw, "cannot extract bottlenecks", http.StatusBadRequest)
224 return
225 }
226
227 // If there are no bottlenecks there is nothing to do.
228 if len(bns) == 0 {
229 http.Error(rw, "No bottlenecks found.", http.StatusNotFound)
230 return
231 }
232
233 // load validities and limiting factors
234 for i := range bns {
235 if err := bns[i].loadLimitingValidities(ctx, conn, from, to); err != nil {
236 log.Printf("error: %v\n", err)
237 http.Error(rw, "cannot load validities", http.StatusInternalServerError)
238 return
239 }
240 // load LCDs
241 if err := bns[i].loadLDCs(ctx, conn, from, to); err != nil {
242 log.Printf("error: %v\n", err)
243 http.Error(rw, "cannot load LDCs", http.StatusInternalServerError)
244 return
245 }
246 // load values
247 if err := bns[i].loadValues(ctx, conn, from, to, los); err != nil {
248 log.Printf("error: %v\n", err)
249 http.Error(rw, "cannot load values", http.StatusInternalServerError)
250 return
251 }
252 }
253
254 // separate breaks for depth and width
255 var (
256 breaks = parseBreaks(req.FormValue("breaks"), afdRefs)
257 depthBreaks = parseBreaks(req.FormValue("depthbreaks"), breaks)
258 widthBreaks = parseBreaks(req.FormValue("widthbreaks"), breaks)
259 chooseBreaks = [...][]float64{
260 limitingDepth: depthBreaks,
261 limitingWidth: widthBreaks,
262 }
263
264 useDepth = bns.hasLimiting(limitingDepth, from, to)
265 useWidth = bns.hasLimiting(limitingWidth, from, to)
266 )
267
268 if useDepth && useWidth && len(widthBreaks) != len(depthBreaks) {
269 http.Error(
270 rw,
271 fmt.Sprintf("class breaks lengths differ: %d != %d",
272 len(widthBreaks), len(depthBreaks)),
273 http.StatusBadRequest,
274 )
275 return
276 }
277
278 availability := vars["type"] == "availability"
279
280 var record []string
281 if !availability {
282 // in days
283 record = makeHeader(useDepth && useWidth, 1, breaks, 'd')
284 } else {
285 // percentage
286 record = makeHeader(useDepth && useWidth, 3, breaks, '%')
287 }
288
289 rw.Header().Add("Content-Type", "text/csv")
290
291 out := csv.NewWriter(rw)
292
293 if err := out.Write(record); err != nil {
294 // Too late for HTTP status message.
295 log.Printf("error: %v\n", err)
296 return
297 }
298
299 for i := range record[1:] {
300 record[i+1] = "0"
301 }
302
303 // For every day on every bottleneck we need to find out if this day is valid.
304 validities := make([]func(time.Time, time.Time) *limitingValidity, len(bns))
305 for i := range bns {
306 validities[i] = bns[i].validities.find()
307 }
308
309 // Mode reflects if we use monthly, quarterly od yearly intervals.
310 mode := parseFWAMode(req.FormValue("mode"))
311
312 label, finish := interval(mode, from)
313
314 var (
315 totalDays, overLDCDays int
316 missingLDCs = make([]int, len(validities))
317 counters = make([]int, len(breaks)+1)
318 )
319
320 var current, next time.Time
321
322 write := func() error {
323 record[0] = label(current)
324
325 if !availability {
326 record[1] = strconv.Itoa(totalDays - overLDCDays)
327 record[2] = strconv.Itoa(overLDCDays)
328 for i, c := range counters {
329 record[3+i] = strconv.Itoa(c)
330 }
331 } else {
332 overPerc := float64(overLDCDays) * 100 / float64(totalDays)
333 record[1] = fmt.Sprintf("%.3f", 100-overPerc)
334 record[2] = fmt.Sprintf("%.3f", overPerc)
335 for i, c := range counters {
336 perc := float64(c) * 100 / float64(totalDays)
337 record[3+i] = fmt.Sprintf("%.3f", perc)
338 }
339 }
340
341 return out.Write(record)
342 }
343
344 // Stop yesterday
345 end := common.MinTime(dusk(time.Now()).Add(-time.Nanosecond), to)
346
347 // We step through the time in steps of one day.
348 for current = from; current.Before(end); {
349
350 next = current.AddDate(0, 0, 1)
351
352 // Assume that a bottleneck is over LDC.
353 overLDC := true
354 lowest := len(counters) - 1
355
356 var hasValid bool
357
358 // check all bottlenecks
359 for i, validity := range validities {
360
361 // Check if bottleneck is available for this day.
362 vs := validity(current, next)
363 if vs == nil {
364 continue
365 }
366
367 // Let's see if we have a LDC for this day.
368 ldc := vs.ldcs.find(current, next)
369 if ldc == nil {
370 missingLDCs[i]++
371 continue
372 }
373
374 hasValid = true
375
376 if overLDC { // If its already not shipable we need no further tests.
377 result := bns[i].measurements.classify(
378 current, next,
379 ldc.value,
380 (*availMeasurement).getValue)
381
382 if result[1] < 12*time.Hour {
383 overLDC = false
384 }
385 }
386
387 if min := minClass(bns[i].measurements.classify(
388 current, next,
389 chooseBreaks[vs.limiting],
390 limitingAccess[vs.limiting]),
391 12*time.Hour,
392 ); min < lowest {
393 lowest = min
394 }
395 }
396
397 if hasValid {
398 if overLDC {
399 overLDCDays++
400 }
401 counters[lowest]++
402 } else { // assume that all is in best conditions
403 overLDCDays++
404 counters[len(counters)-1]++
405 }
406
407 totalDays++
408
409 if finish(next) {
410 if err := write(); err != nil {
411 // Too late for HTTP status message.
412 log.Printf("error: %v\n", err)
413 return
414 }
415
416 // Reset counters
417 overLDCDays, totalDays = 0, 0
418 for i := range counters {
419 counters[i] = 0
420 }
421 }
422
423 current = next
424 }
425
426 // Write rest if last period was not finished.
427 if totalDays > 0 {
428 if err := write(); err != nil {
429 // Too late for HTTP status message.
430 log.Printf("error: %v\n", err)
431 return
432 }
433 }
434
435 // TODO: Log missing LDCs
436
437 out.Flush()
438 if err := out.Error(); err != nil {
439 // Too late for HTTP status message.
440 log.Printf("error: %v\n", err)
441 }
442 }
443
444 func minClass(classes []time.Duration, threshold time.Duration) int {
445 var sum time.Duration
446 for i, v := range classes {
447 if sum += v; sum >= threshold {
448 return i
449 }
450 }
451 return len(classes) - 1
452 }
453
454 func dusk(t time.Time) time.Time {
455 return time.Date(
456 t.Year(),
457 t.Month(),
458 t.Day(),
459 0, 0, 0, 0,
460 t.Location())
461 }
462
463 func dawn(t time.Time) time.Time {
464 return time.Date(
465 t.Year(),
466 t.Month(),
467 t.Day(),
468 23, 59, 59, 999999999,
469 t.Location())
470 }
471
472 func parseFromTo(
473 rw http.ResponseWriter,
474 req *http.Request,
475 ) (time.Time, time.Time, bool) {
476 from, ok := parseFormTime(rw, req, "from", time.Now().AddDate(-1, 0, 0))
477 if !ok {
478 return time.Time{}, time.Time{}, false
479 }
480
481 to, ok := parseFormTime(rw, req, "to", from.AddDate(1, 0, 0))
482 if !ok {
483 return time.Time{}, time.Time{}, false
484 }
485
486 from, to = common.OrderTime(from, to)
487 // Operate on daily basis so go to full days.
488 return dusk(from), dawn(to), true
489 }
490
491 func parseFWAMode(mode string) fwaMode {
492 switch strings.ToLower(mode) {
493 case "monthly":
494 return fwaMonthly
495 case "quarterly":
496 return fwaQuarterly
497 case "yearly":
498 return fwaYearly
499 default:
500 return fwaMonthly
501 }
502 }
503
504 func breaksToReferenceValue(breaks string) []float64 {
505 parts := strings.Split(breaks, ",")
506 var values []float64
507
508 for _, part := range parts {
509 part = strings.TrimSpace(part)
510 if v, err := strconv.ParseFloat(part, 64); err == nil {
511 values = append(values, v)
512 }
513 }
514
515 return common.DedupFloat64s(values)
516 }
517
518 func parseBreaks(breaks string, defaults []float64) []float64 {
519 if breaks != "" {
520 return breaksToReferenceValue(breaks)
521 }
522 return defaults
523 }
524
525 func (tr *timeRange) intersects(from, to time.Time) bool {
526 return !(to.Before(tr.lower) || from.After(tr.upper))
527 }
528
529 func (tr *timeRange) toUTC() {
530 tr.lower = tr.lower.UTC()
531 tr.upper = tr.upper.UTC()
532 }
533
534 func (lvs limitingValidities) find() func(from, to time.Time) *limitingValidity {
535
536 var last *limitingValidity
537
538 return func(from, to time.Time) *limitingValidity {
539 if last != nil && last.intersects(from, to) {
540 return last
541 }
542 for i := range lvs {
543 if lv := &lvs[i]; lv.intersects(from, to) {
544 last = lv
545 return lv
546 }
547 }
548 return nil
549 }
550 }
551
552 func (lvs limitingValidities) hasLimiting(limiting limitingFactor, from, to time.Time) bool {
553 for i := range lvs {
554 if lvs[i].limiting == limiting && lvs[i].intersects(from, to) {
555 return true
556 }
557 }
558 return false
559 }
560
561 func (bns bottlenecks) hasLimiting(limiting limitingFactor, from, to time.Time) bool {
562 for i := range bns {
563 if bns[i].validities.hasLimiting(limiting, from, to) {
564 return true
565 }
566 }
567 return false
568 }
569
570 func parseLimitingFactor(limiting string) limitingFactor {
571 switch limiting {
572 case "depth":
573 return limitingDepth
574 case "width":
575 return limitingWidth
576 default:
577 log.Printf("warn: unknown limitation '%s'. default to 'depth'\n", limiting)
578 return limitingDepth
579 }
580 }
581
582 func loadLimitingValidities(
583 ctx context.Context,
584 conn *sql.Conn,
585 bottleneckID string,
586 from, to time.Time,
587 ) (limitingValidities, error) {
588
589 var lvs limitingValidities
590
591 rows, err := conn.QueryContext(
592 ctx,
593 selectBottlenecksLimitingSQL,
594 bottleneckID,
595 from, to)
596
597 if err != nil {
598 return nil, err
599 }
600 defer rows.Close()
601
602 for rows.Next() {
603 var (
604 lv limitingValidity
605 limiting string
606 upper sql.NullTime
607 )
608 if err := rows.Scan(
609 &lv.lower,
610 &upper,
611 &limiting,
612 ); err != nil {
613 return nil, err
614 }
615 if upper.Valid {
616 lv.upper = upper.Time
617 } else {
618 lv.upper = to.Add(24 * time.Hour)
619 }
620 lv.toUTC()
621 lv.limiting = parseLimitingFactor(limiting)
622 lvs = append(lvs, lv)
623 }
624
625 return lvs, rows.Err()
626 }
627
628 func loadSymbolBottlenecks(
629 ctx context.Context,
630 conn *sql.Conn,
631 what, name string,
632 from, to time.Time,
633 ) (bottlenecks, error) {
634
635 rows, err := conn.QueryContext(
636 ctx,
637 fmt.Sprintf(selectSymbolBottlenecksSQL, what),
638 name,
639 from, to)
640 if err != nil {
641 return nil, err
642 }
643 defer rows.Close()
644
645 var bns bottlenecks
646
647 for rows.Next() {
648 var b bottleneck
649 if err := rows.Scan(&b.id); err != nil {
650 return nil, err
651 }
652 bns = append(bns, b)
653 }
654
655 return bns, rows.Err()
656 }
657
658 func (bn *bottleneck) loadLimitingValidities(
659 ctx context.Context,
660 conn *sql.Conn,
661 from, to time.Time,
662 ) error {
663 vs, err := loadLimitingValidities(
664 ctx,
665 conn,
666 bn.id,
667 from, to)
668 if err == nil {
669 bn.validities = vs
670 }
671 return err
672 }
673
674 func (bn *bottleneck) loadLDCs(
675 ctx context.Context,
676 conn *sql.Conn,
677 from, to time.Time,
678 ) error {
679 rows, err := conn.QueryContext(
680 ctx, selectLDCsSQL,
681 bn.id,
682 from, to)
683 if err != nil {
684 return err
685 }
686 defer rows.Close()
687 for rows.Next() {
688 l := ldc{value: []float64{0}}
689 var upper sql.NullTime
690 if err := rows.Scan(&l.lower, &upper, &l.value[0]); err != nil {
691 return err
692 }
693 if upper.Valid {
694 l.upper = upper.Time
695 } else {
696 l.upper = to.Add(24 * time.Hour)
697 }
698 l.toUTC()
699 for i := range bn.validities {
700 vs := &bn.validities[i]
701
702 if vs.intersects(l.lower, l.upper) {
703 vs.ldcs = append(vs.ldcs, &l)
704 }
705 }
706 }
707 return rows.Err()
708 }
709
710 func (bn *bottleneck) loadValues(
711 ctx context.Context,
712 conn *sql.Conn,
713 from, to time.Time,
714 los int,
715 ) error {
716 rows, err := conn.QueryContext(
717 ctx, selectMeasurementsSQL,
718 bn.id,
719 los,
720 from, to)
721 if err != nil {
722 return err
723 }
724 defer rows.Close()
725
726 var ms availMeasurements
727
728 for rows.Next() {
729 var m availMeasurement
730 if err := rows.Scan(
731 &m.when,
732 &m.depth,
733 &m.width,
734 &m.value,
735 ); err != nil {
736 return err
737 }
738 m.when = m.when.UTC()
739 ms = append(ms, m)
740 }
741 if err := rows.Err(); err != nil {
742 return err
743 }
744 bn.measurements = ms
745 return nil
746 }
747
748 func (measurement *availMeasurement) getDepth() float64 {
749 return float64(measurement.depth)
750 }
751
752 func (measurement *availMeasurement) getValue() float64 {
753 return float64(measurement.value)
754 }
755
756 func (measurement *availMeasurement) getWidth() float64 {
757 return float64(measurement.width)
758 }
759
760 func (measurements availMeasurements) classify(
761 from, to time.Time,
762 breaks []float64,
763 access func(*availMeasurement) float64,
764 ) []time.Duration {
765
766 if len(breaks) == 0 {
767 return []time.Duration{}
768 }
769
770 result := make([]time.Duration, len(breaks)+1)
771 classes := make([]float64, len(breaks)+2)
772 values := make([]time.Time, len(classes))
773
774 // Add sentinels
775 classes[0] = breaks[0] - 9999
776 classes[len(classes)-1] = breaks[len(breaks)-1] + 9999
777 for i := range breaks {
778 classes[i+1] = breaks[i]
779 }
780
781 idx := sort.Search(len(measurements), func(i int) bool {
782 // All values before from can be ignored.
783 return !measurements[i].when.Before(from)
784 })
785
786 if idx >= len(measurements) {
787 return result
788 }
789
790 // Be safe for interpolation.
791 if idx > 0 {
792 idx--
793 }
794
795 measurements = measurements[idx:]
796
797 for i := 0; i < len(measurements)-1; i++ {
798 p1 := &measurements[i]
799 p2 := &measurements[i+1]
800
801 if p1.when.After(to) {
802 return result
803 }
804
805 if p2.when.Before(from) {
806 continue
807 }
808
809 // TODO: Discuss if we want somethinh like this.
810 if false && p2.when.Sub(p1.when).Hours() > 1.5 {
811 // Don't interpolate ranges bigger then one and a half hour
812 continue
813 }
814
815 lo, hi := common.MaxTime(p1.when, from), common.MinTime(p2.when, to)
816
817 m1, m2 := access(p1), access(p2)
818 if m1 == m2 { // The whole interval is in only one class.
819 for j := 0; j < len(classes)-1; j++ {
820 if classes[j] <= m1 && m1 <= classes[j+1] {
821 result[j] += hi.Sub(lo)
822 break
823 }
824 }
825 continue
826 }
827
828 f := common.InterpolateTime(
829 p1.when, m1,
830 p2.when, m2,
831 )
832
833 for j, c := range classes {
834 values[j] = f(c)
835 }
836
837 for j := 0; j < len(values)-1; j++ {
838 start, end := common.OrderTime(values[j], values[j+1])
839
840 if start.After(hi) || end.Before(lo) {
841 continue
842 }
843
844 start, end = common.MaxTime(start, lo), common.MinTime(end, hi)
845 result[j] += end.Sub(start)
846 }
847 }
848
849 return result
850 }
851
852 func interval(mode fwaMode, t time.Time) (
853 label func(time.Time) string,
854 finish func(time.Time) bool,
855 ) {
856 switch mode {
857 case fwaMonthly:
858 label, finish = monthLabel, otherMonth(t)
859 case fwaQuarterly:
860 label, finish = quarterLabel, otherQuarter(t)
861 case fwaYearly:
862 label, finish = yearLabel, otherYear(t)
863 default:
864 panic("Unknown mode")
865 }
866 return
867 }
868
869 func monthLabel(t time.Time) string {
870 return fmt.Sprintf("%02d-%d", t.Month(), t.Year())
871 }
872
873 func quarterLabel(t time.Time) string {
874 return fmt.Sprintf("Q%d-%d", (int(t.Month())-1)/3+1, t.Year())
875 }
876
877 func yearLabel(t time.Time) string {
878 return strconv.Itoa(t.Year())
879 }
880
881 func otherMonth(t time.Time) func(time.Time) bool {
882 return func(x time.Time) bool {
883 flag := t.Day() == x.Day()
884 if flag {
885 t = x
886 }
887 return flag
888 }
889 }
890
891 func otherQuarter(t time.Time) func(time.Time) bool {
892 return func(x time.Time) bool {
893 flag := (t.Month()-1)/3 != (x.Month()-1)/3
894 if flag {
895 t = x
896 }
897 return flag
898 }
899 }
900
901 func otherYear(t time.Time) func(time.Time) bool {
902 return func(x time.Time) bool {
903 flag := t.Year() != x.Year()
904 if flag {
905 t = x
906 }
907 return flag
908 }
909 }
910
911 func makeHeader(flag bool, prec int, breaks []float64, unit rune) []string {
912 record := make([]string, 1+2+len(breaks)+1)
913 record[0] = "# time"
914 record[1] = fmt.Sprintf("# < LDC [%c]", unit)
915 record[2] = fmt.Sprintf("# >= LDC [%c]", unit)
916 for i, v := range breaks {
917 if flag {
918 if i == 0 {
919 record[3] = fmt.Sprintf("# < break_1 [%c]", unit)
920 }
921 record[i+4] = fmt.Sprintf("# >= break_%d [%c]", i+1, unit)
922 } else {
923 if i == 0 {
924 record[3] = fmt.Sprintf("# < %.*f [%c]", prec, v, unit)
925 }
926 record[i+4] = fmt.Sprintf("# >= %.*f [%c]", prec, v, unit)
927 }
928 }
929 return record
930 }