diff --git a/kernel/av/av.go b/kernel/av/av.go index 7f3f2f0e9..5c370aeef 100644 --- a/kernel/av/av.go +++ b/kernel/av/av.go @@ -40,10 +40,9 @@ type AttributeView struct { Columns []*Column `json:"columns"` // 表格列名 Rows []*Row `json:"rows"` // 表格行记录 - Type AttributeViewType `json:"type"` // 属性视图类型 - Filters []*AttributeViewFilter `json:"filters"` // 过滤规则 - Sorts []*AttributeViewSort `json:"sorts"` // 排序规则 - Calculates []*AttributeViewCalc `json:"calculates"` // 计算规则 + Type AttributeViewType `json:"type"` // 属性视图类型 + Filters []*AttributeViewFilter `json:"filters"` // 过滤规则 + Sorts []*AttributeViewSort `json:"sorts"` // 排序规则 } // AttributeViewType 描述了属性视图的类型。 @@ -305,91 +304,3 @@ func (av *AttributeView) FilterRows() { } av.Rows = rows } - -type AttributeViewCalc struct { - Column string `json:"column"` - Operator CalcOperator `json:"operator"` - Value *Value `json:"value"` -} - -type CalcOperator string - -const ( - CalcOperatorNone CalcOperator = "" - CalcOperatorCountAll CalcOperator = "Count all" - CalcOperatorCountValues CalcOperator = "Count values" - CalcOperatorCountUniqueValues CalcOperator = "Count unique values" - CalcOperatorCountEmpty CalcOperator = "Count empty" - CalcOperatorCountNotEmpty CalcOperator = "Count not empty" - CalcOperatorPercentEmpty CalcOperator = "Percent empty" - CalcOperatorPercentNotEmpty CalcOperator = "Percent not empty" - CalcOperatorSum CalcOperator = "Sum" - CalcOperatorAverage CalcOperator = "Average" - CalcOperatorMedian CalcOperator = "Median" - CalcOperatorMin CalcOperator = "Min" - CalcOperatorMax CalcOperator = "Max" - CalcOperatorRange CalcOperator = "Range" - CalcOperatorEarliest CalcOperator = "Earliest" - CalcOperatorLatest CalcOperator = "Latest" -) - -func (av *AttributeView) CalcCols() { - if 1 > len(av.Calculates) { - return - } - - var colIndexes []int - for _, c := range av.Calculates { - for i, col := range av.Columns { - if col.ID == c.Column { - colIndexes = append(colIndexes, i) - break - } - } - } - - var colValues []*Value - for _, row := range av.Rows { - for _, index := range colIndexes { - colValues = append(colValues, row.Cells[index].Value) - } - } - - for i, c := range av.Calculates { - switch c.Operator { - case CalcOperatorCountAll: - av.Calculates[i].Value = &Value{Number: &ValueNumber{Content: float64(len(colValues))}} - case CalcOperatorCountValues: - var countValues int - for _, v := range colValues { - if v.Number.IsNotEmpty { - countValues++ - } - } - - av.Calculates[i].Value = &Value{Number: &ValueNumber{Content: float64(countValues)}} - case CalcOperatorCountUniqueValues: - var countUniqueValues int - uniqueValues := map[float64]bool{} - for _, v := range colValues { - if v.Number.IsNotEmpty { - if !uniqueValues[v.Number.Content] { - countUniqueValues++ - uniqueValues[v.Number.Content] = true - } - } - } - - av.Calculates[i].Value = &Value{Number: &ValueNumber{Content: float64(countUniqueValues)}} - case CalcOperatorCountEmpty: - var countEmpty int - for _, v := range colValues { - if !v.Number.IsNotEmpty { - countEmpty++ - } - } - - av.Calculates[i].Value = &Value{Number: &ValueNumber{Content: float64(countEmpty)}} - } - } -} diff --git a/kernel/av/column.go b/kernel/av/column.go index 1afb7c0ec..178829168 100644 --- a/kernel/av/column.go +++ b/kernel/av/column.go @@ -16,7 +16,11 @@ package av -import "github.com/88250/lute/ast" +import ( + "github.com/88250/lute/ast" + "math" + "sort" +) type ColumnType string @@ -33,13 +37,14 @@ const ( // Column 描述了属性视图的基础结构。 type Column struct { - ID string `json:"id"` // 列 ID - Name string `json:"name"` // 列名 - Type ColumnType `json:"type"` // 列类型 - Icon string `json:"icon"` // 列图标 - Wrap bool `json:"wrap"` // 是否换行 - Hidden bool `json:"hidden"` // 是否隐藏 - Width string `json:"width"` // 列宽度 + ID string `json:"id"` // 列 ID + Name string `json:"name"` // 列名 + Type ColumnType `json:"type"` // 列类型 + Icon string `json:"icon"` // 列图标 + Wrap bool `json:"wrap"` // 是否换行 + Hidden bool `json:"hidden"` // 是否隐藏 + Width string `json:"width"` // 列宽度 + Calc *ColumnCalc `json:"calc"` // 计算 // 以下是某些列类型的特有属性 @@ -60,3 +65,414 @@ func NewColumn(name string, columnType ColumnType) *Column { Type: columnType, } } + +type ColumnCalc struct { + Column string `json:"column"` + Operator CalcOperator `json:"operator"` + Result *ColumnCalcResult `json:"result"` +} + +type ColumnCalcResult struct { + Number *ValueNumber `json:"number"` + Date *ValueDate `json:"date"` +} + +type CalcOperator string + +const ( + CalcOperatorNone CalcOperator = "" + CalcOperatorCountAll CalcOperator = "Count all" + CalcOperatorCountValues CalcOperator = "Count values" + CalcOperatorCountUniqueValues CalcOperator = "Count unique values" + CalcOperatorCountEmpty CalcOperator = "Count empty" + CalcOperatorCountNotEmpty CalcOperator = "Count not empty" + CalcOperatorPercentEmpty CalcOperator = "Percent empty" + CalcOperatorPercentNotEmpty CalcOperator = "Percent not empty" + CalcOperatorSum CalcOperator = "Sum" + CalcOperatorAverage CalcOperator = "Average" + CalcOperatorMedian CalcOperator = "Median" + CalcOperatorMin CalcOperator = "Min" + CalcOperatorMax CalcOperator = "Max" + CalcOperatorRange CalcOperator = "Range" + CalcOperatorEarliest CalcOperator = "Earliest" + CalcOperatorLatest CalcOperator = "Latest" +) + +func (av *AttributeView) CalcCols() { + for i, col := range av.Columns { + if nil == col.Calc { + continue + } + + if CalcOperatorNone == col.Calc.Operator { + continue + } + + switch col.Type { + case ColumnTypeText: + av.calcColText(col, i) + case ColumnTypeNumber: + av.calcColNumber(col, i) + case ColumnTypeDate: + av.calcColDate(col, i) + case ColumnTypeSelect: + av.calcColSelect(col, i) + } + + } +} + +func (av *AttributeView) calcColSelect(col *Column, colIndex int) { + switch col.Calc.Operator { + case CalcOperatorCountAll: + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(len(av.Rows))}} + case CalcOperatorCountValues: + countValues := 0 + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Select && "" != row.Cells[colIndex].Value.Select.Content { + countValues++ + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countValues)}} + case CalcOperatorCountUniqueValues: + countUniqueValues := 0 + uniqueValues := map[string]bool{} + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Select && "" != row.Cells[colIndex].Value.Select.Content { + uniqueValues[row.Cells[colIndex].Value.Select.Content] = true + countUniqueValues++ + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countUniqueValues)}} + case CalcOperatorCountEmpty: + countEmpty := 0 + for _, row := range av.Rows { + if nil == row.Cells[colIndex] || nil == row.Cells[colIndex].Value || nil == row.Cells[colIndex].Value.Select || "" == row.Cells[colIndex].Value.Select.Content { + countEmpty++ + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countEmpty)}} + case CalcOperatorCountNotEmpty: + countNotEmpty := 0 + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Select && "" != row.Cells[colIndex].Value.Select.Content { + countNotEmpty++ + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countNotEmpty)}} + case CalcOperatorPercentEmpty: + countEmpty := 0 + for _, row := range av.Rows { + if nil == row.Cells[colIndex] || nil == row.Cells[colIndex].Value || nil == row.Cells[colIndex].Value.Select || "" == row.Cells[colIndex].Value.Select.Content { + countEmpty++ + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countEmpty) / float64(len(av.Rows))}} + case CalcOperatorPercentNotEmpty: + countNotEmpty := 0 + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Select && "" != row.Cells[colIndex].Value.Select.Content { + countNotEmpty++ + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countNotEmpty) / float64(len(av.Rows))}} + } +} + +func (av *AttributeView) calcColDate(col *Column, colIndex int) { + switch col.Calc.Operator { + case CalcOperatorCountAll: + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(len(av.Rows))}} + case CalcOperatorCountValues: + countValues := 0 + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Date && 0 != row.Cells[colIndex].Value.Date.Content { + countValues++ + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countValues)}} + case CalcOperatorCountUniqueValues: + countUniqueValues := 0 + uniqueValues := map[int64]bool{} + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Date && 0 != row.Cells[colIndex].Value.Date.Content { + if _, ok := uniqueValues[row.Cells[colIndex].Value.Date.Content]; !ok { + countUniqueValues++ + uniqueValues[row.Cells[colIndex].Value.Date.Content] = true + } + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countUniqueValues)}} + case CalcOperatorCountEmpty: + countEmpty := 0 + for _, row := range av.Rows { + if nil == row.Cells[colIndex] || nil == row.Cells[colIndex].Value || nil == row.Cells[colIndex].Value.Date || 0 == row.Cells[colIndex].Value.Date.Content { + countEmpty++ + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countEmpty)}} + case CalcOperatorCountNotEmpty: + countNotEmpty := 0 + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Date && 0 != row.Cells[colIndex].Value.Date.Content { + countNotEmpty++ + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countNotEmpty)}} + case CalcOperatorPercentEmpty: + countEmpty := 0 + for _, row := range av.Rows { + if nil == row.Cells[colIndex] || nil == row.Cells[colIndex].Value || nil == row.Cells[colIndex].Value.Date || 0 == row.Cells[colIndex].Value.Date.Content { + countEmpty++ + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countEmpty) / float64(len(av.Rows))}} + case CalcOperatorPercentNotEmpty: + countNotEmpty := 0 + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Date && 0 != row.Cells[colIndex].Value.Date.Content { + countNotEmpty++ + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countNotEmpty) / float64(len(av.Rows))}} + case CalcOperatorEarliest: + earliest := int64(0) + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Date && 0 != row.Cells[colIndex].Value.Date.Content { + if 0 == earliest || earliest > row.Cells[colIndex].Value.Date.Content { + earliest = row.Cells[colIndex].Value.Date.Content + } + } + } + if 0 != earliest { + col.Calc.Result = &ColumnCalcResult{Date: &ValueDate{Content: earliest}} + } + case CalcOperatorLatest: + latest := int64(0) + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Date && 0 != row.Cells[colIndex].Value.Date.Content { + if 0 == latest || latest < row.Cells[colIndex].Value.Date.Content { + latest = row.Cells[colIndex].Value.Date.Content + } + } + } + if 0 != latest { + col.Calc.Result = &ColumnCalcResult{Date: &ValueDate{Content: latest}} + } + case CalcOperatorRange: + earliest := int64(0) + latest := int64(0) + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Date && 0 != row.Cells[colIndex].Value.Date.Content { + if 0 == earliest || earliest > row.Cells[colIndex].Value.Date.Content { + earliest = row.Cells[colIndex].Value.Date.Content + } + if 0 == latest || latest < row.Cells[colIndex].Value.Date.Content { + latest = row.Cells[colIndex].Value.Date.Content + } + } + } + if 0 != earliest && 0 != latest { + col.Calc.Result = &ColumnCalcResult{Date: &ValueDate{Content: latest - earliest}} + } + } +} + +func (av *AttributeView) calcColNumber(col *Column, colIndex int) { + switch col.Calc.Operator { + case CalcOperatorCountAll: + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(len(av.Rows))}} + case CalcOperatorCountValues: + countValues := 0 + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Number { + countValues++ + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countValues)}} + case CalcOperatorCountUniqueValues: + countUniqueValues := 0 + uniqueValues := map[float64]bool{} + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Number && row.Cells[colIndex].Value.Number.IsNotEmpty { + if !uniqueValues[row.Cells[colIndex].Value.Number.Content] { + uniqueValues[row.Cells[colIndex].Value.Number.Content] = true + countUniqueValues++ + } + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countUniqueValues)}} + case CalcOperatorCountEmpty: + countEmpty := 0 + for _, row := range av.Rows { + if nil == row.Cells[colIndex] || nil == row.Cells[colIndex].Value || nil == row.Cells[colIndex].Value.Number && !row.Cells[colIndex].Value.Number.IsNotEmpty { + countEmpty++ + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countEmpty)}} + case CalcOperatorCountNotEmpty: + countNotEmpty := 0 + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Number && row.Cells[colIndex].Value.Number.IsNotEmpty { + countNotEmpty++ + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countNotEmpty)}} + case CalcOperatorPercentEmpty: + countEmpty := 0 + for _, row := range av.Rows { + if nil == row.Cells[colIndex] || nil == row.Cells[colIndex].Value || nil == row.Cells[colIndex].Value.Number && !row.Cells[colIndex].Value.Number.IsNotEmpty { + countEmpty++ + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countEmpty) / float64(len(av.Rows))}} + case CalcOperatorPercentNotEmpty: + countNotEmpty := 0 + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Number && row.Cells[colIndex].Value.Number.IsNotEmpty { + countNotEmpty++ + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countNotEmpty) / float64(len(av.Rows))}} + case CalcOperatorSum: + sum := 0.0 + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Number && row.Cells[colIndex].Value.Number.IsNotEmpty { + sum += row.Cells[colIndex].Value.Number.Content + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: sum}} + case CalcOperatorAverage: + sum := 0.0 + count := 0 + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Number && row.Cells[colIndex].Value.Number.IsNotEmpty { + sum += row.Cells[colIndex].Value.Number.Content + count++ + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: sum / float64(count)}} + case CalcOperatorMedian: + values := []float64{} + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Number && row.Cells[colIndex].Value.Number.IsNotEmpty { + values = append(values, row.Cells[colIndex].Value.Number.Content) + } + } + sort.Float64s(values) + if len(values) > 0 { + if len(values)%2 == 0 { + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: (values[len(values)/2-1] + values[len(values)/2]) / 2}} + } else { + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: values[len(values)/2]}} + } + } else { + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{IsNotEmpty: false}} + } + case CalcOperatorMin: + min := math.MaxFloat64 + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Number && row.Cells[colIndex].Value.Number.IsNotEmpty { + if row.Cells[colIndex].Value.Number.Content < min { + min = row.Cells[colIndex].Value.Number.Content + } + } + } + if math.MaxFloat64 != min { + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: min}} + } else { + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{IsNotEmpty: false}} + } + case CalcOperatorMax: + max := -math.MaxFloat64 + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Number && row.Cells[colIndex].Value.Number.IsNotEmpty { + if row.Cells[colIndex].Value.Number.Content > max { + max = row.Cells[colIndex].Value.Number.Content + } + } + } + if -math.MaxFloat64 != max { + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: max}} + } else { + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{IsNotEmpty: false}} + } + case CalcOperatorRange: + min := math.MaxFloat64 + max := -math.MaxFloat64 + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Number && row.Cells[colIndex].Value.Number.IsNotEmpty { + if row.Cells[colIndex].Value.Number.Content < min { + min = row.Cells[colIndex].Value.Number.Content + } + if row.Cells[colIndex].Value.Number.Content > max { + max = row.Cells[colIndex].Value.Number.Content + } + } + } + if math.MaxFloat64 != min && -math.MaxFloat64 != max { + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: max - min}} + } else { + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{IsNotEmpty: false}} + } + } +} + +func (av *AttributeView) calcColText(col *Column, colIndex int) { + switch col.Calc.Operator { + case CalcOperatorCountAll: + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(len(av.Rows))}} + case CalcOperatorCountValues: + countValues := 0 + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Text && "" != row.Cells[colIndex].Value.Text.Content { + countValues++ + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countValues)}} + case CalcOperatorCountUniqueValues: + countUniqueValues := 0 + uniqueValues := map[string]bool{} + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Text && "" != row.Cells[colIndex].Value.Text.Content { + if !uniqueValues[row.Cells[colIndex].Value.Text.Content] { + uniqueValues[row.Cells[colIndex].Value.Text.Content] = true + countUniqueValues++ + } + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countUniqueValues)}} + case CalcOperatorCountEmpty: + countEmpty := 0 + for _, row := range av.Rows { + if nil == row.Cells[colIndex] || nil == row.Cells[colIndex].Value || nil == row.Cells[colIndex].Value.Text || "" == row.Cells[colIndex].Value.Text.Content { + countEmpty++ + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countEmpty)}} + case CalcOperatorCountNotEmpty: + countNotEmpty := 0 + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Text && "" != row.Cells[colIndex].Value.Text.Content { + countNotEmpty++ + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countNotEmpty)}} + case CalcOperatorPercentEmpty: + countEmpty := 0 + for _, row := range av.Rows { + if nil == row.Cells[colIndex] || nil == row.Cells[colIndex].Value || nil == row.Cells[colIndex].Value.Text || "" == row.Cells[colIndex].Value.Text.Content { + countEmpty++ + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countEmpty) / float64(len(av.Rows))}} + case CalcOperatorPercentNotEmpty: + countNotEmpty := 0 + for _, row := range av.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Text && "" != row.Cells[colIndex].Value.Text.Content { + countNotEmpty++ + } + } + col.Calc.Result = &ColumnCalcResult{Number: &ValueNumber{Content: float64(countNotEmpty) / float64(len(av.Rows))}} + } +}