├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── README.zh-cn.md ├── common-functions.png └── each-merge.png ├── pom.xml └── src ├── main └── java │ └── tech │ └── simter │ └── jxls │ └── ext │ ├── CommonFunctions.java │ ├── EachMergeCommand.java │ └── JxlsUtils.java └── test ├── java └── tech │ └── simter │ └── jxls │ ├── ChartsTest.java │ ├── FormulaTest.java │ └── ext │ ├── Bean.java │ ├── CommonFunctionsTest.java │ ├── DynamicColumnTest.java │ ├── EachMergeCommandTest.java │ ├── JxlsUtilsTest.java │ └── TwoSubListTest.java └── resources ├── logback-test.xml └── templates ├── charts.xlsx ├── common-functions-complex.xls ├── common-functions-complex.xlsx ├── common-functions.xlsx ├── dynamic-column.xlsx ├── each-merge.xlsx ├── each-merge2.xlsx ├── formula.xlsx └── two-sub-list.xlsx /.gitignore: -------------------------------------------------------------------------------- 1 | .metadata 2 | .settings 3 | .classpath 4 | .project 5 | target 6 | Thumbs.db 7 | *.pdb 8 | *.log 9 | *.log.* 10 | build 11 | bin 12 | .DS_Store 13 | .idea 14 | *.iml -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # simter-jxls-ext changelog 2 | 3 | ## 3.0.0 - 2022-06-21 4 | 5 | - Upgrade to simter-dependencies-3.0.0 (jdk-17) 6 | 7 | ## 2.0.0 - 2020-11-19 8 | 9 | - Upgrade to simter-dependencies-2.0.0 10 | 11 | ## 2.0.0-M2 - 2020-09-28 12 | 13 | - Merge v1.1.1 change 14 | - Fixed merge cell style error 15 | 1. create blank cell if not exists 16 | 2. first set cell style 17 | 3. last do the merge 18 | 19 | ## 1.1.1 - 2020-09-25 20 | 21 | - Use afterApplyAtCell instead of afterTransformCell in EachMergeCommand 22 | 23 | > For jxls-2.6 parent's afterTransformCell is call after sub's afterTransformCell. 24 | > But change after jxls-2.7+. 25 | > This change is for future compatibility. 26 | 27 | ## 2.0.0-M1 - 2020-06-02 28 | 29 | - Upgrade to simter-dependencies-2.0.0-M1 30 | 31 | ## 1.2.0-M3 - 2020-04-15 32 | 33 | - Upgrade to simter-1.3.0-M14 34 | 35 | ## 1.2.0-M2 - 2020-02-10 36 | 37 | - Upgrade to simter-1.3.0-M13 38 | 39 | ## 1.2.0-M1 - 2020-01-08 40 | 41 | - Upgrade to simter-1.3.0-M11 42 | - Support join list item to string with a special delimiter 43 | - Add `String CommonFunctions.join(List list, String delimiter)` method 44 | - Add `String CommonFunctions.join(List list)` method 45 | - Support join special key or property value of list item to a string with a special delimiter 46 | - Add `String CommonFunctions.joinProperty(List list, String name, String delimiter)` method 47 | - Add `String CommonFunctions.joinProperty(List list, String namer)` method 48 | - Support format `Duration` 49 | - Add `String duration(Temporal startTime, Temporal endTime)` method 50 | - Add `String format(Duration duration)` method 51 | 52 | ## 1.1.0 - 2019-07-03 53 | 54 | No code changed, just polishing maven config and unit test. 55 | 56 | - Use JUnit5|AssertJ instead of JUnit4|Hamcrest 57 | - Change parent to simter-dependencies-1.2.0 58 | 59 | ## 1.0.0 - 2019-01-08 60 | 61 | - Just align version 62 | 63 | ## 0.5.0 - 2018-01-05 64 | 65 | - Just align version 66 | 67 | ## 0.4.0 - 2018-01-05 68 | 69 | - Just centralize-version 70 | 71 | ## 0.3.0 - 2017-12-12 72 | 73 | - Add Jxls common functions 74 | - Add Jxls `jx:each-merge` command for auto merge cells -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 rj000 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [simter-jxls-ext](https://github.com/simter/simter-jxls-ext) [[中文]] 2 | 3 | Simter [Jxls] Extensions. Includes: 4 | - Common Functions 5 | - Format number: fn.format(1123456789.456, '#,###.00') > 1,123,456,789.56 6 | - Round number: fn.round(45.678, 2) > 45.68 7 | - Convert string to int: fn.toInt('123') > 123 8 | - Concat multipul strings: fn.concat('ab', 'c', ...) > 'abc' 9 | - Format java8 date/time: fn.format(LocalDateTime.now(), 'yyyy-MM-dd HH:mm:ss') > 2017-01-01 12:30:50 10 | 11 | - EachMergeCommand: `jx:each-merge`, for auto merge cells 12 | 13 | See the usage code bellow. 14 | 15 | ## Installation 16 | 17 | ```xml 18 | 19 | tech.simter 20 | simter-jxls-ext 21 | 1.0.0 22 | 23 | ``` 24 | 25 | ## Requirement 26 | 27 | - [Jxls] 2+ 28 | - Java 8+ 29 | 30 | ## Usage 31 | 32 | ### Common Functions 33 | 34 | ```java 35 | Context context = new Context(); 36 | 37 | // inject common-functions 38 | context.putVar("fn", CommonFunctions.getSingleton()); 39 | 40 | // other data 41 | context.putVar("num", new BigDecimal("123.456")); 42 | context.putVar("datetime", LocalDateTime.now()); 43 | context.putVar("date", LocalDate.now()); 44 | context.putVar("time", LocalTime.now()); 45 | context.putVar("str", "123"); 46 | 47 | // render template 48 | InputStream template = ...; 49 | OutputStream output = ...; 50 | JxlsHelper.getInstance().processTemplate(template, output, context); 51 | ``` 52 | 53 | Check the unit test code from [CommonFunctionsTest.java]. The [template][common-functions-template] and render result show bellow: 54 | 55 | ![common-functions.png] 56 | 57 | ### EachMergeCommand - Auto merge cells 58 | 59 | ```java 60 | // global add custom each-merge command to XlsCommentAreaBuilder 61 | XlsCommentAreaBuilder.addCommandMapping(EachMergeCommand.COMMAND_NAME, EachMergeCommand.class); 62 | 63 | // generate a main-sub structure data 64 | Context context = new Context(); 65 | context.putVar("rows", generateRowsData()); 66 | 67 | // render template 68 | InputStream template = ...; 69 | OutputStream output = ...; 70 | JxlsHelper.getInstance().processTemplate(template, output, context); 71 | ``` 72 | 73 | The `generateRowsData()` method generates the bellow structure data: 74 | 75 | ```javascript 76 | [ 77 | { 78 | sn: 1, 79 | name: 'row1', 80 | subs: [ 81 | {sn: '1-1', name: 'row1sub1'}, 82 | ... 83 | ] 84 | }, 85 | ... 86 | ] 87 | ``` 88 | 89 | Check the unit test code from [EachMergeCommandTest.java]. The [template][each-merge-template] and render result show bellow: 90 | 91 | ![each-merge.png] 92 | 93 | ## Build 94 | 95 | ```bash 96 | mvn clean package 97 | ``` 98 | 99 | ## Deploy 100 | 101 | First take a look at [simter-parent] deploy config. 102 | 103 | ### Deploy to LAN Nexus Repository 104 | 105 | ```bash 106 | mvn clean deploy -P lan 107 | ``` 108 | 109 | ### Deploy to Sonatype Repository 110 | 111 | ```bash 112 | mvn clean deploy -P sonatype 113 | ``` 114 | 115 | After deployed, login into . Through `Staging Repositories`, search this package, 116 | then close and release it. After couple hours, it will be synced 117 | to [Maven Central Repository](http://repo1.maven.org/maven2/tech/simter/simter-jxls-ext). 118 | 119 | ### Deploy to Bintray Repository 120 | 121 | ```bash 122 | mvn clean deploy -P bintray 123 | ``` 124 | 125 | Will deploy to `https://api.bintray.com/maven/simter/maven/tech.simter:simter-jxls-ext/;publish=1`. 126 | So first create a package `https://bintray.com/simter/maven/tech.simter:simter-jxls-ext` on Bintray. 127 | After deployed, check it from . 128 | 129 | 130 | [Jxls]: http://jxls.sourceforge.net 131 | [oss.sonatype.org]: https://oss.sonatype.org 132 | [simter-parent]: https://github.com/simter/simter-parent 133 | [中文]: https://github.com/simter/simter-jxls-ext/blob/master/docs/README.zh-cn.md 134 | 135 | [CommonFunctionsTest.java]: https://github.com/simter/simter-jxls-ext/blob/master/src/test/java/tech/simter/jxls/ext/CommonFunctionsTest.java#L77 136 | [common-functions-template]: https://github.com/simter/simter-jxls-ext/raw/master/src/test/resources/templates/common-functions.xlsx 137 | [common-functions.png]: docs/common-functions.png 138 | 139 | [EachMergeCommandTest.java]: https://github.com/simter/simter-jxls-ext/blob/master/src/test/java/tech/simter/jxls/ext/EachMergeCommandTest.java#L30 140 | [each-merge-template]: https://github.com/simter/simter-jxls-ext/raw/master/src/test/resources/templates/each-merge.xlsx 141 | [each-merge.png]: docs/each-merge.png -------------------------------------------------------------------------------- /docs/README.zh-cn.md: -------------------------------------------------------------------------------- 1 | # [simter-jxls-ext](https://github.com/simter/simter-jxls-ext) [[English]] 2 | 3 | Simter [Jxls] Extensions. Includes: 4 | - Common Functions 5 | - Format number: fn.format(1123456789.456, '#,###.00') > 1,123,456,789.56 6 | - Round number: fn.round(45.678, 2) > 45.68 7 | - Convert string to int: fn.toInt('123') > 123 8 | - Concat multipul strings: fn.concat('ab', 'c', ...) > 'abc' 9 | - Format java8 date/time: fn.format(LocalDateTime.now(), 'yyyy-MM-dd HH:mm:ss') > 2017-01-01 12:30:50 10 | 11 | - EachMergeCommand: `jx:each-merge`, for auto merge cells 12 | 13 | See the usage code bellow. 14 | 15 | ## 安装 16 | 17 | ```xml 18 | 19 | tech.simter 20 | simter-jxls-ext 21 | 1.0.0 22 | 23 | ``` 24 | 25 | ## 要求 26 | 27 | - [Jxls] 2+ 28 | - Java 8+ 29 | 30 | ## 使用 31 | 32 | ### Common Functions 33 | 34 | ```java 35 | Context context = new Context(); 36 | 37 | // inject common-functions 38 | context.putVar("fn", CommonFunctions.getSingleton()); 39 | 40 | // other data 41 | context.putVar("num", new BigDecimal("123.456")); 42 | context.putVar("datetime", LocalDateTime.now()); 43 | context.putVar("date", LocalDate.now()); 44 | context.putVar("time", LocalTime.now()); 45 | context.putVar("str", "123"); 46 | 47 | // render template 48 | InputStream template = ...; 49 | OutputStream output = ...; 50 | JxlsHelper.getInstance().processTemplate(template, output, context); 51 | ``` 52 | 53 | 相应的单元测试代码参见 [CommonFunctionsTest.java]. [Excel 模板][common-functions-template] 和渲染结果截图如下: 54 | 55 | ![common-functions.png] 56 | 57 | ### EachMergeCommand - 自动合并单元格 58 | 59 | ```java 60 | // 全局注册自定义的 each-merge 指令到 XlsCommentAreaBuilder 61 | XlsCommentAreaBuilder.addCommandMapping(EachMergeCommand.COMMAND_NAME, EachMergeCommand.class); 62 | 63 | // 生成 "主-从" 结构的数据 64 | Context context = new Context(); 65 | context.putVar("rows", generateRowsData()); 66 | 67 | // 渲染模板 68 | InputStream template = ...; 69 | OutputStream output = ...; 70 | JxlsHelper.getInstance().processTemplate(template, output, context); 71 | ``` 72 | 73 | 方法 `generateRowsData()` 生成的数据结构如下: 74 | 75 | ```javascript 76 | [ 77 | { 78 | sn: 1, 79 | name: 'row1', 80 | subs: [ 81 | {sn: '1-1', name: 'row1sub1'}, 82 | ... 83 | ] 84 | }, 85 | ... 86 | ] 87 | ``` 88 | 89 | 相应的单元测试代码参见 [EachMergeCommandTest.java]. [Excel 模板][each-merge-template] 和渲染结果截图如下: 90 | 91 | ![each-merge.png] 92 | 93 | ## 构建 94 | 95 | ```bash 96 | mvn clean package 97 | ``` 98 | 99 | ## 发布 100 | 101 | 请先查看 [simter-parent] 的发布配置说明。 102 | 103 | ### 发布到局域网 Nexus 仓库 104 | 105 | ```bash 106 | mvn clean deploy -P lan 107 | ``` 108 | 109 | ### 发布到 Sonatype 仓库 110 | 111 | ```bash 112 | mvn clean deploy -P sonatype 113 | ``` 114 | 115 | 发布成功后登陆到 ,在 `Staging Repositories` 找到这个包,然后将其 close 和 release。 116 | 过几个小时后,就会自动同步到 [Maven 中心仓库](http://repo1.maven.org/maven2/tech/simter/simter-jxls-ext) 了。 117 | 118 | ### 发布到 Bintray 仓库 119 | 120 | ```bash 121 | mvn clean deploy -P bintray 122 | ``` 123 | 124 | 发布之前要先在 Bintray 创建 package `https://bintray.com/simter/maven/tech.simter:simter-jxls-ext`。 125 | 发布到的地址为 `https://api.bintray.com/maven/simter/maven/tech.simter:simter-jxls-ext/;publish=1`。 126 | 发布成功后可以到 检查一下结果。 127 | 128 | 129 | [English]: https://github.com/simter/simter-jxls-ext/blob/master/README.md 130 | [simter-parent]: https://github.com/simter/simter-parent/blob/master/docs/README.zh-cn.md 131 | 132 | [Jxls]: http://jxls.sourceforge.net 133 | [oss.sonatype.org]: https://oss.sonatype.org 134 | 135 | [CommonFunctionsTest.java]: https://github.com/simter/simter-jxls-ext/blob/master/src/test/java/tech/simter/jxls/ext/CommonFunctionsTest.java#L77 136 | [common-functions-template]: https://github.com/simter/simter-jxls-ext/raw/master/src/test/resources/templates/common-functions.xlsx 137 | [common-functions.png]: common-functions.png 138 | 139 | [EachMergeCommandTest.java]: https://github.com/simter/simter-jxls-ext/blob/master/src/test/java/tech/simter/jxls/ext/EachMergeCommandTest.java#L30 140 | [each-merge-template]: https://github.com/simter/simter-jxls-ext/raw/master/src/test/resources/templates/each-merge.xlsx 141 | [each-merge.png]: each-merge.png -------------------------------------------------------------------------------- /docs/common-functions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simter/simter-jxls-ext/b88f95d5fa0d9090c16b0f1ed1ea908932967077/docs/common-functions.png -------------------------------------------------------------------------------- /docs/each-merge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simter/simter-jxls-ext/b88f95d5fa0d9090c16b0f1ed1ea908932967077/docs/each-merge.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | tech.simter 6 | simter-dependencies 7 | 3.0.0 8 | 9 | 10 | simter-jxls-ext 11 | 3.0.0 12 | jar 13 | simter-jxls-ext 14 | Simter Jxls Extension 15 | https://github.com/simter/simter-jxls-ext 16 | 17 | 18 | MIT 19 | https://opensource.org/licenses/MIT 20 | repo 21 | 22 | 23 | 24 | https://github.com/simter/simter-jxls-ext.git 25 | 26 | 27 | simter 28 | https://github.com/simter 29 | 30 | 31 | 32 | RJ.Hwang 33 | rongjihuang@gmail.com 34 | 35 | 36 | 37 | GitHub 38 | https://github.com/simter/simter-jxls-ext/issues 39 | 40 | 41 | 42 | org.jxls 43 | jxls 44 | 45 | 46 | org.jxls 47 | jxls-poi 48 | 49 | 50 | javax.ws.rs 51 | javax.ws.rs-api 52 | true 53 | 54 | 55 | org.slf4j 56 | slf4j-api 57 | 58 | 59 | 60 | 61 | org.junit.jupiter 62 | junit-jupiter 63 | test 64 | 65 | 66 | org.assertj 67 | assertj-core 68 | test 69 | 70 | 71 | org.slf4j 72 | jcl-over-slf4j 73 | test 74 | 75 | 76 | ch.qos.logback 77 | logback-classic 78 | test 79 | 80 | 81 | org.springframework.boot 82 | spring-boot 83 | test 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/main/java/tech/simter/jxls/ext/CommonFunctions.java: -------------------------------------------------------------------------------- 1 | package tech.simter.jxls.ext; 2 | 3 | import org.apache.commons.beanutils.BeanUtils; 4 | 5 | import java.math.BigDecimal; 6 | import java.text.DecimalFormat; 7 | import java.time.Duration; 8 | import java.time.format.DateTimeFormatter; 9 | import java.time.temporal.Temporal; 10 | import java.time.temporal.TemporalAccessor; 11 | import java.util.Arrays; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.Objects; 15 | import java.util.stream.Collectors; 16 | 17 | import static java.math.RoundingMode.HALF_UP; 18 | 19 | /** 20 | * The common functions for jxls. 21 | *

