wangguanquan / eec

A fast and lower memory excel write/read tool.一个非POI底层,支持流式处理的高效且超低内存的Excel读写工具
https://github.com/wangguanquan/eec/wiki
Apache License 2.0
179 stars 54 forks source link

【特性讨论】数据写入能否按需设置横向 #307

Open 2259289435 opened 1 year ago

2259289435 commented 1 year ago

场景:使用 excel 打印,列头少情况下,经常采用数据合并,目的:一节省纸张,二数据量大

举例: 现在 excel 生成的格式为: 行一:跨列标题 行二:列1 列2 行三:数据1 数据1 行四:数据2 数据2

希望的格式为:两条数据合成一行 行一:跨列标题 行二:列1 列2 列1 列2 行三:数据1 数据1 数据2 数据2 行四:数据3 数据3 数据4 数据4 行五:数据5 数据5

现在通过编码去手动合并两行为一个对象显示可以实现,但底层还是数据与行 1 : 1,能否支持数据与行 N :1 ?

wangguanquan commented 1 year ago

可以再讨论,如果是横向的话那表头也得转向吧,感觉这个需求只需要将数据二次处理即可,N:1指的是什么呢?

在 2022年11月14日,10:40,2259289435 @.***> 写道:

场景:使用 excel 打印,列头少情况下,经常采用数据合并,目的:一节省纸张,二数据量大

举例: 现在 excel 生成的格式为: 行一:跨列标题 行二:列1 列2 行三:数据1 数据1 行四:数据2 数据2

希望的格式为:两条数据合成一行 行一:跨列标题 行二:列1 列2 列1 列2 行三:数据1 数据1 数据2 数据2 行四:数据3 数据3 数据4 数据4 行五:数据5 数据5

现在通过编码去手动合并两行为一个对象显示可以实现,但底层还是数据与行 1 : 1,能否支持数据与行 N :1 ?

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you are subscribed to this thread.

wangguanquan commented 1 year ago

能将使用场景具体化吗?原始数据是什么,最终效果是怎样,可以截图一个具体excel文件

在 2022年11月14日,10:40,2259289435 @.***> 写道:

场景:使用 excel 打印,列头少情况下,经常采用数据合并,目的:一节省纸张,二数据量大

举例: 现在 excel 生成的格式为: 行一:跨列标题 行二:列1 列2 行三:数据1 数据1 行四:数据2 数据2

希望的格式为:两条数据合成一行 行一:跨列标题 行二:列1 列2 列1 列2 行三:数据1 数据1 数据2 数据2 行四:数据3 数据3 数据4 数据4 行五:数据5 数据5

现在通过编码去手动合并两行为一个对象显示可以实现,但底层还是数据与行 1 : 1,能否支持数据与行 N :1 ?

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you are subscribed to this thread.

2259289435 commented 1 year ago

数据: 1 | 南瓜 | 11 2 | 冬瓜 | 12 3 | 西瓜 | 10 4 | 北瓜 | 12 ...

最终效果 image

2259289435 commented 1 year ago

数据与行 N : 1,比如上图就是数据与行 3 : 1,好处就是作业人员使用的时候可以一张纸看到尽可能多的数据,并且节省纸张。

现在确实可以通用listmap业务自行二次封装处理,但这种场景是比较通用的一种业务需求,所以想了解能否把这种数据与行自定义对应的能力封装到框架内。

wangguanquan commented 1 year ago

感谢反馈,已了解使用场景,考虑添加扩展属性或注解以支持新特性

wangguanquan commented 1 year ago

临时的你可以使用如下自定义WorksheetWriter实现该功能,原理很简单首先是表头需要特殊处理,将原来的表头复制n次,其次写行数据的时候判断是否需要写行头和行尾。需要注意写出时取消原有的斑马线,如果要实现斑马线需要使用setStyleProcessor()方法添加一个样式处理器(取模判断是否在一行上即可)

  1. 自定义平铺WorksheetWriter
  2. 导出时使用该writer即可
/**
 * 自定义平铺WorksheetWriter
 */
public static class TileXMLWorksheetWriter extends XMLWorksheetWriter {
    private int tile; // 平铺的数量,也就是每行重复输出多少条数据
    private String date; // 可忽略,仅仅是表头上的日期
    public TileXMLWorksheetWriter(int tile) {
        this.tile = tile;
    } 

    public TileXMLWorksheetWriter(int tile, String date) {
        this.tile = tile;
        this.date = date;
    }

    public int getTile() {
        return tile;
    }

