Files
cloudpods/pkg/util/excelutils/excelutils.go
2025-01-16 11:54:41 +08:00

305 lines
7.9 KiB
Go

// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package excelutils
import (
"bytes"
"fmt"
"io"
"os"
"strings"
excelize "github.com/xuri/excelize/v2"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/gotypes"
)
func decimalBaseMaxWidth(decNum int, base int) int {
if decNum == 0 {
return 1
}
width := 0
for decNum > 0 {
decNum = decNum / base
width += 1
}
return width
}
func decimalBaseN(decNum int, base int, width int) (int, int) {
b := 1
for i := 0; i < width-1; i += 1 {
decNum = decNum / base
b = b * base
}
return decNum, b
}
func decimal2Base(decNum int, base int) []int {
width := decimalBaseMaxWidth(decNum, base)
ret := make([]int, width)
for i := width; i > 0; i -= 1 {
ith, divider := decimalBaseN(decNum, base, i)
decNum -= ith * divider
ret[width-i] = ith
}
return ret
}
func decimal2Alphabet(decNum int) string {
var buf bytes.Buffer
b26 := decimal2Base(decNum, 26)
for i := 0; i < len(b26); i += 1 {
if i == 0 && len(b26) > 1 {
buf.WriteByte(byte('A' + b26[i] - 1))
} else {
buf.WriteByte(byte('A' + b26[i]))
}
}
return buf.String()
}
func exportHeader(xlsx *excelize.File, texts []string, rowIndex int, sheet string) {
if len(sheet) == 0 {
sheet = DEFAULT_SHEET
}
for i := 0; i < len(texts); i += 1 {
cell := fmt.Sprintf("%s%d", decimal2Alphabet(i), rowIndex)
xlsx.SetCellValue(sheet, cell, texts[i])
}
}
func exportRow(xlsx *excelize.File, data jsonutils.JSONObject, keys []string, rowIndex int, sheet string) {
if len(sheet) == 0 {
sheet = DEFAULT_SHEET
}
for i := 0; i < len(keys); i += 1 {
cell := fmt.Sprintf("%s%d", decimal2Alphabet(i), rowIndex)
// var valStr string
var val jsonutils.JSONObject
if strings.Contains(keys[i], ".") {
val, _ = data.GetIgnoreCases(strings.Split(keys[i], ".")...)
// hack payment_bills
if !gotypes.IsNil(val) && strings.HasPrefix(keys[i], "tags.") {
vv := []string{}
val.Unmarshal(&vv)
if len(vv) > 0 || val.Equals(jsonutils.Marshal([]string{})) {
val = jsonutils.NewString(strings.Join(vv, ","))
}
}
} else {
val, _ = data.GetIgnoreCases(keys[i])
}
if val != nil {
// hack, make floating point number prettier
if fval, ok := val.(*jsonutils.JSONFloat); ok {
// 费用需要原样导出,避免数额不准
fvalResult, _ := fval.Float()
xlsx.SetCellValue(sheet, cell, fvalResult)
} else if ival, ok := val.(*jsonutils.JSONInt); ok {
i, _ := ival.Int()
xlsx.SetCellValue(sheet, cell, i)
} else if bval, ok := val.(*jsonutils.JSONBool); ok {
b, _ := bval.Bool()
xlsx.SetCellValue(sheet, cell, b)
} else {
s, _ := val.GetString()
xlsx.SetCellValue(sheet, cell, s)
}
} else {
xlsx.SetCellValue(sheet, cell, "")
}
}
}
func Export(data []jsonutils.JSONObject, keys []string, texts []string, writer io.Writer) error {
xlsx := excelize.NewFile()
exportHeader(xlsx, texts, 1, "")
for i := 0; i < len(data); i += 1 {
exportRow(xlsx, data[i], keys, i+2, "")
}
return xlsx.Write(writer)
}
// key:data中对应的key,text:头
func ExportFile(data []jsonutils.JSONObject, keys []string, texts []string, filename string) error {
writer, err := os.Create(filename)
if err != nil {
return err
}
defer writer.Close()
return Export(data, keys, texts, writer)
}
func ExportWriter(data []jsonutils.JSONObject, keys []string, texts []string, writer io.Writer) *excelize.File {
xlsx := excelize.NewFile()
exportHeader(xlsx, texts, 1, "")
for i := 0; i < len(data); i += 1 {
exportRow(xlsx, data[i], keys, i+2, "")
}
return xlsx
}
type SExcelChartSeries struct {
Name string
Categories string
Values string
}
type SExcelChartTitle struct {
Name string
}
type SExcelChartFormat struct {
XScale float64 `json:"x_scale"`
YScale float64 `json:"y_scale"`
XOffset float64 `json:"x_offset"`
YOffset float64 `json:"y_offset"`
PrintObj bool `json:"print_obj"`
LockAspectRatio bool `json:"lock_aspect_ratio"`
Locked bool `json:"locked"`
}
type SExcelChartParams struct {
Type ExcelChartType
Series []SExcelChartSeries
Title SExcelChartTitle
Format SExcelChartFormat
}
func AddNewSheet(data []jsonutils.JSONObject, keys []string, texts []string, sheet string, f *excelize.File) *excelize.File {
// 新建sheet
f.NewSheet(sheet)
exportHeader(f, texts, 1, sheet)
for i := 0; i < len(data); i += 1 {
exportRow(f, data[i], keys, i+2, sheet)
}
return f
}
func AddChartWithSheet(key, sheet string, chartType ExcelChartType, f *excelize.File) (*excelize.File, error) {
// 获取对应sheet中的所有header
keys, err := f.GetRows(sheet)
if err != nil {
return nil, errors.Wrap(err, "GetRows")
}
if len(keys) == 0 {
return nil, errors.Errorf("empty sheet")
}
// 获取header对应的坐标
keyIndex := 'A'
for k, v := range keys[0] {
if v == key {
keyIndex = keyIndex + rune(k)
}
}
// 图表请求
params := SExcelChartParams{
Type: chartType,
Series: []SExcelChartSeries{
{
Name: fmt.Sprintf("'%s'!$%s$1", sheet, string(keyIndex)),
Categories: fmt.Sprintf("'%s'!$A$2:$A$%d", sheet, len(keys)),
Values: fmt.Sprintf("'%s'!$%s$2:$%s$%d", sheet, string(keyIndex), string(keyIndex), len(keys))},
},
Title: SExcelChartTitle{Name: fmt.Sprintf("%s - %s", key, ChartMap[chartType])},
Format: SExcelChartFormat{
XScale: 2.7,
YScale: 2.9,
PrintObj: true,
},
}
paramObj := jsonutils.Marshal(params)
excelChart := &excelize.Chart{}
paramObj.Unmarshal(excelChart)
err = f.AddChart(sheet, "A1", excelChart)
if err != nil {
return nil, err
}
return f, nil
}
func CheckChartType(chartType string) bool {
_, isExist := ChartMap[ExcelChartType(chartType)]
return isExist
}
// 按列设置单元格格式
func SetCellStyleWithColumnKey(keys []string, sheet, style string, f *excelize.File) (*excelize.File, error) {
styleID, err := f.NewStyle(&excelize.Style{CustomNumFmt: &style})
if err != nil {
return nil, errors.Wrap(err, "NewStyle")
}
// 优先按行获取所有数据
rows, err := f.GetRows(sheet)
if err != nil {
return nil, errors.Wrap(err, "GetRows")
}
if len(rows) == 0 {
return nil, errors.Errorf("rows is empty")
}
// 所需设置key的索引
columnIndex := []int{}
// 遍历第一行的所有值
for index, value := range rows[0] {
for _, key := range keys {
// 若第一行的值与目标key相等
if key == value {
columnIndex = append(columnIndex, index)
}
}
}
// 遍历所需设置的索引
for _, index := range columnIndex {
// 根据索引获取excel的列名
colName, err := excelize.ColumnNumberToName(index + 1)
if err != nil {
return nil, errors.Wrap(err, "ColumnNumberToName")
}
if err := f.SetCellStyle(sheet, fmt.Sprintf("%s%d", colName, 2), fmt.Sprintf("%s%d", colName, len(rows)), styleID); err != nil {
return nil, errors.Wrap(err, "SetCellStyle")
}
}
return f, nil
}
// 按行读取excel文件,注:第一行为json的key
func ReadDataWithRow(f *excelize.File, sheet string) (jsonutils.JSONObject, error) {
datas := jsonutils.NewArray()
keys := []string{}
rows, err := f.GetRows(sheet)
if err != nil {
return nil, errors.Wrap(err, "get rows")
}
for index, row := range rows {
data := jsonutils.NewDict()
if index == 0 {
keys = row
continue
}
for i, value := range row {
if i >= len(keys) {
break
}
data.Set(keys[i], jsonutils.NewString(value))
}
datas.Add(data)
}
return jsonutils.Marshal(datas), nil
}