cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / internal / ui / rules / validation.go
validation.go 8.0K
1package rules23import (4 "errors"5 "strings"67 "src.rest/cat/012/internal/locale"8 "src.rest/cat/012/internal/sheet"9 "src.rest/cat/012/internal/ui/theme"10)1112var errRange = errors.New("Enter the range the rule applies to, e.g. B2:B100")1314// dvKind lists data validation rules.15type dvKind struct{}1617func (dvKind) title() string { return "Data validation rules" }18func (dvKind) count(s *sheet.Sheet) int { return len(s.Validations()) }19func (dvKind) remove(s *sheet.Sheet, i int) { s.DeleteValidation(i) }20func (dvKind) move(*sheet.Sheet, int, int) bool { return false }21func (dvKind) ordered() bool { return false }2223func (dvKind) command(action string) string { return "data.validation_" + action }24func (dvKind) listHint() string {25 return "A cell has one rule: a new rule takes its cells from the others"26}2728// marks are how the list shows each kind of rule, as the cells do.29var marks = map[sheet.ValidKind]string{sheet.ValidList: " ▾ ", sheet.ValidRange: " ▾ ", sheet.ValidCheckbox: "[✓]"}3031func (dvKind) item(th *theme.Theme, h Host, i, w int, sel bool) string {32 v := h.Sheet().Validations()[i]33 base := th.MenuBar34 if sel {35 base = th.MenuSelected36 }37 mark := marks[v.Kind]38 if mark == "" {39 mark = " "40 }41 text := base.Render(" "+mark+" "+sheet.RangesText(v.Ranges)+" ") + base.Render(v.Summary())42 right := ""43 if v.Reject {44 right = th.Muted.Inherit(base).Render("rejects ")45 }46 return spread(base, text, right, w)47}4849// dvForm is a data validation rule being edited.50type dvForm struct {51 i int52 ranges string53 kind int // a sheet.ValidKind54 items string55 source string56 op int // index in the comparisons of the kind; see ops57 args [2]string58 formula string59 reject int // 0 warns, 1 rejects60 help string61 display int // index in sheet.DropDisplays62 checked string // a checkbox's own values, "" for TRUE and FALSE63 unchecked string64 loc *locale.Locale // what args are typed in65}6667func (dvKind) form(h Host, i int, sel sheet.Rect) form {68 f := &dvForm{i: i, ranges: sel.String(), kind: int(sheet.ValidList), loc: h.Sheet().Locale()}69 if i < 0 {70 return f71 }72 v := h.Sheet().Validations()[i]73 f.ranges, f.kind, f.source, f.help = sheet.RangesText(v.Ranges), int(v.Kind), v.Source, v.Help74 f.display = max(indexOf(sheet.DropDisplays(), v.Display), 0)75 if on, off, custom := v.CheckboxValues(); custom {76 f.checked, f.unchecked = on, off77 } else {78 f.items = strings.Join(v.Items, ", ")79 }80 if v.Reject {81 f.reject = 182 }83 switch {84 case v.Kind == sheet.ValidFormula:85 f.formula = sheet.LocalArg(v.Args[0], f.loc)86 case v.Kind.Compares():87 f.op, f.args = max(indexOf(f.ops(), v.Op), 0), localArgs(v.Args, f.loc)88 }89 return f90}9192func (f *dvForm) title() string {93 if f.i < 0 {94 return "Add data validation"95 }96 return "Edit data validation"97}9899// ops are the comparisons the kind offers: dates may be any date.100func (f *dvForm) ops() []sheet.RuleOp {101 if sheet.ValidKind(f.kind) == sheet.ValidDate {102 return append([]sheet.RuleOp{sheet.RuleNone}, sheet.CompareOps()...)103 }104 return sheet.CompareOps()105}106107func displayTitles() []string {108 ds := sheet.DropDisplays()109 out := make([]string, len(ds))110 for i, d := range ds {111 out[i] = d.Title()112 }113 return out114}115116// opTitle names a comparison as the kind reads it.117func opTitle(k sheet.ValidKind, op sheet.RuleOp) string {118 titles := map[sheet.RuleOp]string{sheet.RuleNone: "Is a valid date", sheet.RuleBetween: "Between",119 sheet.RuleNotBetween: "Not between", sheet.RuleEqual: "Equal to", sheet.RuleNotEqual: "Not equal to",120 sheet.RuleGreater: "Greater than", sheet.RuleGreaterEq: "Greater than or equal to",121 sheet.RuleLess: "Less than", sheet.RuleLessEq: "Less than or equal to"}122 if k == sheet.ValidDate {123 titles[sheet.RuleEqual], titles[sheet.RuleNotEqual] = "On", "Not on"124 titles[sheet.RuleGreater], titles[sheet.RuleGreaterEq] = "After", "On or after"125 titles[sheet.RuleLess], titles[sheet.RuleLessEq] = "Before", "On or before"126 }127 return titles[op]128}129130// rule is the rule the form describes.131func (f *dvForm) rule() (sheet.Validation, error) {132 rs, ok := sheet.ParseRanges(f.ranges)133 if !ok && strings.TrimSpace(f.ranges) != "" {134 return sheet.Validation{}, errRange135 }136 k := sheet.ValidKind(f.kind)137 v := sheet.Validation{Ranges: rs, Kind: k, Reject: f.reject == 1, Help: strings.TrimSpace(f.help)}138 if k.Dropdown() {139 v.Display = sheet.DropDisplays()[f.display]140 }141 switch {142 case k == sheet.ValidCheckbox && strings.TrimSpace(f.checked+f.unchecked) != "":143 v.Items = []string{strings.TrimSpace(f.checked)}144 if off := strings.TrimSpace(f.unchecked); off != "" {145 v.Items = append(v.Items, off)146 }147 case k == sheet.ValidList:148 for _, it := range strings.Split(f.items, ",") {149 if it = strings.TrimSpace(it); it != "" {150 v.Items = append(v.Items, it)151 }152 }153 case k == sheet.ValidRange:154 v.Source = strings.TrimSpace(f.source)155 case k == sheet.ValidFormula:156 v.Args[0] = sheet.CanonicalArg(strings.TrimSpace(f.formula), f.loc)157 case k.Compares():158 ops := f.ops()159 v.Op = ops[min(f.op, len(ops)-1)]160 v.Args = canonicalArgs(f.args, f.loc)161 }162 return v, nil163}164165func (f *dvForm) save(h Host) error {166 v, err := f.rule()167 if err != nil {168 return err169 }170 return h.SaveValidation(f.i, v)171}172173func (f *dvForm) rows(*theme.Theme, Host) []row {174 kinds := sheet.ValidKinds()175 titles := make([]string, len(kinds))176 for i, k := range kinds {177 titles[i] = k.Title()178 }179 rs := []row{180 {kind: rowText, label: "Apply to", text: &f.ranges, placeholder: "B2:B100", hint: "The ranges the rule applies to, e.g. B2:B100 or B2:B9,D2:D9"},181 {kind: rowChoice, label: "Criteria", choices: titles, at: &f.kind, hint: "What the cells may hold"},182 }183 rs = append(rs, f.criteriaRows()...)184 return append(rs, row{kind: rowSep},185 row{kind: rowChoice, label: "If invalid", choices: []string{"Show a warning", "Reject the input"}, at: &f.reject,186 hint: "Keep an invalid entry and mark it, or refuse it"},187 row{kind: rowText, label: "Help text", text: &f.help, placeholder: "the rule's own", hint: "What the context line says about the cells; empty for the rule's own words"})188}189190// criteriaRows are the rows of what the criteria need.191func (f *dvForm) criteriaRows() []row {192 k := sheet.ValidKind(f.kind)193 display := row{kind: rowChoice, label: "Display", choices: displayTitles(), at: &f.display,194 hint: "How the cells show their dropdown: the value as a chip, an arrow at the right, or plain text"}195 switch {196 case k == sheet.ValidList:197 return []row{{kind: rowText, label: "Items", text: &f.items, placeholder: "Yes, No, Maybe", hint: "The dropdown's items, separated by commas"}, display}198 case k == sheet.ValidRange:199 return []row{{kind: rowText, label: "From range", text: &f.source, placeholder: "Lists!A1:A20", hint: "The range whose values the dropdown lists"}, display}200 case k == sheet.ValidCheckbox:201 return []row{202 {kind: rowText, label: "Checked", text: &f.checked, placeholder: "TRUE", hint: "What a checked box holds; empty for TRUE"},203 {kind: rowText, label: "Unchecked", text: &f.unchecked, placeholder: "FALSE", hint: "What an unchecked box holds; empty for FALSE, or a blank with a value of its own for checked"},204 }205 case k == sheet.ValidFormula:206 return []row{{kind: rowText, label: "Formula", text: &f.formula, placeholder: "=B2<=C2", hint: "TRUE for valid entries, written for the first cell"}}207 case !k.Compares():208 return nil209 }210 ops := f.ops()211 f.op = min(f.op, len(ops)-1)212 titles := make([]string, len(ops))213 for i, op := range ops {214 titles[i] = opTitle(k, op)215 }216 rs := []row{{kind: rowChoice, label: "Condition", choices: titles, at: &f.op, hint: "How entries compare"}}217 holder := "number"218 if k == sheet.ValidDate {219 holder = "2026-09-30"220 }221 switch ops[f.op].Args() {222 case 1:223 rs = append(rs, row{kind: rowText, label: "Value", text: &f.args[0], placeholder: holder, hint: "The value to compare with, or a formula starting with ="})224 case 2:225 rs = append(rs, row{kind: rowText, label: "Between", text: &f.args[0], placeholder: holder, hint: "The lowest value"},226 row{kind: rowText, label: "And", text: &f.args[1], placeholder: holder, hint: "The highest value"})227 }228 return rs229}