    public void setTile(int tile) {
        this.tile = tile;
    }

    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }

    @Override
    protected void writeBefore() throws IOException {
        // The header columns
        columns = sheet.getAndSortHeaderColumns();
        // Give new columns
        tileColumns();

        boolean nonHeader = sheet.getNonHeader() == 1;

        bw.write(Const.EXCEL_XML_DECLARATION);
        // Declaration
        bw.newLine();
        // Root node
        writeRootNode();

        // Dimension
        writeDimension();

        // SheetViews default value
        writeSheetViews();

        // Default row height and width
        int fillSpace = 6;
        BigDecimal width = BigDecimal.valueOf(!nonHeader ? sheet.getDefaultWidth() : 8.38D);
        String defaultWidth = width.setScale(2, BigDecimal.ROUND_HALF_UP).toString();
        writeSheetFormat(fillSpace, defaultWidth);

        // cols
        writeCols(fillSpace, defaultWidth);

        // Write body data
        beforeSheetDate(nonHeader);
    }

    protected void tileColumns() {
        if (tile == 1) return;

        int x = columns.length, y = x * tile, t = columns[columns.length - 1].getRealColIndex();
        // Bound check
        if (y > Const.Limit.MAX_COLUMNS_ON_SHEET)
            throw new TooManyColumnsException(y, Const.Limit.MAX_COLUMNS_ON_SHEET);

        Column[] _columns = new Column[y];
        for (int i = 0; i < y; i++) {
            // 第一个对象的表头不需要复制
            Column col = i < x ? columns[i] : new Column(columns[i % x]).addSubColumn(new Column());
            col.realColIndex = columns[i % x].realColIndex + t * (i / x);
            _columns[i] = col;

            // 替换拣货单上的日期
            Column _col = col;
            do {
                if (StringUtil.isNotEmpty(_col.getName()) && _col.getName().contains("{date}"))
                    _col.setName(_col.getName().replace("{date}", date));
            }
            while ((_col = _col.next) != null);
        }

        this.columns = _columns;

        // FIXME 这里强行指定合并替换掉原本的头
        List<Dimension> mergeCells = Collections.singletonList(new Dimension(1, (short) 1, 1, (short) y));
        sheet.putExtProp(Const.ExtendPropertyKey.MERGE_CELLS, mergeCells);
    }

    @Override
    protected void writeRow(Row row) throws IOException {
        Cell[] cells = row.getCells();
        int len = cells.length, r = row.getIndex() / tile + startRow, c = columns[columns.length - 1].realColIndex / tile, y = row.getIndex() % tile;
        if (y == 0) startRow(r - startRow, columns[columns.length - 1].realColIndex);

        for (int i = 0; i < len; i++) {
            Cell cell = cells[i];
            int xf = cell.xf, col = i + c * y;
            switch (cell.t) {
                case INLINESTR:
                case SST:
                    writeString(cell.sv, r, col, xf);
                    break;
                case NUMERIC:
                    writeNumeric(cell.nv, r, col, xf);
                    break;
                case LONG:
                    writeNumeric(cell.lv, r, col, xf);
                    break;
                case DATE:
                case DATETIME:
                case DOUBLE:
                case TIME:
                    writeDouble(cell.dv, r, col, xf);
                    break;
                case BOOL:
                    writeBool(cell.bv, r, col, xf);
                    break;
                case DECIMAL:
                    writeDecimal(cell.mv, r, col, xf);
                    break;
                case CHARACTER:
                    writeChar(cell.cv, r, col, xf);
                    break;
                case BLANK:
                    writeNull(r, col, xf);
                    break;
                default:
            }
        }
        // 注意这里可能不会关闭row需要在writeAfter进行二次处理
        if (y == tile - 1)
            bw.write("</row>");
    }

    @Override
    protected void writeRowAutoSize(Row row) throws IOException {
        Cell[] cells = row.getCells();
        int len = cells.length, r = row.getIndex() / tile + startRow, c = columns[columns.length - 1].realColIndex / tile, y = row.getIndex() % tile;
        if (y == 0) startRow(r - startRow, columns[columns.length - 1].realColIndex);

        for (int i = 0; i < len; i++) {
            Cell cell = cells[i];
            int xf = cell.xf, col = i + c * y;
            switch (cell.t) {
                case INLINESTR:
                case SST:
                    writeStringAutoSize(cell.sv, r, col, xf);
                    break;
                case NUMERIC:
                    writeNumericAutoSize(cell.nv, r, col, xf);
                    break;
                case LONG:
                    writeNumericAutoSize(cell.lv, r, col, xf);
                    break;
                case DATE:
                case DATETIME:
                case DOUBLE:
                case TIME:
                    writeDoubleAutoSize(cell.dv, r, col, xf);
                    break;
                case BOOL:
                    writeBool(cell.bv, r, col, xf);
                    break;
                case DECIMAL:
                    writeDecimalAutoSize(cell.mv, r, col, xf);
                    break;
                case CHARACTER:
                    writeChar(cell.cv, r, col, xf);
                    break;
                case BLANK:
                    writeNull(r, col, xf);
                    break;
                default:
            }
        }
        // 注意这里可能不会关闭row需要在writeAfter进行二次处理
        if (y == tile - 1)
            bw.write("</row>");
    }

    @Override
    protected void writeAfter(int total) throws IOException {
        if (total > 0 && (total - 1) % tile < tile - 1) bw.write("</row>");
        super.writeAfter(total);
    }
}
public static class TileEntity {
    @ExcelColumn("{date} 拣货单")
    @ExcelColumn(value = "差异", maxWidth = 8.6D)
    private String diff;
    @ExcelColumn("{date} 拣货单")
    @ExcelColumn(value = "序号", maxWidth = 6.8D)
    private Integer no;
    @ExcelColumn("{date} 拣货单")
    @ExcelColumn(value = "商品", maxWidth = 12.0D)
    private String product;
    @ExcelColumn("{date} 拣货单")
    @ExcelColumn(value = "数量", maxWidth = 6.8D)
    private Integer num;

    public static List<TileEntity> randomTestData() {
        int n = random.nextInt(30) + 10;
        List<TileEntity> list = new ArrayList<>(n);
        for (int i = 0; i < n; i++) {
            TileEntity e = new TileEntity();
            e.no = i + 1;
            e.product = getRandomString(10);
            e.num = random.nextInt(20) + 1;
            list.add(e);
        }
        return list;
    }
}