22 | * 1) format date-time.
23 | * 2) format number.
24 | * 3) concat string.
25 | * 4) string to int.
26 | * 5) join list to string.
27 | * 6) join list item property value to string.
28 | * 29 | * @author RJ 30 | */ 31 | public final class CommonFunctions { 32 | private static final CommonFunctions singleton = new CommonFunctions(); 33 | 34 | public static CommonFunctions getSingleton() { 35 | return singleton; 36 | } 37 | 38 | private CommonFunctions() { 39 | } 40 | 41 | /** 42 | * Format java.time. 43 | * 44 | * @param temporal javaTime instance 45 | * @param pattern the formatter 46 | * @return the formatted value 47 | */ 48 | public String format(TemporalAccessor temporal, String pattern) { 49 | return temporal == null ? null : DateTimeFormatter.ofPattern(pattern).format(temporal); 50 | } 51 | 52 | /** 53 | * Format a duration to a string. 54 | *

55 | * The format of the returned string will be '{@code nDnHnMnS}', 56 | * where n is the relevant days, hours, minutes or seconds part of the duration. 57 | * If a section has a zero value, it is omitted. 58 | * 59 | * @param duration the duration 60 | * @return if the duration is null, return null. Otherwise, return an not null '{@code nDnHnMnS}' representation of this duration 61 | */ 62 | public String format(Duration duration) { 63 | if (duration == null) return null; 64 | 65 | StringBuilder buffer = new StringBuilder(); 66 | if (duration.isNegative()) buffer.append("-"); 67 | duration = duration.abs(); 68 | if (duration.toDays() > 0) { 69 | buffer.append(duration.toDays()).append("D"); 70 | } 71 | if (duration.toHours() % 24 > 0) { 72 | buffer.append(duration.toHours() % 24).append("H"); 73 | } 74 | if (duration.toMinutes() % 60 > 0) { 75 | buffer.append(duration.toMinutes() % 60).append("M"); 76 | } 77 | if (duration.getSeconds() % 60 > 0) { 78 | buffer.append(duration.getSeconds() % 60).append("S"); 79 | } 80 | return buffer.toString(); 81 | } 82 | 83 | /** 84 | * Calculate two times duration and format to a string with pattern '{@code nDnHnMnS}'. 85 | *

86 | * The format of the returned string will be '{@code nDnHnMnS}', where n is 87 | * the relevant hours, minutes or seconds part of the duration. 88 | * If a section has a zero value, it is omitted. 89 | * 90 | * @param startTime the start time 91 | * @param endTime the end time 92 | * @return if the startTime or endTime is null, return null. Otherwise, return an not null '{@code nDnHnMnS}' representation 93 | */ 94 | public String duration(Temporal startTime, Temporal endTime) { 95 | if (startTime == null || endTime == null) return null; 96 | else return format(Duration.between(startTime, endTime)); 97 | } 98 | 99 | /** 100 | * Format number. 101 | * 102 | * @param number the number value 103 | * @param pattern the formatter 104 | * @return the formatted value 105 | */ 106 | public String format(Number number, String pattern) { 107 | return number == null ? null : new DecimalFormat(pattern).format(number); 108 | } 109 | 110 | /** 111 | * Round number half up with special scale. 112 | * 113 | * @param number the number value 114 | * @param scale the scale value 115 | * @return half up number value 116 | */ 117 | public Number round(Number number, int scale) { 118 | return number == null ? null : (number instanceof BigDecimal ? 119 | ((BigDecimal) number).setScale(scale, HALF_UP) 120 | : new BigDecimal(number.toString()).setScale(scale, HALF_UP)); 121 | } 122 | 123 | /** 124 | * Concat all param to a string. 125 | * 126 | * @param items the params 127 | * @return a string 128 | */ 129 | public String concat(Object... items) { 130 | if (items == null || items.length == 0) return null; 131 | return Arrays.stream(items).map(i -> i == null ? "" : i.toString()).collect(Collectors.joining()); 132 | } 133 | 134 | /** 135 | * Convert string value to a Integer value. 136 | * 137 | * @param str the string value 138 | * @return an Integer value 139 | */ 140 | public Integer toInt(String str) { 141 | return str == null ? null : Integer.valueOf(str); 142 | } 143 | 144 | /** 145 | * Join all list item to a string with a special delimiter. 146 | *

147 | * Note:null item would be ignored. 148 | * 149 | * @param list the list to join 150 | * @param delimiter the delimiter to join item 151 | * @return a joined string 152 | */ 153 | public String join(List list, String delimiter) { 154 | if (delimiter == null) delimiter = ", "; 155 | return list.stream() 156 | .filter(Objects::nonNull) 157 | .map(Object::toString) 158 | .collect(Collectors.joining(delimiter)); 159 | } 160 | 161 | /** 162 | * Join all list item to a string with ", " delimiter. 163 | *

164 | * Note:null item would be ignored. 165 | * 166 | * @param list the list to join 167 | * @return a joined string 168 | */ 169 | public String join(List list) { 170 | return join(list, ", "); 171 | } 172 | 173 | /** 174 | * Join special key or property value of list item to a string with a special delimiter. 175 | *

176 | * Note:null item or null property value would be ignored. 177 | * 178 | * @param list the list to join 179 | * @param name the bean property name or map key to get the value 180 | * @param delimiter the delimiter to join item 181 | * @return a joined string 182 | */ 183 | public String joinProperty(List list, String name, String delimiter) { 184 | return list.stream() 185 | .map(item -> { 186 | if (item != null) { 187 | if (item instanceof Map) return ((Map) item).get(name); // 取 Map 中特定 key 的值 188 | else { // 取 bean 中特定属性的值 189 | try { 190 | return BeanUtils.getProperty(item, name); 191 | } catch (Exception e) { 192 | throw new RuntimeException(e); 193 | } 194 | } 195 | } else return null; 196 | }) 197 | .filter(Objects::nonNull) 198 | .map(Object::toString) 199 | .collect(Collectors.joining(delimiter)); 200 | } 201 | 202 | /** 203 | * Join special key or property value of list item to a string with ", " delimiter. 204 | *

205 | * Note:null item or null property value would be ignored. 206 | * 207 | * @param list the list to join 208 | * @param name the bean property name or map key to get the value 209 | * @return a joined string 210 | */ 211 | public String joinProperty(List list, String name) { 212 | return joinProperty(list, name, ", "); 213 | } 214 | } -------------------------------------------------------------------------------- /src/main/java/tech/simter/jxls/ext/EachMergeCommand.java: -------------------------------------------------------------------------------- 1 | package tech.simter.jxls.ext; 2 | 3 | import org.apache.poi.ss.usermodel.Cell; 4 | import org.apache.poi.ss.usermodel.Row; 5 | import org.apache.poi.ss.usermodel.Sheet; 6 | import org.apache.poi.ss.usermodel.Workbook; 7 | import org.apache.poi.ss.util.CellRangeAddress; 8 | import org.jxls.area.Area; 9 | import org.jxls.command.CellRefGenerator; 10 | import org.jxls.command.EachCommand; 11 | import org.jxls.common.*; 12 | import org.jxls.transform.Transformer; 13 | import org.jxls.transform.poi.PoiTransformer; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | import java.util.ArrayList; 18 | import java.util.Collection; 19 | import java.util.List; 20 | import java.util.stream.Collectors; 21 | import java.util.stream.IntStream; 22 | 23 | /** 24 | * Extends {@link EachCommand} to support merge cells. 25 | * 26 | * @author RJ 27 | */ 28 | public class EachMergeCommand extends EachCommand { 29 | private static final Logger logger = LoggerFactory.getLogger(EachMergeCommand.class); 30 | 31 | public static final String COMMAND_NAME = "each-merge"; 32 | 33 | public EachMergeCommand() { 34 | super(); 35 | } 36 | 37 | public EachMergeCommand(String var, String items, Direction direction) { 38 | super(var, items, direction); 39 | } 40 | 41 | public EachMergeCommand(String items, Area area) { 42 | super(items, area); 43 | } 44 | 45 | public EachMergeCommand(String var, String items, Area area) { 46 | super(var, items, area); 47 | } 48 | 49 | public EachMergeCommand(String var, String items, Area area, Direction direction) { 50 | super(var, items, area, direction); 51 | } 52 | 53 | public EachMergeCommand(String var, String items, Area area, CellRefGenerator cellRefGenerator) { 54 | super(var, items, area, cellRefGenerator); 55 | } 56 | 57 | @Override 58 | public Size applyAt(CellRef cellRef, Context context) { 59 | // collect sub command areas 60 | List childAreas = this.getAreaList().stream() 61 | .flatMap(area1 -> area1.getCommandDataList().stream()) 62 | .flatMap(commandData -> commandData.getCommand().getAreaList().stream()) 63 | .collect(Collectors.toList()); 64 | List childAreaRefs = childAreas.stream() 65 | .map(Area::getAreaRef).collect(Collectors.toList()); 66 | 67 | // register AreaListener for parent command area 68 | Area parentArea = this.getAreaList().get(0); 69 | MergeCellListener listener = new MergeCellListener(getTransformer(), parentArea.getAreaRef(), childAreaRefs, 70 | ((Collection) context.getVar(this.getItems())).size()); // 总数据量 71 | logger.info("register MergeCellListener@{} for parent-area '{}', cellRef={}", listener.hashCode(), parentArea.getAreaRef(), cellRef); 72 | parentArea.addAreaListener(listener); 73 | 74 | // register AreaListener for all sub command area 75 | childAreas.forEach(area -> { 76 | logger.info("register MergeCellListener@{} for child-area '{}', cellRef={}, parent={}", listener.hashCode(), area.getAreaRef(), cellRef, parentArea.getAreaRef()); 77 | area.addAreaListener(listener); 78 | }); 79 | 80 | // standard dealing 81 | return super.applyAt(cellRef, context); 82 | } 83 | 84 | /** 85 | * The {@link AreaListener} for merge cells. 86 | */ 87 | public static class MergeCellListener implements AreaListener { 88 | private final PoiTransformer transformer; 89 | private final int parentStartColumn; // parent command start column 90 | private final int[] childStartColumns; // all sub command start column 91 | private final int[] mergeColumns; // to merge columns 92 | private final List records = new ArrayList<>(); // 0 - start column, 1 - end column 93 | private final int parentCount; 94 | 95 | private int childRow; 96 | private int parentProcessed; 97 | 98 | MergeCellListener(Transformer transformer, AreaRef parent, List children, int parentCount) { 99 | this.transformer = (PoiTransformer) transformer; 100 | this.parentCount = parentCount; 101 | this.parentStartColumn = parent.getFirstCellRef().getCol(); 102 | 103 | // find all sub command columns 104 | int[] childCols = children.stream() 105 | .flatMapToInt(ref -> IntStream.range(ref.getFirstCellRef().getCol(), ref.getLastCellRef().getCol() + 1)) 106 | .distinct().sorted() 107 | .toArray(); 108 | 109 | // find all sub command start column 110 | this.childStartColumns = children.stream() 111 | .mapToInt(ref -> ref.getFirstCellRef().getCol()) 112 | .distinct().sorted().toArray(); 113 | 114 | // get columns to merge by filter childCols 115 | this.mergeColumns = IntStream.range(parent.getFirstCellRef().getCol(), parent.getLastCellRef().getCol() + 1) 116 | .filter(parentCol -> IntStream.of(childCols).noneMatch(childCol -> childCol == parentCol)) 117 | .toArray(); 118 | 119 | if (logger.isDebugEnabled()) { 120 | logger.debug("parentArea={}", parent); 121 | logger.debug("parentStartColumn={}", parentStartColumn); 122 | logger.debug("childStartColumns={}", childStartColumns); 123 | logger.debug("mergeColumns={}", mergeColumns); 124 | logger.debug("childCols={}", childCols); 125 | } 126 | } 127 | 128 | @Override 129 | public void beforeApplyAtCell(CellRef cellRef, Context context) { 130 | if (logger.isDebugEnabled()) { 131 | if (cellRef.getCol() == parentStartColumn) // parent command excel-row 132 | logger.debug("start parent: cellRef={} [{}, {}]", cellRef, cellRef.getRow(), cellRef.getCol()); 133 | } 134 | } 135 | 136 | @Override 137 | public void beforeTransformCell(CellRef srcCell, CellRef targetCell, Context context) { 138 | } 139 | 140 | @Override 141 | public void afterTransformCell(CellRef srcCell, CellRef targetCell, Context context) { 142 | } 143 | 144 | // parent's afterApplyAtCell call after all child's afterApplyAtCell invoked. 145 | @Override 146 | public void afterApplyAtCell(CellRef cellRef, Context context) { 147 | boolean isParentRow = cellRef.getCol() == parentStartColumn; 148 | 149 | if (isParentRow) { // parent command excel-row 150 | logger.debug("end parent: cellRef={} [{}, {}]", cellRef, cellRef.getRow(), cellRef.getCol()); 151 | 152 | // set parent-row position 153 | this.parentProcessed++; 154 | 155 | // record merge region only when more than one excel-row in the parent-row 156 | if (cellRef.getRow() < this.childRow) this.records.add(new int[]{cellRef.getRow(), this.childRow}); 157 | 158 | // only do the merge after last parent row 159 | if (this.parentProcessed == this.parentCount) { 160 | Workbook workbook = transformer.getWorkbook(); 161 | Sheet sheet = workbook.getSheet(cellRef.getSheetName()); 162 | doMerge(sheet, this.records, this.mergeColumns, cellRef); 163 | } 164 | 165 | // reset child excel-row index 166 | this.childRow = 0; 167 | } else if (IntStream.of(childStartColumns).anyMatch(col -> col == cellRef.getCol())) { // sub command excel-row 168 | if (logger.isDebugEnabled()) { 169 | int subIndex = -1; 170 | for (int i = 0; i < childStartColumns.length; i++) { 171 | if (childStartColumns[i] == cellRef.getCol()) { 172 | subIndex = i; 173 | break; 174 | } 175 | } 176 | logger.debug(" sub{}: cellRef={} [{}, {}]", subIndex, cellRef, cellRef.getRow(), cellRef.getCol()); 177 | } 178 | // record the current excel-row index of sub command process 179 | this.childRow = Math.max(this.childRow, cellRef.getRow()); 180 | } 181 | } 182 | 183 | private static void doMerge(Sheet sheet, List records, int[] mergeColumns, CellRef srcCell) { 184 | if (logger.isDebugEnabled()) { 185 | logger.debug("start merge: sheetName={}, records={}", sheet.getSheetName(), 186 | records.stream().map(startEnd -> "[" + startEnd[0] + "," + startEnd[1] + "]") 187 | .collect(Collectors.joining(","))); 188 | } 189 | records.forEach(startEnd -> merge4Row(sheet, startEnd[0], startEnd[1], mergeColumns, srcCell)); 190 | } 191 | 192 | private static void merge4Row(Sheet sheet, int fromRow, int toRow, int[] mergeColumns, CellRef srcCell) { 193 | if (fromRow >= toRow) { 194 | logger.warn(" No need to merge because same row:fromRow={}, toRow={}", fromRow, toRow); 195 | return; 196 | } 197 | Cell originCell; 198 | for (int col : mergeColumns) { 199 | logger.debug(" fromRow={}, toRow={}, col={}", fromRow, toRow, col); 200 | 201 | // set all merge-cells style to the origin cell style 202 | originCell = sheet.getRow(srcCell.getRow()).getCell(col); 203 | for (int r = fromRow; r <= toRow; r++) { 204 | Row row = sheet.getRow(r); 205 | Cell cell = row.getCell(col); 206 | if (cell == null) { 207 | // create blank cell if not exists 208 | // Note: if not create the blank cell, the merge region border style sometime loss 209 | cell = row.createCell(col); 210 | } 211 | cell.setCellStyle(originCell.getCellStyle()); 212 | } 213 | 214 | // do the merge 215 | sheet.addMergedRegion(new CellRangeAddress(fromRow, toRow, col, col)); 216 | } 217 | } 218 | } 219 | } -------------------------------------------------------------------------------- /src/main/java/tech/simter/jxls/ext/JxlsUtils.java: -------------------------------------------------------------------------------- 1 | package tech.simter.jxls.ext; 2 | 3 | import org.jxls.builder.xls.XlsCommentAreaBuilder; 4 | import org.jxls.common.Context; 5 | import org.jxls.util.JxlsHelper; 6 | 7 | import javax.ws.rs.core.Response; 8 | import javax.ws.rs.core.StreamingOutput; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.OutputStream; 12 | import java.io.UnsupportedEncodingException; 13 | import java.net.URLEncoder; 14 | import java.time.OffsetDateTime; 15 | import java.util.Map; 16 | 17 | /** 18 | * The Jxls Utils. 19 | * 20 | * @author RJ 21 | */ 22 | public class JxlsUtils { 23 | static { 24 | // global add custom each-merge command to XlsCommentAreaBuilder 25 | XlsCommentAreaBuilder.addCommandMapping(EachMergeCommand.COMMAND_NAME, EachMergeCommand.class); 26 | } 27 | 28 | /** 29 | * Render the excel template with the specified data to the {@link OutputStream}. 30 | * 31 | * @param template the excel template, can be xlsx or xls format 32 | * @param target the output target 33 | * @param data the data 34 | * @throws RuntimeException if has IOException inner 35 | */ 36 | public static void renderTemplate(InputStream template, Map data, OutputStream target) { 37 | // Convert to jxls Context 38 | Context context = convert2Context(data); 39 | 40 | // Add default functions 41 | addDefault(context); 42 | 43 | // render 44 | renderByJxls(template, target, context); 45 | } 46 | 47 | private static void renderByJxls(InputStream template, OutputStream target, Context context) { 48 | try { 49 | JxlsHelper.getInstance().processTemplate(template, target, context); 50 | } catch (IOException e) { 51 | throw new RuntimeException(e.getMessage(), e); 52 | } 53 | } 54 | 55 | public static Context convert2Context(Map data) { 56 | Context context = new Context(); 57 | if (data != null) data.forEach(context::putVar); 58 | return context; 59 | } 60 | 61 | private static void addDefault(Context context) { 62 | // Current timestamp 63 | if (context.getVar("ts") == null) context.putVar("ts", OffsetDateTime.now()); 64 | 65 | // Add default functions 66 | if (context.getVar("fn") == null) context.putVar("fn", CommonFunctions.getSingleton()); 67 | } 68 | 69 | /** 70 | * Generate a {@link Response.ResponseBuilder} instance 71 | * and render the excel template with the specified data to its output stream. 72 | * 73 | * @param template the excel template, can be xlsx or xls format 74 | * @param data the data 75 | * @return the instance of {@link Response.ResponseBuilder} with the excel data 76 | * @throws RuntimeException if has IOException or UnsupportedEncodingException inner 77 | */ 78 | public static Response.ResponseBuilder renderTemplate2Response(InputStream template, Map data) { 79 | return renderTemplate2Response(template, data, null); 80 | } 81 | 82 | /** 83 | * Generate a {@link Response.ResponseBuilder} instance 84 | * and render the excel template with the specified data to its output stream. 85 | * 86 | * @param template the excel template, can be xlsx or xls format 87 | * @param data the data 88 | * @param filename the download filename of the response 89 | * @return the instance of {@link Response.ResponseBuilder} with the excel data 90 | * @throws RuntimeException if has IOException or UnsupportedEncodingException inner 91 | */ 92 | public static Response.ResponseBuilder renderTemplate2Response(InputStream template, Map data, 93 | String filename) { 94 | StreamingOutput stream = (OutputStream output) -> { 95 | // Convert to jxls Context 96 | Context context = convert2Context(data); 97 | 98 | // Add default functions 99 | addDefault(context); 100 | 101 | // render 102 | renderByJxls(template, output, context); 103 | }; 104 | 105 | // create response 106 | Response.ResponseBuilder builder = Response.ok(stream); 107 | if (filename != null) { 108 | try { 109 | builder.header("Content-Disposition", "attachment; filename=\"" + URLEncoder.encode(filename, "UTF-8") + "\""); 110 | } catch (UnsupportedEncodingException e) { 111 | throw new RuntimeException(e.getMessage(), e); 112 | } 113 | } 114 | return builder; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/test/java/tech/simter/jxls/ChartsTest.java: -------------------------------------------------------------------------------- 1 | package tech.simter.jxls; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.jxls.common.Context; 5 | import org.jxls.util.JxlsHelper; 6 | 7 | import java.io.File; 8 | import java.io.FileOutputStream; 9 | import java.io.InputStream; 10 | import java.io.OutputStream; 11 | import java.time.OffsetDateTime; 12 | import java.time.temporal.ChronoUnit; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | import static java.time.temporal.ChronoUnit.SECONDS; 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | import static org.junit.jupiter.api.Assertions.assertTrue; 19 | 20 | /** 21 | * Excel charts with fixed size collection test. 22 | * 23 | * @author RJ 24 | */ 25 | public class ChartsTest { 26 | @SuppressWarnings("ResultOfMethodCallIgnored") 27 | @Test 28 | public void test() throws Exception { 29 | // template 30 | InputStream template = getClass().getClassLoader().getResourceAsStream("templates/charts.xlsx"); 31 | 32 | // output to 33 | File out = new File("target/charts-result.xlsx"); 34 | if (out.exists()) out.delete(); 35 | OutputStream output = new FileOutputStream(out); 36 | 37 | // template data 38 | Context context = new Context(); 39 | List rows = new ArrayList<>(); 40 | rows.add(new Item("Derek", 3000, 2000)); 41 | rows.add(new Item("Elsa", 1500, 500)); 42 | rows.add(new Item("Oleg", 2300, 1300)); 43 | rows.add(new Item("Neil", 2500,1500)); 44 | rows.add(new Item("Maria", 1700, 700)); 45 | rows.add(new Item("John", 2800, 2000)); 46 | rows.add(new Item("Leonid", 1700, 1000)); 47 | context.putVar("items", rows); 48 | context.putVar("title", "X-Y"); 49 | context.putVar("ts", OffsetDateTime.now().truncatedTo(SECONDS).toString()); 50 | 51 | // render 52 | // 必须设置 .setEvaluateFormulas(true),否则生成的 Excel 文件, 53 | // 用到公式的地方不会显示公式的结果,需要双击单元格回车才能看到公式结果。 54 | JxlsHelper.getInstance() 55 | .setEvaluateFormulas(true) 56 | .processTemplate(template, output, context); 57 | 58 | // verify 59 | assertTrue(out.exists()); 60 | assertThat(out.getTotalSpace()).isGreaterThan(0); 61 | } 62 | 63 | public static class Item { 64 | private final String x; 65 | private final int y1; 66 | private final int y2; 67 | 68 | public Item(String x, int y1, int y2) { 69 | this.x = x; 70 | this.y1 = y1; 71 | this.y2 = y2; 72 | } 73 | 74 | public String getX() { 75 | return x; 76 | } 77 | 78 | public int getY1() { 79 | return y1; 80 | } 81 | 82 | public int getY2() { 83 | return y2; 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/test/java/tech/simter/jxls/FormulaTest.java: -------------------------------------------------------------------------------- 1 | package tech.simter.jxls; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.jxls.common.Context; 5 | import org.jxls.util.JxlsHelper; 6 | 7 | import java.io.File; 8 | import java.io.FileOutputStream; 9 | import java.io.InputStream; 10 | import java.io.OutputStream; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | import static org.junit.jupiter.api.Assertions.assertTrue; 16 | 17 | /** 18 | * Excel formula test. 19 | * 20 | * @author RJ 21 | */ 22 | public class FormulaTest { 23 | @Test 24 | public void test() throws Exception { 25 | // template 26 | InputStream template = getClass().getClassLoader().getResourceAsStream("templates/formula.xlsx"); 27 | 28 | // output to 29 | File out = new File("target/formula-result.xlsx"); 30 | if (out.exists()) out.delete(); 31 | OutputStream output = new FileOutputStream(out); 32 | 33 | // template data 34 | Context context = new Context(); 35 | List rows = new ArrayList<>(); 36 | rows.add(1); 37 | rows.add(2); 38 | rows.add(3); 39 | context.putVar("rows", rows); 40 | context.putVar("title", "测试"); 41 | 42 | // render 43 | // 必须设置 .setEvaluateFormulas(true),否则生成的 Excel 文件, 44 | // 用到公式的地方不会显示公式的结果,需要双击单元格回车才能看到公式结果。 45 | JxlsHelper.getInstance() 46 | .setEvaluateFormulas(true) 47 | .processTemplate(template, output, context); 48 | 49 | // verify 50 | assertTrue(out.exists()); 51 | assertThat(out.getTotalSpace()).isGreaterThan(0); 52 | } 53 | } -------------------------------------------------------------------------------- /src/test/java/tech/simter/jxls/ext/Bean.java: -------------------------------------------------------------------------------- 1 | package tech.simter.jxls.ext; 2 | 3 | public class Bean { 4 | private String p1; 5 | private Integer p2; 6 | private int p3; 7 | 8 | public Bean() { 9 | } 10 | 11 | public Bean(String p1) { 12 | this.p1 = p1; 13 | } 14 | 15 | public String getP1() { 16 | return p1; 17 | } 18 | 19 | public void setP1(String p1) { 20 | this.p1 = p1; 21 | } 22 | 23 | public Integer getP2() { 24 | return p2; 25 | } 26 | 27 | public void setP2(Integer p2) { 28 | this.p2 = p2; 29 | } 30 | 31 | public int getP3() { 32 | return p3; 33 | } 34 | 35 | public void setP3(int p3) { 36 | this.p3 = p3; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/tech/simter/jxls/ext/CommonFunctionsTest.java: -------------------------------------------------------------------------------- 1 | package tech.simter.jxls.ext; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.jxls.common.Context; 5 | import org.jxls.util.JxlsHelper; 6 | 7 | import java.io.File; 8 | import java.io.FileOutputStream; 9 | import java.io.InputStream; 10 | import java.io.OutputStream; 11 | import java.math.BigDecimal; 12 | import java.time.*; 13 | import java.time.temporal.TemporalAccessor; 14 | import java.util.Arrays; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | 18 | import static org.junit.jupiter.api.Assertions.assertEquals; 19 | import static org.junit.jupiter.api.Assertions.assertNull; 20 | 21 | /** 22 | * Common Functions test. 23 | * 24 | * @author RJ 25 | */ 26 | class CommonFunctionsTest { 27 | private final CommonFunctions fn = CommonFunctions.getSingleton(); 28 | 29 | @Test 30 | void formatNumber() { 31 | assertNull(fn.round(null, 0)); 32 | assertEquals("123,456,789.10", fn.format(123456789.1, "#,###.00")); 33 | assertEquals("123,456,789.12", fn.format(123456789.12, "#,###.00")); 34 | assertEquals("123,456,789.12", fn.format(123456789.124, "#,###.00")); 35 | assertEquals("123,456,789.12", fn.format(123456789.125, "#,###.00")); 36 | assertEquals("123,456,789.13", fn.format(123456789.126, "#,###.00")); 37 | assertEquals("1,123,456,789.00", fn.format(1123456789, "#,###.00")); 38 | } 39 | 40 | @Test 41 | void roundNumber() { 42 | assertNull(fn.round(null, 0)); 43 | assertNull(fn.round(null, 1)); 44 | assertNull(fn.round(null, 2)); 45 | assertEquals(new BigDecimal(123456789), fn.round(123456789, 0)); 46 | assertEquals(new BigDecimal(123456789), fn.round(123456789.45, 0)); 47 | assertEquals(new BigDecimal("123456789.5"), fn.round(123456789.45, 1)); 48 | assertEquals(new BigDecimal("123456789.45"), fn.round(123456789.454, 2)); 49 | assertEquals(new BigDecimal("123456789.46"), fn.round(123456789.455, 2)); 50 | assertEquals(new BigDecimal("123456789.46"), fn.round(123456789.456, 2)); 51 | } 52 | 53 | @Test 54 | void toInt() { 55 | assertNull(fn.toInt(null)); 56 | assertEquals(123, fn.toInt("123")); 57 | } 58 | 59 | @Test 60 | void concatString() { 61 | assertNull(fn.concat()); 62 | assertEquals("abc", fn.concat("abc")); 63 | assertEquals("abc", fn.concat("ab", "c")); 64 | } 65 | 66 | @Test 67 | void formatDateTime() { 68 | assertNull(fn.format((TemporalAccessor) null, null)); 69 | assertEquals("2017", fn.format(Year.of(2017), "yyyy")); 70 | assertEquals("2017-01", fn.format(YearMonth.of(2017, 1), "yyyy-MM")); 71 | assertEquals("01", fn.format(Month.of(1), "MM")); 72 | assertEquals("2017-01-02", fn.format(LocalDate.of(2017, 1, 2), "yyyy-MM-dd")); 73 | assertEquals("2017/1/12", fn.format(LocalDate.of(2017, 1, 12), "yyyy/M/d")); 74 | assertEquals("2017-01-02 10:20:30", fn.format(LocalDateTime.of(2017, 1, 2, 10, 20, 30), "yyyy-MM-dd HH:mm:ss")); 75 | assertEquals("13:20:30", fn.format(LocalTime.of(13, 20, 30), "HH:mm:ss")); 76 | } 77 | 78 | @Test 79 | void formatDuration() { 80 | assertNull(fn.format((Duration) null)); 81 | assertEquals("1D", fn.format(Duration.ofDays(1))); 82 | assertEquals("1H", fn.format(Duration.ofHours(1))); 83 | assertEquals("1M", fn.format(Duration.ofMinutes(1))); 84 | assertEquals("1S", fn.format(Duration.ofSeconds(1))); 85 | assertEquals("1D1H", fn.format(Duration.ofDays(1).plusHours(1))); 86 | assertEquals("23H", fn.format(Duration.ofDays(1).minusHours(1))); 87 | assertEquals("-1D", fn.format(Duration.ofDays(-1))); 88 | assertEquals("-1D1H", fn.format(Duration.ofDays(-1).plusHours(-1))); 89 | assertEquals("-23H", fn.format(Duration.ofDays(-1).plusHours(1))); 90 | } 91 | 92 | @Test 93 | void duration() { 94 | // null 95 | assertNull(fn.duration(null, null)); 96 | assertNull(fn.duration(null, LocalDateTime.now())); 97 | assertNull(fn.duration(LocalDateTime.now(), null)); 98 | 99 | LocalDateTime startTime = LocalDateTime.of(2020, 1, 1, 0, 0, 0); 100 | assertEquals("1D", fn.duration(startTime, startTime.plusDays(1))); 101 | assertEquals("-1D", fn.duration(startTime.plusDays(1), startTime)); 102 | assertEquals("1H", fn.duration(startTime, startTime.plusHours(1))); 103 | assertEquals("-1H", fn.duration(startTime.plusHours(1), startTime)); 104 | assertEquals("1M", fn.duration(startTime, startTime.plusMinutes(1))); 105 | assertEquals("-1M", fn.duration(startTime.plusMinutes(1), startTime)); 106 | assertEquals("1S", fn.duration(startTime, startTime.plusSeconds(1))); 107 | assertEquals("-1S", fn.duration(startTime.plusSeconds(1), startTime)); 108 | 109 | assertEquals("1D1H", fn.duration(startTime, startTime.plusDays(1).plusHours(1))); 110 | assertEquals("-1D1H", fn.duration(startTime.plusDays(1).plusHours(1), startTime)); 111 | assertEquals("1D1H", fn.duration(startTime, startTime.plusHours(24 + 1))); 112 | assertEquals("-1D1H", fn.duration(startTime.plusHours(24 + 1), startTime)); 113 | 114 | assertEquals("1D1M", fn.duration(startTime, startTime.plusDays(1).plusMinutes(1))); 115 | assertEquals("-1D1M", fn.duration(startTime.plusDays(1).plusMinutes(1), startTime)); 116 | assertEquals("1D1M", fn.duration(startTime, startTime.plusMinutes(24 * 60 + 1))); 117 | assertEquals("-1D1M", fn.duration(startTime.plusMinutes(24 * 60 + 1), startTime)); 118 | 119 | assertEquals("1D1S", fn.duration(startTime, startTime.plusDays(1).plusSeconds(1))); 120 | assertEquals("-1D1S", fn.duration(startTime.plusDays(1).plusSeconds(1), startTime)); 121 | assertEquals("1D1S", fn.duration(startTime, startTime.plusSeconds(24 * 60 * 60 + 1))); 122 | assertEquals("-1D1S", fn.duration(startTime.plusSeconds(24 * 60 * 60 + 1), startTime)); 123 | } 124 | 125 | @Test 126 | void join() { 127 | assertEquals("s1, s2", fn.join(Arrays.asList("s1", null, "s2"))); 128 | assertEquals("s1-s2", fn.join(Arrays.asList("s1", null, "s2"), "-")); 129 | } 130 | 131 | @Test 132 | void joinProperty() { 133 | Map map = new HashMap<>(); 134 | map.put("p1", "m1"); 135 | Bean bean = new Bean(); 136 | bean.setP1("b1"); 137 | assertEquals("m1, b1", fn.joinProperty(Arrays.asList(map, bean), "p1")); 138 | assertEquals("m1,b1", fn.joinProperty(Arrays.asList(map, bean), "p1", ",")); 139 | assertEquals("", fn.joinProperty(Arrays.asList(map, bean), "p2", ",")); 140 | 141 | bean = new Bean(); 142 | bean.setP2(2); 143 | assertEquals("2", fn.joinProperty(Arrays.asList(map, bean), "p2", ",")); 144 | 145 | bean = new Bean(); 146 | bean.setP3(3); 147 | assertEquals("3", fn.joinProperty(Arrays.asList(map, bean), "p3", ",")); 148 | } 149 | 150 | @Test 151 | void renderTemplate() throws Exception { 152 | // template 153 | InputStream template = getClass().getClassLoader().getResourceAsStream("templates/common-functions.xlsx"); 154 | 155 | // output to 156 | File out = new File("target/common-functions-result.xlsx"); 157 | if (out.exists()) out.delete(); 158 | OutputStream output = new FileOutputStream(out); 159 | 160 | // data 161 | Context context = new Context(); 162 | 163 | //-- inject common-functions 164 | context.putVar("fn", CommonFunctions.getSingleton()); 165 | 166 | //-- other data 167 | context.putVar("num", new BigDecimal("123.456")); 168 | context.putVar("datetime", LocalDateTime.now()); 169 | context.putVar("date", LocalDate.now()); 170 | context.putVar("time", LocalTime.now()); 171 | context.putVar("str", "123"); 172 | context.putVar("beans", Arrays.asList(new Bean("p1"), new Bean("p2"))); 173 | context.putVar("strings", Arrays.asList("s1", "s2")); 174 | 175 | // render 176 | JxlsHelper.getInstance().processTemplate(template, output, context); 177 | } 178 | } -------------------------------------------------------------------------------- /src/test/java/tech/simter/jxls/ext/DynamicColumnTest.java: -------------------------------------------------------------------------------- 1 | package tech.simter.jxls.ext; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.io.File; 6 | import java.io.FileOutputStream; 7 | import java.io.InputStream; 8 | import java.io.OutputStream; 9 | import java.util.*; 10 | 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | 13 | /** 14 | * The Jxls test. 15 | * 16 | * @author RJ 17 | */ 18 | class DynamicColumnTest { 19 | @Test 20 | void test() throws Exception { 21 | // template 22 | InputStream template = getClass().getClassLoader().getResourceAsStream("templates/dynamic-column.xlsx"); 23 | 24 | // output to 25 | File out = new File("target/dynamic-column-result.xlsx"); 26 | if (out.exists()) out.delete(); 27 | OutputStream output = new FileOutputStream(out); 28 | 29 | // template data 30 | Map data = generateData(); 31 | 32 | // render 33 | JxlsUtils.renderTemplate(template, data, output); 34 | 35 | // verify 36 | assertThat(out.getTotalSpace()).isGreaterThan(0); 37 | } 38 | 39 | private Map generateData() { 40 | Map data = new HashMap<>(); 41 | data.put("subject", "JXLS dynamic columns test"); 42 | 43 | List> rows = new ArrayList<>(); 44 | data.put("rows", rows); 45 | List itemNames = Arrays.asList("Item1", "Item2", "Item3"); 46 | data.put("itemNames", itemNames); 47 | int rowNumber = 0; 48 | rows.add(createRow(++rowNumber, itemNames)); 49 | rows.add(createRow(++rowNumber, itemNames)); 50 | rows.add(createRow(++rowNumber, itemNames)); 51 | 52 | return data; 53 | } 54 | 55 | private Map createRow(int rowNumber, List itemNames) { 56 | Map row = new HashMap<>(); 57 | row.put("sn", rowNumber); 58 | row.put("name", "row" + rowNumber); 59 | 60 | List itemValues = new ArrayList<>(); 61 | int i = 0; 62 | for (String ignored : itemNames) { 63 | itemValues.add(100000000.345678 * rowNumber + (++i)); 64 | } 65 | row.put("itemValues", itemValues); 66 | 67 | return row; 68 | } 69 | } -------------------------------------------------------------------------------- /src/test/java/tech/simter/jxls/ext/EachMergeCommandTest.java: -------------------------------------------------------------------------------- 1 | package tech.simter.jxls.ext; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.jxls.builder.xls.XlsCommentAreaBuilder; 5 | import org.jxls.common.Context; 6 | import org.jxls.util.JxlsHelper; 7 | 8 | import java.io.File; 9 | import java.io.FileOutputStream; 10 | import java.io.InputStream; 11 | import java.io.OutputStream; 12 | import java.util.ArrayList; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | import static tech.simter.jxls.ext.JxlsUtils.convert2Context; 19 | 20 | /** 21 | * The each-merge command test. 22 | * 23 | * @author RJ 24 | */ 25 | class EachMergeCommandTest { 26 | // one main command with one sub command 27 | @Test 28 | void mergeWithOneSubCommand() throws Exception { 29 | // global add custom each-merge command to XlsCommentAreaBuilder 30 | XlsCommentAreaBuilder.addCommandMapping(EachMergeCommand.COMMAND_NAME, EachMergeCommand.class); 31 | 32 | // template 33 | InputStream template = getClass().getClassLoader().getResourceAsStream("templates/each-merge.xlsx"); 34 | 35 | // output to 36 | File out = new File("target/each-merge-result1.xlsx"); 37 | if (out.exists()) out.delete(); 38 | OutputStream output = new FileOutputStream(out); 39 | 40 | // generate template data 41 | Context context = convert2Context(generateData()); 42 | 43 | // render 44 | JxlsHelper.getInstance().processTemplate(template, output, context); 45 | 46 | // verify 47 | assertThat(out.getTotalSpace()).isGreaterThan(0); 48 | } 49 | 50 | @Test 51 | void mergeWithOneSubCommand1() throws Exception { 52 | // template 53 | InputStream template = getClass().getClassLoader().getResourceAsStream("templates/each-merge.xlsx"); 54 | 55 | // output to 56 | File out = new File("target/each-merge-result2.xlsx"); 57 | if (out.exists()) out.delete(); 58 | OutputStream output = new FileOutputStream(out); 59 | 60 | // generate template data 61 | Map data = generateData(); 62 | 63 | // render 64 | JxlsUtils.renderTemplate(template, data, output); 65 | 66 | // verify 67 | assertThat(out.getTotalSpace()).isGreaterThan(0); 68 | } 69 | 70 | // one main command with two sub commands 71 | @Test 72 | void mergeWithTwoSubCommand() throws Exception { 73 | // template 74 | InputStream template = getClass().getClassLoader().getResourceAsStream("templates/each-merge2.xlsx"); 75 | 76 | // output to 77 | File out = new File("target/each-merge-result3.xlsx"); 78 | if (out.exists()) out.delete(); 79 | OutputStream output = new FileOutputStream(out); 80 | 81 | // generate template data 82 | Map data = generateData(); 83 | copySubsToSubs1(data); // create the second sub command data 84 | 85 | // render 86 | JxlsUtils.renderTemplate(template, data, output); 87 | 88 | // verify 89 | assertThat(out.getTotalSpace()).isGreaterThan(0); 90 | } 91 | 92 | @SuppressWarnings("unchecked") 93 | private void copySubsToSubs1(Map data) { 94 | ((List>) data.get("rows")).forEach(row -> { 95 | List> subs = (List>) row.get("subs"); 96 | List> subs1 = new ArrayList<>(); 97 | row.put("subs1", subs1); 98 | subs.forEach(sub -> { 99 | HashMap newMap = new HashMap<>(sub); 100 | for (Map.Entry e : newMap.entrySet()) { 101 | e.setValue(e.getValue() + "-s"); 102 | } 103 | subs1.add(newMap); 104 | }); 105 | }); 106 | } 107 | 108 | private static Map generateData() { 109 | Map data = new HashMap<>(); 110 | data.put("subject", "JXLS merge cell test"); 111 | 112 | List> rows = new ArrayList<>(); 113 | data.put("rows", rows); 114 | int rowNumber = 0; 115 | //rows.add(createRow(++rowNumber, 3)); 116 | rows.add(createRow(++rowNumber, 3)); 117 | rows.add(createRow(++rowNumber, 1)); 118 | rows.add(createRow(++rowNumber, 2)); 119 | //rows.add(createRow(++rowNumber, 0)); 120 | 121 | return data; 122 | } 123 | 124 | private static Map createRow(int rowNumber, int subsCount) { 125 | Map row = new HashMap<>(); 126 | row.put("sn", rowNumber); 127 | row.put("name", "row" + rowNumber); 128 | if (subsCount >= 0) row.put("subs", createSubs(rowNumber, subsCount)); 129 | return row; 130 | } 131 | 132 | private static List> createSubs(int rowNumber, int count) { 133 | List> subs = new ArrayList<>(); 134 | Map sub; 135 | for (int i = 1; i <= count; i++) { 136 | sub = new HashMap<>(); 137 | subs.add(sub); 138 | sub.put("sn", rowNumber + "-" + i); 139 | sub.put("name", "row" + rowNumber + "sub" + +i); 140 | } 141 | return subs; 142 | } 143 | } -------------------------------------------------------------------------------- /src/test/java/tech/simter/jxls/ext/JxlsUtilsTest.java: -------------------------------------------------------------------------------- 1 | package tech.simter.jxls.ext; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.io.File; 6 | import java.io.FileOutputStream; 7 | import java.io.InputStream; 8 | import java.io.OutputStream; 9 | import java.math.BigDecimal; 10 | import java.time.LocalDateTime; 11 | import java.time.YearMonth; 12 | import java.util.ArrayList; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | import static java.math.RoundingMode.HALF_UP; 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.junit.jupiter.api.Assertions.assertTrue; 20 | 21 | /** 22 | * The Excel Utils test. 23 | * 24 | * @author RJ 25 | */ 26 | class JxlsUtilsTest { 27 | @Test 28 | void xlsx() throws Exception { 29 | // template 30 | InputStream template = getClass().getClassLoader().getResourceAsStream("templates/common-functions-complex.xlsx"); 31 | 32 | // output to 33 | File out = new File("target/common-functions-complex-result.xlsx"); 34 | if (out.exists()) out.delete(); 35 | OutputStream output = new FileOutputStream(out); 36 | 37 | // template data 38 | Map data = generateData(); 39 | 40 | // render 41 | JxlsUtils.renderTemplate(template, data, output); 42 | 43 | // verify 44 | assertTrue(out.exists()); 45 | assertThat(out.getTotalSpace()).isGreaterThan(0); 46 | } 47 | 48 | @Test 49 | void xls() throws Exception { 50 | // from template 51 | InputStream template = getClass().getClassLoader().getResourceAsStream("templates/common-functions-complex.xls"); 52 | 53 | // output to 54 | File out = new File("target/common-functions-complex-result.xls"); 55 | if (out.exists()) out.delete(); 56 | OutputStream output = new FileOutputStream(out); 57 | 58 | // template data 59 | Map data = generateData(); 60 | 61 | // render 62 | JxlsUtils.renderTemplate(template, data, output); 63 | 64 | // verify 65 | assertTrue(out.exists()); 66 | assertThat(out.getTotalSpace()).isGreaterThan(0); 67 | } 68 | 69 | // generate test data 70 | private Map generateData() { 71 | Map data = new HashMap<>(); 72 | data.put("stage", 0); 73 | Map stageLabels = new HashMap<>(); 74 | stageLabels.put(0, "TODO"); 75 | stageLabels.put(1, "ALLOW"); 76 | data.put("stageLabels", stageLabels); 77 | 78 | List> rows = new ArrayList<>(); 79 | data.put("rows", rows); 80 | Map row; 81 | for (int i = 0; i < 10; i++) { 82 | row = new HashMap<>(); 83 | rows.add(row); 84 | row.put("yearMonth", YearMonth.now()); 85 | row.put("dateTime", LocalDateTime.now()); 86 | row.put("str", "test"); 87 | row.put("money", new BigDecimal("100.01").add(BigDecimal.valueOf((float) i / 1000)).setScale(2, HALF_UP)); 88 | row.put("stage", i % 2); 89 | if (i % 2 == 0) { 90 | row.put("remark", "remark" + (i + 1)); 91 | } 92 | } 93 | return data; 94 | } 95 | } -------------------------------------------------------------------------------- /src/test/java/tech/simter/jxls/ext/TwoSubListTest.java: -------------------------------------------------------------------------------- 1 | package tech.simter.jxls.ext; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.io.File; 6 | import java.io.FileOutputStream; 7 | import java.io.InputStream; 8 | import java.io.OutputStream; 9 | import java.util.ArrayList; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | 16 | /** 17 | * The Jxls test. 18 | * 19 | * @author RJ 20 | */ 21 | class TwoSubListTest { 22 | @Test 23 | void test() throws Exception { 24 | // template 25 | InputStream template = getClass().getClassLoader().getResourceAsStream("templates/two-sub-list.xlsx"); 26 | 27 | // output to 28 | File out = new File("target/two-sub-list-result.xlsx"); 29 | if (out.exists()) out.delete(); 30 | OutputStream output = new FileOutputStream(out); 31 | 32 | // template data 33 | Map data = generateData(); 34 | 35 | // render 36 | JxlsUtils.renderTemplate(template, data, output); 37 | 38 | // verify 39 | assertThat(out.getTotalSpace()).isGreaterThan(0); 40 | } 41 | 42 | private Map generateData() { 43 | Map data = new HashMap<>(); 44 | data.put("subject", "JXLS two-sub-list test"); 45 | 46 | List> rows = new ArrayList<>(); 47 | data.put("rows", rows); 48 | int rowNumber = 0; 49 | boolean align = true; 50 | rows.add(createRow(++rowNumber, 2, 2, align)); 51 | rows.add(createRow(++rowNumber, 2, 1, align)); 52 | rows.add(createRow(++rowNumber, 2, 0, align)); // 0 代表空的集合(不是 null 而是 size = 0) 53 | 54 | // -1 代表不存在的集合,会导致 jx:each 报错:org.jxls.common.JxlsException: r.subs2 expression is not a collection 55 | //rows.add(createRow(++rowNumber, 2, -1, align)); 56 | 57 | rows.add(createRow(++rowNumber, 1, 1, align)); 58 | rows.add(createRow(++rowNumber, 1, 0, align)); 59 | rows.add(createRow(++rowNumber, 0, 0, align)); 60 | 61 | rows.add(createRow(++rowNumber, 1, 2, align)); 62 | rows.add(createRow(++rowNumber, 0, 2, align)); 63 | 64 | return data; 65 | } 66 | 67 | @SuppressWarnings("unchecked") 68 | private Map createRow(int rowNumber, int leftSubsCount, int rightSubsCount, boolean align) { 69 | Map row = new HashMap<>(); 70 | row.put("sn", rowNumber); 71 | row.put("name", "row" + rowNumber); 72 | if (leftSubsCount >= 0) row.put("subs1", createSubs(rowNumber, 1, leftSubsCount)); 73 | if (rightSubsCount >= 0) row.put("subs2", createSubs(rowNumber, 2, rightSubsCount)); 74 | 75 | // 如果 leftSubsCount != rightSubsCount, 取两者的最大值,将各自的集合追加空元素来对齐集合的总长度 76 | // 目的是保证不会生成没有格式的空白单元格 77 | List> subs; 78 | if (align && (leftSubsCount != rightSubsCount || leftSubsCount == 0)) { 79 | int maxCount = Math.max(1, Math.max(leftSubsCount, rightSubsCount)); 80 | if (leftSubsCount < maxCount) { 81 | subs = (List>) row.get("subs1"); 82 | for (int i = 0; i < maxCount - leftSubsCount; i++) subs.add(new HashMap<>()); 83 | } 84 | if (rightSubsCount < maxCount) { 85 | subs = (List>) row.get("subs2"); 86 | for (int i = 0; i < maxCount - rightSubsCount; i++) subs.add(new HashMap<>()); 87 | } 88 | } 89 | return row; 90 | } 91 | 92 | private List> createSubs(int rowNumber, int subNumber, int count) { 93 | List> subs = new ArrayList<>(); 94 | Map sub; 95 | for (int i = 1; i <= count; i++) { 96 | sub = new HashMap<>(); 97 | subs.add(sub); 98 | sub.put("sn", rowNumber + "-" + i); 99 | sub.put("name", "row" + rowNumber + "sub" + subNumber + "-" + i); 100 | } 101 | return subs; 102 | } 103 | } -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/test/resources/templates/charts.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simter/simter-jxls-ext/b88f95d5fa0d9090c16b0f1ed1ea908932967077/src/test/resources/templates/charts.xlsx -------------------------------------------------------------------------------- /src/test/resources/templates/common-functions-complex.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simter/simter-jxls-ext/b88f95d5fa0d9090c16b0f1ed1ea908932967077/src/test/resources/templates/common-functions-complex.xls -------------------------------------------------------------------------------- /src/test/resources/templates/common-functions-complex.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simter/simter-jxls-ext/b88f95d5fa0d9090c16b0f1ed1ea908932967077/src/test/resources/templates/common-functions-complex.xlsx -------------------------------------------------------------------------------- /src/test/resources/templates/common-functions.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simter/simter-jxls-ext/b88f95d5fa0d9090c16b0f1ed1ea908932967077/src/test/resources/templates/common-functions.xlsx -------------------------------------------------------------------------------- /src/test/resources/templates/dynamic-column.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simter/simter-jxls-ext/b88f95d5fa0d9090c16b0f1ed1ea908932967077/src/test/resources/templates/dynamic-column.xlsx -------------------------------------------------------------------------------- /src/test/resources/templates/each-merge.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simter/simter-jxls-ext/b88f95d5fa0d9090c16b0f1ed1ea908932967077/src/test/resources/templates/each-merge.xlsx -------------------------------------------------------------------------------- /src/test/resources/templates/each-merge2.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simter/simter-jxls-ext/b88f95d5fa0d9090c16b0f1ed1ea908932967077/src/test/resources/templates/each-merge2.xlsx -------------------------------------------------------------------------------- /src/test/resources/templates/formula.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simter/simter-jxls-ext/b88f95d5fa0d9090c16b0f1ed1ea908932967077/src/test/resources/templates/formula.xlsx -------------------------------------------------------------------------------- /src/test/resources/templates/two-sub-list.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simter/simter-jxls-ext/b88f95d5fa0d9090c16b0f1ed1ea908932967077/src/test/resources/templates/two-sub-list.xlsx --------------------------------------------------------------------------------