Numeric Array Formula and Matrix Function [from Bob95132] This closes #69
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1808297 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
e726dd68de
commit
5ef2f1636c
@ -34,5 +34,6 @@
|
||||
<classpathentry exported="true" kind="lib" path="lib/commons-codec-1.10.jar"/>
|
||||
<classpathentry exported="true" kind="lib" path="lib/commons-logging-1.2.jar"/>
|
||||
<classpathentry exported="true" kind="lib" path="lib/commons-collections4-4.1.jar"/>
|
||||
<classpathentry kind="lib" path="lib/commons-math3-3.6.1.jar"/>
|
||||
<classpathentry kind="output" path="bin"/>
|
||||
</classpath>
|
||||
|
@ -73,7 +73,7 @@ subprojects {
|
||||
// See https://github.com/melix/japicmp-gradle-plugin
|
||||
apply plugin: 'me.champeau.gradle.japicmp'
|
||||
|
||||
version = '3.16-beta3'
|
||||
version = '3.18-beta1'
|
||||
ext {
|
||||
japicmpversion = '3.15'
|
||||
}
|
||||
@ -150,6 +150,7 @@ project('main') {
|
||||
compile 'commons-codec:commons-codec:1.10'
|
||||
compile 'commons-logging:commons-logging:1.2'
|
||||
compile 'org.apache.commons:commons-collections4:4.1'
|
||||
compile 'org.apache.commons:commons-math3:3.6.1'
|
||||
|
||||
testCompile 'junit:junit:4.12'
|
||||
}
|
||||
@ -196,6 +197,7 @@ project('ooxml') {
|
||||
dependencies {
|
||||
compile 'org.apache.xmlbeans:xmlbeans:2.6.0'
|
||||
compile 'org.apache.commons:commons-collections4:4.1'
|
||||
compile 'org.apache.commons:commons-math3:3.6.1'
|
||||
compile 'org.apache.santuario:xmlsec:2.0.6'
|
||||
compile 'org.bouncycastle:bcpkix-jdk15on:1.54'
|
||||
compile 'com.github.virtuald:curvesapi:1.04'
|
||||
|
@ -178,6 +178,9 @@ under the License.
|
||||
<property name="main.commons-collections4.jar" location="${main.lib}/commons-collections4-4.1.jar"/>
|
||||
<property name="main.commons-collections4.url"
|
||||
value="${repository.m2}/maven2/org/apache/commons/commons-collections4/4.1/commons-collections4-4.1.jar"/>
|
||||
<property name="main.commons-math3.jar" location="${main.lib}/commons-math3-3.6.1.jar"/>
|
||||
<property name="main.commons-math3.url"
|
||||
value="${repository.m2}/maven2/org/apache/commons/commons-math3/3.6.1/commons-math3-3.6.1.jar"/>
|
||||
|
||||
<!-- xml signature libs -->
|
||||
<property name="dsig.xmlsec.jar" location="${compile.lib}/xmlsec-2.0.6.jar"/>
|
||||
@ -326,6 +329,7 @@ under the License.
|
||||
<pathelement location="${main.commons-logging.jar}"/>
|
||||
<pathelement location="${main.commons-codec.jar}"/>
|
||||
<pathelement location="${main.commons-collections4.jar}"/>
|
||||
<pathelement location="${main.commons-math3.jar}"/>
|
||||
</path>
|
||||
|
||||
<!-- some libraries should only be required for compiling/running tests -->
|
||||
@ -631,6 +635,7 @@ under the License.
|
||||
<available file="${dsig.xmlsec.jar}"/>
|
||||
<available file="${dsig.sl4j-api.jar}"/>
|
||||
<available file="${main.commons-collections4.jar}"/>
|
||||
<available file="${main.commons-math3.jar}"/>
|
||||
</and>
|
||||
<isset property="disconnected"/>
|
||||
</or>
|
||||
@ -652,6 +657,7 @@ under the License.
|
||||
<downloadfile src="${asm.url}" dest="${asm.jar}"/>
|
||||
<downloadfile src="${jacoco.url}" dest="${jacoco.zip}"/>
|
||||
<downloadfile src="${main.commons-collections4.url}" dest="${main.commons-collections4.jar}"/>
|
||||
<downloadfile src="${main.commons-math3.url}" dest="${main.commons-math3.jar}"/>
|
||||
<unzip src="${jacoco.zip}" dest=".">
|
||||
<patternset>
|
||||
<include name="lib/*.jar"/>
|
||||
@ -2044,6 +2050,7 @@ under the License.
|
||||
<include name="commons-codec-*.jar"/>
|
||||
<include name="commons-logging-*.jar"/>
|
||||
<include name="commons-collections4-*.jar"/>
|
||||
<include name="commons-math3-*.jar"/>
|
||||
<include name="junit-*.jar"/>
|
||||
<include name="log4j-*.jar"/>
|
||||
</fileset>
|
||||
@ -2306,6 +2313,7 @@ under the License.
|
||||
<auxClasspath path="${ooxml.curvesapi.jar}" />
|
||||
<auxClasspath path="${ooxml.xmlbeans26.jar}" />
|
||||
<auxClasspath path="${main.commons-collections4.jar}" />
|
||||
<auxClasspath path="${main.commons-math3.jar}" />
|
||||
<auxClasspath path="${main.commons-codec.jar}" />
|
||||
<auxClasspath path="${main.commons-logging.jar}" />
|
||||
<auxClasspath path="${main.junit.jar}" />
|
||||
|
@ -115,6 +115,11 @@
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
<version>4.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-math3</artifactId>
|
||||
<version>3.6.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
|
@ -20,6 +20,8 @@ package org.apache.poi.hssf.usermodel;
|
||||
import org.apache.poi.ss.formula.EvaluationCell;
|
||||
import org.apache.poi.ss.formula.EvaluationSheet;
|
||||
import org.apache.poi.ss.usermodel.CellType;
|
||||
import org.apache.poi.ss.util.CellRangeAddress;
|
||||
|
||||
/**
|
||||
* HSSF wrapper for a cell under evaluation
|
||||
*/
|
||||
@ -93,6 +95,17 @@ final class HSSFEvaluationCell implements EvaluationCell {
|
||||
public String getStringCellValue() {
|
||||
return _cell.getRichStringCellValue().getString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellRangeAddress getArrayFormulaRange() {
|
||||
return _cell.getArrayFormulaRange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPartOfArrayFormulaGroup() {
|
||||
return _cell.isPartOfArrayFormulaGroup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Will return {@link CellType} in a future version of POI.
|
||||
* For forwards compatibility, do not hard-code cell type literals in your code.
|
||||
|
131
src/java/org/apache/poi/ss/formula/CacheAreaEval.java
Normal file
131
src/java/org/apache/poi/ss/formula/CacheAreaEval.java
Normal file
@ -0,0 +1,131 @@
|
||||
/* ====================================================================
|
||||
Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You 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 org.apache.poi.ss.formula;
|
||||
|
||||
import org.apache.poi.ss.formula.TwoDEval;
|
||||
import org.apache.poi.ss.formula.eval.AreaEval;
|
||||
import org.apache.poi.ss.formula.eval.AreaEvalBase;
|
||||
import org.apache.poi.ss.formula.eval.BlankEval;
|
||||
import org.apache.poi.ss.formula.eval.ValueEval;
|
||||
import org.apache.poi.ss.formula.ptg.AreaI;
|
||||
import org.apache.poi.ss.formula.ptg.AreaI.OffsetArea;
|
||||
import org.apache.poi.ss.util.CellReference;
|
||||
|
||||
/**
|
||||
* @author Robert Hulbert
|
||||
* Provides holding structure for temporary values in arrays during the evaluation process.
|
||||
* As such, Row/Column references do not actually correspond to data in the file.
|
||||
*/
|
||||
|
||||
public final class CacheAreaEval extends AreaEvalBase {
|
||||
|
||||
/* Value Containter */
|
||||
private final ValueEval[] _values;
|
||||
|
||||
public CacheAreaEval(AreaI ptg, ValueEval[] values) {
|
||||
super(ptg);
|
||||
_values = values;
|
||||
}
|
||||
|
||||
public CacheAreaEval(int firstRow, int firstColumn, int lastRow, int lastColumn, ValueEval[] values) {
|
||||
super(firstRow, firstColumn, lastRow, lastColumn);
|
||||
_values = values;
|
||||
}
|
||||
|
||||
public ValueEval getRelativeValue(int relativeRowIndex, int relativeColumnIndex) {
|
||||
return getRelativeValue(-1, relativeRowIndex, relativeColumnIndex);
|
||||
}
|
||||
|
||||
public ValueEval getRelativeValue(int sheetIndex, int relativeRowIndex, int relativeColumnIndex) {
|
||||
int oneDimensionalIndex = relativeRowIndex * getWidth() + relativeColumnIndex;
|
||||
return _values[oneDimensionalIndex];
|
||||
}
|
||||
|
||||
public AreaEval offset(int relFirstRowIx, int relLastRowIx,
|
||||
int relFirstColIx, int relLastColIx) {
|
||||
|
||||
AreaI area = new OffsetArea(getFirstRow(), getFirstColumn(),
|
||||
relFirstRowIx, relLastRowIx, relFirstColIx, relLastColIx);
|
||||
|
||||
int height = area.getLastRow() - area.getFirstRow() + 1;
|
||||
int width = area.getLastColumn() - area.getFirstColumn() + 1;
|
||||
|
||||
ValueEval newVals[] = new ValueEval[height * width];
|
||||
|
||||
int startRow = area.getFirstRow() - getFirstRow();
|
||||
int startCol = area.getFirstColumn() - getFirstColumn();
|
||||
|
||||
for (int j = 0; j < height; j++) {
|
||||
for (int i = 0; i < width; i++) {
|
||||
ValueEval temp;
|
||||
|
||||
/* CacheAreaEval is only temporary value representation, does not equal sheet selection
|
||||
* so any attempts going beyond the selection results in BlankEval
|
||||
*/
|
||||
if (startRow + j > getLastRow() || startCol + i > getLastColumn()) {
|
||||
temp = BlankEval.instance;
|
||||
}
|
||||
else {
|
||||
temp = _values[(startRow + j) * getWidth() + (startCol + i)];
|
||||
}
|
||||
newVals[j * width + i] = temp;
|
||||
}
|
||||
}
|
||||
|
||||
return new CacheAreaEval(area, newVals);
|
||||
}
|
||||
|
||||
public TwoDEval getRow(int rowIndex) {
|
||||
if (rowIndex >= getHeight()) {
|
||||
throw new IllegalArgumentException("Invalid rowIndex " + rowIndex
|
||||
+ ". Allowable range is (0.." + getHeight() + ").");
|
||||
}
|
||||
int absRowIndex = getFirstRow() + rowIndex;
|
||||
ValueEval[] values = new ValueEval[getWidth()];
|
||||
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
values[i] = getRelativeValue(rowIndex, i);
|
||||
}
|
||||
return new CacheAreaEval(absRowIndex, getFirstColumn() , absRowIndex, getLastColumn(), values);
|
||||
}
|
||||
|
||||
public TwoDEval getColumn(int columnIndex) {
|
||||
if (columnIndex >= getWidth()) {
|
||||
throw new IllegalArgumentException("Invalid columnIndex " + columnIndex
|
||||
+ ". Allowable range is (0.." + getWidth() + ").");
|
||||
}
|
||||
int absColIndex = getFirstColumn() + columnIndex;
|
||||
ValueEval[] values = new ValueEval[getHeight()];
|
||||
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
values[i] = getRelativeValue(i, columnIndex);
|
||||
}
|
||||
|
||||
return new CacheAreaEval(getFirstRow(), absColIndex, getLastRow(), absColIndex, values);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
CellReference crA = new CellReference(getFirstRow(), getFirstColumn());
|
||||
CellReference crB = new CellReference(getLastRow(), getLastColumn());
|
||||
return getClass().getName() + "[" +
|
||||
crA.formatAsString() +
|
||||
':' +
|
||||
crB.formatAsString() +
|
||||
"]";
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@
|
||||
package org.apache.poi.ss.formula;
|
||||
|
||||
import org.apache.poi.ss.usermodel.CellType;
|
||||
import org.apache.poi.ss.util.CellRangeAddress;
|
||||
|
||||
/**
|
||||
* Abstracts a cell for the purpose of formula evaluation. This interface represents both formula
|
||||
@ -56,6 +57,8 @@ public interface EvaluationCell {
|
||||
String getStringCellValue();
|
||||
boolean getBooleanCellValue();
|
||||
int getErrorCellValue();
|
||||
CellRangeAddress getArrayFormulaRange();
|
||||
boolean isPartOfArrayFormulaGroup();
|
||||
|
||||
/**
|
||||
* Will return {@link CellType} in a future version of POI.
|
||||
|
@ -22,11 +22,15 @@ import org.apache.poi.ss.formula.CollaboratingWorkbooksEnvironment.WorkbookNotFo
|
||||
import org.apache.poi.ss.formula.EvaluationWorkbook.ExternalName;
|
||||
import org.apache.poi.ss.formula.EvaluationWorkbook.ExternalSheet;
|
||||
import org.apache.poi.ss.formula.EvaluationWorkbook.ExternalSheetRange;
|
||||
import org.apache.poi.ss.formula.constant.ErrorConstant;
|
||||
import org.apache.poi.ss.formula.eval.AreaEval;
|
||||
import org.apache.poi.ss.formula.eval.BoolEval;
|
||||
import org.apache.poi.ss.formula.eval.ErrorEval;
|
||||
import org.apache.poi.ss.formula.eval.ExternalNameEval;
|
||||
import org.apache.poi.ss.formula.eval.FunctionNameEval;
|
||||
import org.apache.poi.ss.formula.eval.NumberEval;
|
||||
import org.apache.poi.ss.formula.eval.RefEval;
|
||||
import org.apache.poi.ss.formula.eval.StringEval;
|
||||
import org.apache.poi.ss.formula.eval.ValueEval;
|
||||
import org.apache.poi.ss.formula.functions.FreeRefFunction;
|
||||
import org.apache.poi.ss.formula.ptg.Area3DPtg;
|
||||
@ -339,6 +343,42 @@ public final class OperationEvaluationContext {
|
||||
aptg.getLastRow(), aptg.getLastColumn(), sre);
|
||||
}
|
||||
|
||||
public ValueEval getAreaValueEval(int firstRowIndex, int firstColumnIndex,
|
||||
int lastRowIndex, int lastColumnIndex, Object[][] tokens) {
|
||||
|
||||
ValueEval values[] = new ValueEval[tokens.length * tokens[0].length];
|
||||
|
||||
int index = 0;
|
||||
for (int jdx = 0; jdx < tokens.length; jdx++) {
|
||||
for (int idx = 0; idx < tokens[0].length; idx++) {
|
||||
values[index++] = convertObjectEval(tokens[jdx][idx]);
|
||||
}
|
||||
}
|
||||
|
||||
return new CacheAreaEval(firstRowIndex, firstColumnIndex, lastRowIndex,
|
||||
lastColumnIndex, values);
|
||||
}
|
||||
|
||||
private ValueEval convertObjectEval(Object token) {
|
||||
if (token == null) {
|
||||
throw new RuntimeException("Array item cannot be null");
|
||||
}
|
||||
if (token instanceof String) {
|
||||
return new StringEval((String)token);
|
||||
}
|
||||
if (token instanceof Double) {
|
||||
return new NumberEval(((Double)token).doubleValue());
|
||||
}
|
||||
if (token instanceof Boolean) {
|
||||
return BoolEval.valueOf(((Boolean)token).booleanValue());
|
||||
}
|
||||
if (token instanceof ErrorConstant) {
|
||||
return ErrorEval.valueOf(((ErrorConstant)token).getErrorCode());
|
||||
}
|
||||
throw new IllegalArgumentException("Unexpected constant class (" + token.getClass().getName() + ")");
|
||||
}
|
||||
|
||||
|
||||
public ValueEval getNameXEval(NameXPtg nameXPtg) {
|
||||
// Is the name actually on our workbook?
|
||||
ExternalSheet externSheet = _workbook.getExternalSheet(nameXPtg.getSheetRefIndex());
|
||||
|
@ -52,6 +52,7 @@ import org.apache.poi.ss.formula.eval.UnaryMinusEval;
|
||||
import org.apache.poi.ss.formula.eval.UnaryPlusEval;
|
||||
import org.apache.poi.ss.formula.eval.ValueEval;
|
||||
import org.apache.poi.ss.formula.function.FunctionMetadataRegistry;
|
||||
import org.apache.poi.ss.formula.functions.ArrayFunction;
|
||||
import org.apache.poi.ss.formula.functions.Function;
|
||||
import org.apache.poi.ss.formula.functions.Indirect;
|
||||
|
||||
@ -116,6 +117,12 @@ final class OperationEvaluatorFactory {
|
||||
Function result = _instancesByPtgClass.get(ptg);
|
||||
|
||||
if (result != null) {
|
||||
EvaluationSheet evalSheet = ec.getWorkbook().getSheet(ec.getSheetIndex());
|
||||
EvaluationCell evalCell = evalSheet.getCell(ec.getRowIndex(), ec.getColumnIndex());
|
||||
|
||||
if (evalCell.isPartOfArrayFormulaGroup() && result instanceof ArrayFunction)
|
||||
return ((ArrayFunction) result).evaluateArray(args, ec.getRowIndex(), ec.getColumnIndex());
|
||||
|
||||
return result.evaluate(args, ec.getRowIndex(), (short) ec.getColumnIndex());
|
||||
}
|
||||
|
||||
|
@ -530,11 +530,12 @@ public final class WorkbookEvaluator {
|
||||
throw new IllegalStateException("evaluation stack not empty");
|
||||
}
|
||||
|
||||
// "unwrap" result to just the value relevant for the source cell if needed
|
||||
ValueEval result;
|
||||
|
||||
if (ec.isSingleValue()) {
|
||||
result = dereferenceResult(value, ec.getRowIndex(), ec.getColumnIndex());
|
||||
} else {
|
||||
result = dereferenceResult(value, ec);
|
||||
}
|
||||
else {
|
||||
result = value;
|
||||
}
|
||||
|
||||
@ -574,6 +575,38 @@ public final class WorkbookEvaluator {
|
||||
return index-startIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dereferences a single value from any AreaEval or RefEval evaluation
|
||||
* result. If the supplied evaluationResult is just a plain value, it is
|
||||
* returned as-is.
|
||||
*
|
||||
* @return a {@link NumberEval}, {@link StringEval}, {@link BoolEval}, or
|
||||
* {@link ErrorEval}. Never <code>null</code>. {@link BlankEval} is
|
||||
* converted to {@link NumberEval#ZERO}
|
||||
*/
|
||||
private static ValueEval dereferenceResult(ValueEval evaluationResult, OperationEvaluationContext ec) {
|
||||
ValueEval value;
|
||||
|
||||
if (ec == null) {
|
||||
throw new IllegalArgumentException("OperationEvaluationContext ec is null");
|
||||
}
|
||||
if (ec.getWorkbook() == null) {
|
||||
throw new IllegalArgumentException("OperationEvaluationContext ec.getWorkbook() is null");
|
||||
}
|
||||
|
||||
EvaluationSheet evalSheet = ec.getWorkbook().getSheet(ec.getSheetIndex());
|
||||
EvaluationCell evalCell = evalSheet.getCell(ec.getRowIndex(), ec.getColumnIndex());
|
||||
|
||||
if (evalCell.isPartOfArrayFormulaGroup() && evaluationResult instanceof AreaEval) {
|
||||
value = OperandResolver.getElementFromArray((AreaEval) evaluationResult, evalCell);
|
||||
}
|
||||
else {
|
||||
value = dereferenceResult(evaluationResult, ec.getRowIndex(), ec.getColumnIndex());
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dereferences a single value from any AreaEval or RefEval evaluation
|
||||
* result. If the supplied evaluationResult is just a plain value, it is
|
||||
@ -667,6 +700,11 @@ public final class WorkbookEvaluator {
|
||||
return ec.getAreaEval(aptg.getFirstRow(), aptg.getFirstColumn(), aptg.getLastRow(), aptg.getLastColumn());
|
||||
}
|
||||
|
||||
if (ptg instanceof ArrayPtg) {
|
||||
ArrayPtg aptg = (ArrayPtg) ptg;
|
||||
return ec.getAreaValueEval(0, 0, aptg.getRowCount() - 1, aptg.getColumnCount() - 1, aptg.getTokenArrayValues());
|
||||
}
|
||||
|
||||
if (ptg instanceof UnknownPtg) {
|
||||
// POI uses UnknownPtg when the encoded Ptg array seems to be corrupted.
|
||||
// This seems to occur in very rare cases (e.g. unused name formulas in bug 44774, attachment 21790)
|
||||
|
@ -145,6 +145,7 @@ public final class FunctionEval {
|
||||
|
||||
retval[82] = TextFunction.SEARCH;
|
||||
// 83: TRANSPOSE
|
||||
retval[83] = MatrixFunction.TRANSPOSE;
|
||||
|
||||
// 86: TYPE
|
||||
|
||||
@ -183,6 +184,10 @@ public final class FunctionEval {
|
||||
|
||||
retval[162] = TextFunction.CLEAN;
|
||||
|
||||
retval[163] = MatrixFunction.MDETERM;
|
||||
retval[164] = MatrixFunction.MINVERSE;
|
||||
retval[165] = MatrixFunction.MMULT;
|
||||
|
||||
retval[167] = new IPMT();
|
||||
retval[168] = new PPMT();
|
||||
retval[169] = new Counta();
|
||||
|
@ -17,6 +17,9 @@
|
||||
|
||||
package org.apache.poi.ss.formula.eval;
|
||||
|
||||
import org.apache.poi.ss.formula.EvaluationCell;
|
||||
import org.apache.poi.ss.util.CellRangeAddress;
|
||||
import org.apache.poi.ss.formula.eval.ErrorEval;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
@ -71,6 +74,40 @@ public final class OperandResolver {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a single value from an area evaluation utilizing the 2D indices of the cell
|
||||
* within its own area reference to index the value in the area evaluation.
|
||||
*
|
||||
* @param ae area reference after evaluation
|
||||
* @param cell the source cell of the formula that contains its 2D indices
|
||||
* @return a <tt>NumberEval</tt>, <tt>StringEval</tt>, <tt>BoolEval</tt> or <tt>BlankEval</tt>. or <tt>ErrorEval<tt>
|
||||
* Never <code>null</code>.
|
||||
*/
|
||||
|
||||
public static ValueEval getElementFromArray(AreaEval ae, EvaluationCell cell) {
|
||||
CellRangeAddress range = cell.getArrayFormulaRange();
|
||||
int relativeRowIndex = cell.getRowIndex() - range.getFirstRow();
|
||||
int relativeColIndex = cell.getColumnIndex() - range.getFirstColumn();
|
||||
//System.out.println("Row: " + relativeRowIndex + " Col: " + relativeColIndex);
|
||||
|
||||
if (ae.isColumn()) {
|
||||
if (ae.isRow()) {
|
||||
return ae.getRelativeValue(0, 0);
|
||||
}
|
||||
else if(relativeRowIndex < ae.getHeight()) {
|
||||
return ae.getRelativeValue(relativeRowIndex, 0);
|
||||
}
|
||||
}
|
||||
else if (!ae.isRow() && relativeRowIndex < ae.getHeight() && relativeColIndex < ae.getWidth()) {
|
||||
return ae.getRelativeValue(relativeRowIndex, relativeColIndex);
|
||||
}
|
||||
else if (ae.isRow() && relativeColIndex < ae.getWidth()) {
|
||||
return ae.getRelativeValue(0, relativeColIndex);
|
||||
}
|
||||
|
||||
return ErrorEval.NA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements (some perhaps not well known) Excel functionality to select a single cell from an
|
||||
* area depending on the coordinates of the calling cell. Here is an example demonstrating
|
||||
|
@ -17,18 +17,29 @@
|
||||
|
||||
package org.apache.poi.ss.formula.eval;
|
||||
|
||||
import org.apache.poi.ss.formula.functions.ArrayFunction;
|
||||
import org.apache.poi.ss.formula.functions.Fixed2ArgFunction;
|
||||
import org.apache.poi.ss.formula.functions.Function;
|
||||
import org.apache.poi.ss.formula.functions.MatrixFunction.MutableValueCollector;
|
||||
import org.apache.poi.ss.formula.functions.MatrixFunction.TwoArrayArg;
|
||||
|
||||
/**
|
||||
* @author Josh Micich
|
||||
*/
|
||||
public abstract class TwoOperandNumericOperation extends Fixed2ArgFunction {
|
||||
public abstract class TwoOperandNumericOperation extends Fixed2ArgFunction implements ArrayFunction {
|
||||
|
||||
protected final double singleOperandEvaluate(ValueEval arg, int srcCellRow, int srcCellCol) throws EvaluationException {
|
||||
ValueEval ve = OperandResolver.getSingleValue(arg, srcCellRow, srcCellCol);
|
||||
return OperandResolver.coerceValueToDouble(ve);
|
||||
}
|
||||
|
||||
public ValueEval evaluateArray(ValueEval args[], int srcRowIndex, int srcColumnIndex) {
|
||||
if (args.length != 2) {
|
||||
return ErrorEval.VALUE_INVALID;
|
||||
}
|
||||
return new ArrayEval().evaluate(srcRowIndex, srcColumnIndex, args[0], args[1]);
|
||||
}
|
||||
|
||||
public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1) {
|
||||
double result;
|
||||
try {
|
||||
@ -52,6 +63,29 @@ public abstract class TwoOperandNumericOperation extends Fixed2ArgFunction {
|
||||
|
||||
protected abstract double evaluate(double d0, double d1) throws EvaluationException;
|
||||
|
||||
private final class ArrayEval extends TwoArrayArg {
|
||||
private final MutableValueCollector instance = new MutableValueCollector(false, true);
|
||||
|
||||
protected double[] collectValues(ValueEval arg) throws EvaluationException {
|
||||
return instance.collectValues(arg);
|
||||
}
|
||||
|
||||
protected double[][] evaluate(double[][] d1, double[][] d2) throws IllegalArgumentException, EvaluationException {
|
||||
int width = (d1[0].length < d2[0].length) ? d1[0].length : d2[0].length;
|
||||
int height = (d1.length < d2.length) ? d1.length : d2.length;
|
||||
|
||||
double result[][] = new double[height][width];
|
||||
|
||||
for (int j = 0; j < height; j++) {
|
||||
for (int i = 0; i < width; i++) {
|
||||
result[j][i] = TwoOperandNumericOperation.this.evaluate(d1[j][i], d2[j][i]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public static final Function AddEval = new TwoOperandNumericOperation() {
|
||||
protected double evaluate(double d0, double d1) {
|
||||
return d0+d1;
|
||||
|
@ -27,6 +27,8 @@ import org.apache.poi.ss.formula.eval.StringEval;
|
||||
import org.apache.poi.ss.formula.eval.ValueEval;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.CellType;
|
||||
import org.apache.poi.ss.util.CellRangeAddress;
|
||||
|
||||
|
||||
/**
|
||||
* Represents a cell being used for forked evaluation that has had a value set different from the
|
||||
@ -154,6 +156,16 @@ final class ForkedEvaluationCell implements EvaluationCell {
|
||||
public int getColumnIndex() {
|
||||
return _masterCell.getColumnIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellRangeAddress getArrayFormulaRange() {
|
||||
return _masterCell.getArrayFormulaRange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPartOfArrayFormulaGroup() {
|
||||
return _masterCell.isPartOfArrayFormulaGroup();
|
||||
}
|
||||
/**
|
||||
* Will return {@link CellType} in a future version of POI.
|
||||
* For forwards compatibility, do not hard-code cell type literals in your code.
|
||||
|
@ -0,0 +1,44 @@
|
||||
/* ====================================================================
|
||||
Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You 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 org.apache.poi.ss.formula.functions;
|
||||
|
||||
import org.apache.poi.ss.formula.eval.BlankEval;
|
||||
import org.apache.poi.ss.formula.eval.ErrorEval;
|
||||
import org.apache.poi.ss.formula.eval.MissingArgEval;
|
||||
import org.apache.poi.ss.formula.eval.ValueEval;
|
||||
|
||||
/**
|
||||
* @author Robert Hulbert
|
||||
* Common Interface for any excel built-in function that has implemented array formula functionality.
|
||||
*/
|
||||
|
||||
public interface ArrayFunction {
|
||||
|
||||
/**
|
||||
* @param args the evaluated function arguments. Empty values are represented with
|
||||
* {@link BlankEval} or {@link MissingArgEval}, never <code>null</code>.
|
||||
* @param srcRowIndex row index of the cell containing the formula under evaluation
|
||||
* @param srcColumnIndex column index of the cell containing the formula under evaluation
|
||||
* @return The evaluated result, possibly an {@link ErrorEval}, never <code>null</code>.
|
||||
* <b>Note</b> - Excel uses the error code <i>#NUM!</i> instead of IEEE <i>NaN</i>, so when
|
||||
* numeric functions evaluate to {@link Double#NaN} be sure to translate the result to {@link
|
||||
* ErrorEval#NUM_ERROR}.
|
||||
*/
|
||||
|
||||
ValueEval evaluateArray(ValueEval[] args, int srcRowIndex, int srcColumnIndex);
|
||||
}
|
339
src/java/org/apache/poi/ss/formula/functions/MatrixFunction.java
Normal file
339
src/java/org/apache/poi/ss/formula/functions/MatrixFunction.java
Normal file
@ -0,0 +1,339 @@
|
||||
/* ====================================================================
|
||||
Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You 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 org.apache.poi.ss.formula.functions;
|
||||
|
||||
import org.apache.poi.ss.formula.CacheAreaEval;
|
||||
import org.apache.poi.ss.formula.eval.AreaEval;
|
||||
import org.apache.poi.ss.formula.eval.ErrorEval;
|
||||
import org.apache.poi.ss.formula.eval.EvaluationException;
|
||||
import org.apache.poi.ss.formula.eval.NumberEval;
|
||||
import org.apache.poi.ss.formula.eval.OperandResolver;
|
||||
import org.apache.poi.ss.formula.eval.ValueEval;
|
||||
import org.apache.commons.math3.exception.DimensionMismatchException;
|
||||
import org.apache.commons.math3.linear.Array2DRowRealMatrix;
|
||||
import org.apache.commons.math3.linear.LUDecomposition;
|
||||
import org.apache.commons.math3.linear.MatrixUtils;
|
||||
|
||||
/**
|
||||
* @author Robert Hulbert
|
||||
*/
|
||||
public abstract class MatrixFunction implements Function{
|
||||
|
||||
public static final void checkValues(double[] results) throws EvaluationException {
|
||||
for (int idx = 0; idx < results.length; idx++) {
|
||||
if (Double.isNaN(results[idx]) || Double.isInfinite(results[idx])) {
|
||||
throw new EvaluationException(ErrorEval.NUM_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected final double singleOperandEvaluate(ValueEval arg, int srcCellRow, int srcCellCol) throws EvaluationException {
|
||||
ValueEval ve = OperandResolver.getSingleValue(arg, srcCellRow, srcCellCol);
|
||||
return OperandResolver.coerceValueToDouble(ve);
|
||||
}
|
||||
|
||||
/* converts 1D array to 2D array for calculations */
|
||||
private static double[][] fillDoubleArray(double[] vector, int rows, int cols) throws EvaluationException {
|
||||
int i = 0, j = 0;
|
||||
|
||||
if (rows < 1 || cols < 1 || vector.length < 1) {
|
||||
throw new EvaluationException(ErrorEval.VALUE_INVALID);
|
||||
}
|
||||
|
||||
double[][] matrix = new double[rows][cols];
|
||||
|
||||
for (int idx = 0; idx < vector.length; idx++) {
|
||||
if (j < matrix.length) {
|
||||
if (i == matrix[0].length) {
|
||||
i = 0;
|
||||
j++;
|
||||
}
|
||||
matrix[j][i++] = vector[idx];
|
||||
}
|
||||
}
|
||||
|
||||
return matrix;
|
||||
}
|
||||
|
||||
/* retrieves 1D array from 2D array after calculations */
|
||||
private static double[] extractDoubleArray(double[][] matrix) throws EvaluationException {
|
||||
int idx = 0;
|
||||
|
||||
if (matrix == null || matrix.length < 1 || matrix[0].length < 1) {
|
||||
throw new EvaluationException(ErrorEval.VALUE_INVALID);
|
||||
}
|
||||
|
||||
double[] vector = new double[matrix.length * matrix[0].length];
|
||||
|
||||
for (int j = 0; j < matrix.length; j++) {
|
||||
for (int i = 0; i < matrix[0].length; i++) {
|
||||
vector[idx++] = matrix[j][i];
|
||||
}
|
||||
}
|
||||
return vector;
|
||||
}
|
||||
|
||||
public static abstract class OneArrayArg extends Fixed1ArgFunction {
|
||||
protected OneArrayArg() {
|
||||
//no fields to initialize
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0) {
|
||||
if (arg0 instanceof AreaEval) {
|
||||
double result[] = null, resultArray[][];
|
||||
int width = 1, height = 1;
|
||||
|
||||
try {
|
||||
double values[] = collectValues(arg0);
|
||||
double array[][] = fillDoubleArray(values,((AreaEval) arg0).getHeight(),((AreaEval) arg0).getWidth());
|
||||
resultArray = evaluate(array);
|
||||
width = resultArray[0].length;
|
||||
height = resultArray.length;
|
||||
result = extractDoubleArray(resultArray);
|
||||
|
||||
checkValues(result);
|
||||
}
|
||||
catch(EvaluationException e){
|
||||
return e.getErrorEval();
|
||||
}
|
||||
|
||||
ValueEval vals[] = new ValueEval[result.length];
|
||||
|
||||
for (int idx = 0; idx < result.length; idx++) {
|
||||
vals[idx] = new NumberEval(result[idx]);
|
||||
}
|
||||
|
||||
if (result.length == 1) {
|
||||
return vals[0];
|
||||
}
|
||||
else {
|
||||
/* find a better solution */
|
||||
return new CacheAreaEval(((AreaEval) arg0).getFirstRow(), ((AreaEval) arg0).getFirstColumn(),
|
||||
((AreaEval) arg0).getFirstRow() + height - 1,
|
||||
((AreaEval) arg0).getFirstColumn() + width - 1, vals);
|
||||
}
|
||||
}
|
||||
else {
|
||||
double result[][] = null;
|
||||
try {
|
||||
double value = NumericFunction.singleOperandEvaluate(arg0, srcRowIndex, srcColumnIndex);
|
||||
double temp[][] = {{value}};
|
||||
result = evaluate(temp);
|
||||
NumericFunction.checkValue(result[0][0]);
|
||||
}
|
||||
catch (EvaluationException e) {
|
||||
return e.getErrorEval();
|
||||
}
|
||||
|
||||
return new NumberEval(result[0][0]);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract double[][] evaluate(double[][] d1) throws EvaluationException;
|
||||
protected abstract double[] collectValues(ValueEval arg) throws EvaluationException;
|
||||
}
|
||||
|
||||
public static abstract class TwoArrayArg extends Fixed2ArgFunction {
|
||||
protected TwoArrayArg() {
|
||||
//no fields to initialize
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1) {
|
||||
double result[];
|
||||
int width = 1, height = 1;
|
||||
|
||||
try {
|
||||
double array0[][], array1[][], resultArray[][];
|
||||
|
||||
if (arg0 instanceof AreaEval) {
|
||||
try {
|
||||
double values[] = collectValues(arg0);
|
||||
array0 = fillDoubleArray(values, ((AreaEval) arg0).getHeight(), ((AreaEval) arg0).getWidth());
|
||||
}
|
||||
catch(EvaluationException e) {
|
||||
return e.getErrorEval();
|
||||
}
|
||||
}
|
||||
else {
|
||||
try {
|
||||
double value = NumericFunction.singleOperandEvaluate(arg0, srcRowIndex, srcColumnIndex);
|
||||
array0 = new double[][] {{value}};
|
||||
}
|
||||
catch (EvaluationException e) {
|
||||
return e.getErrorEval();
|
||||
}
|
||||
}
|
||||
|
||||
if (arg1 instanceof AreaEval) {
|
||||
try {
|
||||
double values[] = collectValues(arg1);
|
||||
array1 = fillDoubleArray(values, ((AreaEval) arg1).getHeight(),((AreaEval) arg1).getWidth());
|
||||
}
|
||||
catch (EvaluationException e) {
|
||||
return e.getErrorEval();
|
||||
}
|
||||
}
|
||||
else {
|
||||
try {
|
||||
double value = NumericFunction.singleOperandEvaluate(arg1, srcRowIndex, srcColumnIndex);
|
||||
array1 = new double[][] {{value}};
|
||||
}
|
||||
catch (EvaluationException e) {
|
||||
return e.getErrorEval();
|
||||
}
|
||||
}
|
||||
|
||||
resultArray = evaluate(array0, array1);
|
||||
width = resultArray[0].length;
|
||||
height = resultArray.length;
|
||||
result = extractDoubleArray(resultArray);
|
||||
checkValues(result);
|
||||
}
|
||||
catch (EvaluationException e) {
|
||||
return e.getErrorEval();
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
return ErrorEval.VALUE_INVALID;
|
||||
}
|
||||
|
||||
|
||||
ValueEval vals[] = new ValueEval[result.length];
|
||||
|
||||
for (int idx = 0; idx < result.length; idx++) {
|
||||
vals[idx] = new NumberEval(result[idx]);
|
||||
}
|
||||
|
||||
if (result.length == 1)
|
||||
return vals[0];
|
||||
else {
|
||||
return new CacheAreaEval(((AreaEval) arg0).getFirstRow(), ((AreaEval) arg0).getFirstColumn(),
|
||||
((AreaEval) arg0).getFirstRow() + height - 1,
|
||||
((AreaEval) arg0).getFirstColumn() + width - 1, vals);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected abstract double[][] evaluate(double[][] d1, double[][] d2) throws EvaluationException;
|
||||
protected abstract double[] collectValues(ValueEval arg) throws EvaluationException;
|
||||
|
||||
}
|
||||
|
||||
public static final class MutableValueCollector extends MultiOperandNumericFunction {
|
||||
public MutableValueCollector(boolean isReferenceBoolCounted, boolean isBlankCounted) {
|
||||
super(isReferenceBoolCounted, isBlankCounted);
|
||||
}
|
||||
public double[] collectValues(ValueEval...operands) throws EvaluationException {
|
||||
return getNumberArray(operands);
|
||||
}
|
||||
protected double evaluate(double[] values) {
|
||||
throw new IllegalStateException("should not be called");
|
||||
}
|
||||
}
|
||||
|
||||
public static final Function MINVERSE = new OneArrayArg() {
|
||||
private final MutableValueCollector instance = new MutableValueCollector(false, false);
|
||||
|
||||
protected double[] collectValues(ValueEval arg) throws EvaluationException {
|
||||
double[] values = instance.collectValues(arg);
|
||||
|
||||
/* handle case where MDETERM is operating on an array that that is not completely filled*/
|
||||
if (arg instanceof AreaEval && values.length == 1)
|
||||
throw new EvaluationException(ErrorEval.VALUE_INVALID);
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
protected double[][] evaluate(double[][] d1) throws EvaluationException {
|
||||
if (d1.length != d1[0].length) {
|
||||
throw new EvaluationException(ErrorEval.VALUE_INVALID);
|
||||
}
|
||||
|
||||
Array2DRowRealMatrix temp = new Array2DRowRealMatrix(d1);
|
||||
return MatrixUtils.inverse(temp).getData();
|
||||
}
|
||||
};
|
||||
|
||||
public static final Function TRANSPOSE = new OneArrayArg() {
|
||||
private final MutableValueCollector instance = new MutableValueCollector(false, true);
|
||||
|
||||
protected double[] collectValues(ValueEval arg) throws EvaluationException {
|
||||
return instance.collectValues(arg);
|
||||
}
|
||||
|
||||
protected double[][] evaluate(double[][] d1) throws EvaluationException {
|
||||
|
||||
Array2DRowRealMatrix temp = new Array2DRowRealMatrix(d1);
|
||||
return temp.transpose().getData();
|
||||
}
|
||||
};
|
||||
|
||||
public static final Function MDETERM = new OneArrayArg() {
|
||||
private final MutableValueCollector instance = new MutableValueCollector(false, false);
|
||||
|
||||
protected double[] collectValues(ValueEval arg) throws EvaluationException {
|
||||
double[] values = instance.collectValues(arg);
|
||||
|
||||
/* handle case where MDETERM is operating on an array that that is not completely filled*/
|
||||
if (arg instanceof AreaEval && values.length == 1)
|
||||
throw new EvaluationException(ErrorEval.VALUE_INVALID);
|
||||
|
||||
return instance.collectValues(arg);
|
||||
}
|
||||
|
||||
protected double[][] evaluate(double[][] d1) throws EvaluationException {
|
||||
if (d1.length != d1[0].length) {
|
||||
throw new EvaluationException(ErrorEval.VALUE_INVALID);
|
||||
}
|
||||
|
||||
double result[][] = new double[1][1];
|
||||
Array2DRowRealMatrix temp = new Array2DRowRealMatrix(d1);
|
||||
result[0][0] = (new LUDecomposition(temp)).getDeterminant();
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
public static final Function MMULT = new TwoArrayArg() {
|
||||
private final MutableValueCollector instance = new MutableValueCollector(false, false);
|
||||
|
||||
protected double[] collectValues(ValueEval arg) throws EvaluationException {
|
||||
double values[] = instance.collectValues(arg);
|
||||
|
||||
/* handle case where MMULT is operating on an array that is not completely filled*/
|
||||
if (arg instanceof AreaEval && values.length == 1)
|
||||
throw new EvaluationException(ErrorEval.VALUE_INVALID);
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
protected double[][] evaluate(double[][] d1, double[][] d2) throws EvaluationException{
|
||||
Array2DRowRealMatrix first = new Array2DRowRealMatrix(d1);
|
||||
Array2DRowRealMatrix second = new Array2DRowRealMatrix(d2);
|
||||
|
||||
try {
|
||||
MatrixUtils.checkMultiplicationCompatible(first, second);
|
||||
}
|
||||
catch (DimensionMismatchException e) {
|
||||
throw new EvaluationException(ErrorEval.VALUE_INVALID);
|
||||
}
|
||||
|
||||
return first.multiply(second).getData();
|
||||
}
|
||||
};
|
||||
}
|
@ -20,6 +20,7 @@ package org.apache.poi.xssf.streaming;
|
||||
import org.apache.poi.ss.formula.EvaluationCell;
|
||||
import org.apache.poi.ss.formula.EvaluationSheet;
|
||||
import org.apache.poi.ss.usermodel.CellType;
|
||||
import org.apache.poi.ss.util.CellRangeAddress;
|
||||
import org.apache.poi.util.Internal;
|
||||
|
||||
/**
|
||||
@ -97,6 +98,17 @@ final class SXSSFEvaluationCell implements EvaluationCell {
|
||||
public String getStringCellValue() {
|
||||
return _cell.getRichStringCellValue().getString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellRangeAddress getArrayFormulaRange() {
|
||||
return _cell.getArrayFormulaRange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPartOfArrayFormulaGroup() {
|
||||
return _cell.isPartOfArrayFormulaGroup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Will return {@link CellType} in a future version of POI.
|
||||
* For forwards compatibility, do not hard-code cell type literals in your code.
|
||||
|
@ -483,9 +483,12 @@ public final class XSSFCell implements Cell {
|
||||
}
|
||||
|
||||
CTCellFormula f = _cell.getF();
|
||||
if (isPartOfArrayFormulaGroup() && f == null) {
|
||||
XSSFCell cell = getSheet().getFirstCellInArrayFormula(this);
|
||||
return cell.getCellFormula(fpb);
|
||||
if (isPartOfArrayFormulaGroup()) {
|
||||
/* In an excel generated array formula, the formula property might be set, but the string is empty in slave cells */
|
||||
if (f == null || f.getStringValue().isEmpty()) {
|
||||
XSSFCell cell = getSheet().getFirstCellInArrayFormula(this);
|
||||
return cell.getCellFormula(fpb);
|
||||
}
|
||||
}
|
||||
if (f.getT() == STCellFormulaType.SHARED) {
|
||||
return convertSharedFormula((int)f.getSi(), fpb == null ? XSSFEvaluationWorkbook.create(getSheet().getWorkbook()) : fpb);
|
||||
|
@ -20,6 +20,7 @@ package org.apache.poi.xssf.usermodel;
|
||||
import org.apache.poi.ss.formula.EvaluationCell;
|
||||
import org.apache.poi.ss.formula.EvaluationSheet;
|
||||
import org.apache.poi.ss.usermodel.CellType;
|
||||
import org.apache.poi.ss.util.CellRangeAddress;
|
||||
import org.apache.poi.util.Internal;
|
||||
|
||||
/**
|
||||
@ -97,6 +98,17 @@ final class XSSFEvaluationCell implements EvaluationCell {
|
||||
public String getStringCellValue() {
|
||||
return _cell.getRichStringCellValue().getString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellRangeAddress getArrayFormulaRange() {
|
||||
return _cell.getArrayFormulaRange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPartOfArrayFormulaGroup() {
|
||||
return _cell.isPartOfArrayFormulaGroup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Will return {@link CellType} in a future version of POI.
|
||||
* For forwards compatibility, do not hard-code cell type literals in your code.
|
||||
|
@ -0,0 +1,226 @@
|
||||
package org.apache.poi.xssf.usermodel;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
|
||||
import org.apache.poi.ss.formula.eval.ErrorEval;
|
||||
import org.apache.poi.ss.formula.functions.TestMathX;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.CellType;
|
||||
import org.apache.poi.ss.usermodel.CellValue;
|
||||
import org.apache.poi.ss.usermodel.FormulaEvaluator;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.util.LocaleUtil;
|
||||
import org.apache.poi.xssf.XSSFTestDataSamples;
|
||||
import org.apache.poi.xssf.usermodel.XSSFFormulaEvaluator;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameter;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
|
||||
import junit.framework.AssertionFailedError;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public final class TestMatrixFormulasFromXMLSpreadsheet {
|
||||
|
||||
private static XSSFWorkbook workbook;
|
||||
private static Sheet sheet;
|
||||
private static FormulaEvaluator evaluator;
|
||||
private static Locale userLocale;
|
||||
|
||||
/*
|
||||
* Unlike TestFormulaFromSpreadsheet which this class is modified from, there is no
|
||||
* differentiation between operators and functions, if more functionality is implemented with
|
||||
* array formulas then it might be worth it to separate operators from functions
|
||||
*
|
||||
* Also, output matrices are statically 3x3, if larger matrices wanted to be tested
|
||||
* then adding matrix size parameter would be useful and parsing would be based off that.
|
||||
*/
|
||||
|
||||
private static interface Navigator {
|
||||
/**
|
||||
* Name of the test spreadsheet (found in the standard test data folder)
|
||||
*/
|
||||
String FILENAME = "MatrixFormulaEvalTestData.xlsx";
|
||||
/**
|
||||
* Row (zero-based) in the spreadsheet where operations start
|
||||
*/
|
||||
int START_OPERATORS_ROW_INDEX = 1;
|
||||
/**
|
||||
* Column (zero-based) in the spreadsheet where operations start
|
||||
*/
|
||||
int START_OPERATORS_COL_INDEX = 0;
|
||||
/**
|
||||
* Column (zero-based) in the spreadsheet where evaluations start
|
||||
*/
|
||||
int START_RESULT_COL_INDEX = 7;
|
||||
/**
|
||||
* Column separation in the spreadsheet between evaluations and expected results
|
||||
*/
|
||||
int COL_OFF_EXPECTED_RESULT = 3;
|
||||
/**
|
||||
* Row separation in the spreadsheet between operations
|
||||
*/
|
||||
int ROW_OFF_NEXT_OP = 4;
|
||||
/**
|
||||
* Used to indicate when there are no more operations left
|
||||
*/
|
||||
String END_OF_TESTS = "<END>";
|
||||
|
||||
}
|
||||
|
||||
/* Parameters for test case */
|
||||
@Parameter(0)
|
||||
public String targetFunctionName;
|
||||
@Parameter(1)
|
||||
public int formulasRowIdx;
|
||||
|
||||
@AfterClass
|
||||
public static void closeResource() throws Exception {
|
||||
LocaleUtil.setUserLocale(userLocale);
|
||||
workbook.close();
|
||||
}
|
||||
|
||||
/* generating parameter instances */
|
||||
@Parameters(name="{0}")
|
||||
public static Collection<Object[]> data() throws Exception {
|
||||
// Function "Text" uses custom-formats which are locale specific
|
||||
// can't set the locale on a per-testrun execution, as some settings have been
|
||||
// already set, when we would try to change the locale by then
|
||||
userLocale = LocaleUtil.getUserLocale();
|
||||
LocaleUtil.setUserLocale(Locale.ROOT);
|
||||
|
||||
workbook = XSSFTestDataSamples.openSampleWorkbook(Navigator.FILENAME);
|
||||
sheet = workbook.getSheetAt(0);
|
||||
evaluator = new XSSFFormulaEvaluator(workbook);
|
||||
|
||||
List<Object[]> data = new ArrayList<Object[]>();
|
||||
|
||||
processFunctionGroup(data, Navigator.START_OPERATORS_ROW_INDEX, null);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param startRowIndex row index in the spreadsheet where the first function/operator is found
|
||||
* @param testFocusFunctionName name of a single function/operator to test alone.
|
||||
* Typically pass <code>null</code> to test all functions
|
||||
*/
|
||||
private static void processFunctionGroup(List<Object[]> data, int startRowIndex, String testFocusFunctionName) {
|
||||
for (int rowIndex = startRowIndex; true; rowIndex += Navigator.ROW_OFF_NEXT_OP) {
|
||||
Row r = sheet.getRow(rowIndex);
|
||||
String targetFunctionName = getTargetFunctionName(r);
|
||||
assertNotNull("Test spreadsheet cell empty on row ("
|
||||
+ (rowIndex) + "). Expected function name or '"
|
||||
+ Navigator.END_OF_TESTS + "'", targetFunctionName);
|
||||
if(targetFunctionName.equals(Navigator.END_OF_TESTS)) {
|
||||
// found end of functions list
|
||||
break;
|
||||
}
|
||||
if(testFocusFunctionName == null || targetFunctionName.equalsIgnoreCase(testFocusFunctionName)) {
|
||||
data.add(new Object[]{targetFunctionName, rowIndex});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void processFunctionRow() {
|
||||
|
||||
int endColNum = Navigator.START_RESULT_COL_INDEX + Navigator.COL_OFF_EXPECTED_RESULT;
|
||||
|
||||
for (int rowNum = formulasRowIdx; rowNum < formulasRowIdx + Navigator.ROW_OFF_NEXT_OP - 1; rowNum++) {
|
||||
for (int colNum = Navigator.START_RESULT_COL_INDEX; colNum < endColNum; colNum++) {
|
||||
Row r = sheet.getRow(rowNum);
|
||||
|
||||
/* mainly to escape row failures on MDETERM which only returns a scalar */
|
||||
if (r == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Cell c = sheet.getRow(rowNum).getCell(colNum);
|
||||
|
||||
if (c == null || c.getCellTypeEnum() != CellType.FORMULA) {
|
||||
continue;
|
||||
}
|
||||
|
||||
CellValue actValue = evaluator.evaluate(c);
|
||||
Cell expValue = sheet.getRow(rowNum).getCell(colNum + Navigator.COL_OFF_EXPECTED_RESULT);
|
||||
|
||||
String msg = String.format(Locale.ROOT, "Function '%s': Formula: %s @ %d:%d"
|
||||
, targetFunctionName, c.getCellFormula(), rowNum, colNum);
|
||||
|
||||
assertNotNull(msg + " - Bad setup data expected value is null", expValue);
|
||||
assertNotNull(msg + " - actual value was null", actValue);
|
||||
|
||||
final CellType cellType = expValue.getCellTypeEnum();
|
||||
switch (cellType) {
|
||||
case BLANK:
|
||||
assertEquals(msg, CellType.BLANK, actValue.getCellTypeEnum());
|
||||
break;
|
||||
case BOOLEAN:
|
||||
assertEquals(msg, CellType.BOOLEAN, actValue.getCellTypeEnum());
|
||||
assertEquals(msg, expValue.getBooleanCellValue(), actValue.getBooleanValue());
|
||||
break;
|
||||
case ERROR:
|
||||
assertEquals(msg, CellType.ERROR, actValue.getCellTypeEnum());
|
||||
assertEquals(msg, ErrorEval.getText(expValue.getErrorCellValue()), ErrorEval.getText(actValue.getErrorValue()));
|
||||
break;
|
||||
case FORMULA: // will never be used, since we will call method after formula evaluation
|
||||
fail("Cannot expect formula as result of formula evaluation: " + msg);
|
||||
case NUMERIC:
|
||||
assertEquals(msg, CellType.NUMERIC, actValue.getCellTypeEnum());
|
||||
TestMathX.assertEquals(msg, expValue.getNumericCellValue(), actValue.getNumberValue(), TestMathX.POS_ZERO, TestMathX.DIFF_TOLERANCE_FACTOR);
|
||||
break;
|
||||
case STRING:
|
||||
assertEquals(msg, CellType.STRING, actValue.getCellTypeEnum());
|
||||
assertEquals(msg, expValue.getRichStringCellValue().getString(), actValue.getStringValue());
|
||||
break;
|
||||
default:
|
||||
fail("Unexpected cell type: " + cellType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return <code>null</code> if cell is missing, empty or blank
|
||||
*/
|
||||
private static String getTargetFunctionName(Row r) {
|
||||
if(r == null) {
|
||||
System.err.println("Warning - given null row, can't figure out function name");
|
||||
return null;
|
||||
}
|
||||
Cell cell = r.getCell(Navigator.START_OPERATORS_COL_INDEX);
|
||||
System.err.println(String.valueOf(Navigator.START_OPERATORS_COL_INDEX));
|
||||
if(cell == null) {
|
||||
System.err.println("Warning - Row " + r.getRowNum() + " has no cell " + Navigator.START_OPERATORS_COL_INDEX + ", can't figure out function name");
|
||||
return null;
|
||||
}
|
||||
if(cell.getCellTypeEnum() == CellType.BLANK) {
|
||||
return null;
|
||||
}
|
||||
if(cell.getCellTypeEnum() == CellType.STRING) {
|
||||
return cell.getRichStringCellValue().getString();
|
||||
}
|
||||
|
||||
throw new AssertionFailedError("Bad cell type for 'function name' column: ("
|
||||
+ cell.getCellTypeEnum() + ") row (" + (r.getRowNum() +1) + ")");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,223 @@
|
||||
package org.apache.poi.hssf.usermodel;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.apache.poi.hssf.HSSFTestDataSamples;
|
||||
import org.apache.poi.ss.formula.eval.ErrorEval;
|
||||
import org.apache.poi.ss.formula.functions.TestMathX;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.CellType;
|
||||
import org.apache.poi.ss.usermodel.CellValue;
|
||||
import org.apache.poi.ss.usermodel.FormulaEvaluator;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.util.LocaleUtil;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameter;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
|
||||
import junit.framework.AssertionFailedError;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public final class TestMatrixFormulasFromBinarySpreadsheet {
|
||||
|
||||
private static HSSFWorkbook workbook;
|
||||
private static Sheet sheet;
|
||||
private static FormulaEvaluator evaluator;
|
||||
private static Locale userLocale;
|
||||
|
||||
/*
|
||||
* Unlike TestFormulaFromSpreadsheet which this class is modified from, there is no
|
||||
* differentiation between operators and functions, if more functionality is implemented with
|
||||
* array formulas then it might be worth it to separate operators from functions
|
||||
*
|
||||
* Also, output matrices are statically 3x3, if larger matrices wanted to be tested
|
||||
* then adding matrix size parameter would be useful and parsing would be based off that.
|
||||
*/
|
||||
|
||||
private static interface Navigator {
|
||||
/**
|
||||
* Name of the test spreadsheet (found in the standard test data folder)
|
||||
*/
|
||||
String FILENAME = "MatrixFormulaEvalTestData.xls";
|
||||
/**
|
||||
* Row (zero-based) in the spreadsheet where operations start
|
||||
*/
|
||||
int START_OPERATORS_ROW_INDEX = 1;
|
||||
/**
|
||||
* Column (zero-based) in the spreadsheet where operations start
|
||||
*/
|
||||
int START_OPERATORS_COL_INDEX = 0;
|
||||
/**
|
||||
* Column (zero-based) in the spreadsheet where evaluations start
|
||||
*/
|
||||
int START_RESULT_COL_INDEX = 7;
|
||||
/**
|
||||
* Column separation in the spreadsheet between evaluations and expected results
|
||||
*/
|
||||
int COL_OFF_EXPECTED_RESULT = 3;
|
||||
/**
|
||||
* Row separation in the spreadsheet between operations
|
||||
*/
|
||||
int ROW_OFF_NEXT_OP = 4;
|
||||
/**
|
||||
* Used to indicate when there are no more operations left
|
||||
*/
|
||||
String END_OF_TESTS = "<END>";
|
||||
|
||||
}
|
||||
|
||||
/* Parameters for test case */
|
||||
@Parameter(0)
|
||||
public String targetFunctionName;
|
||||
@Parameter(1)
|
||||
public int formulasRowIdx;
|
||||
|
||||
@AfterClass
|
||||
public static void closeResource() throws Exception {
|
||||
LocaleUtil.setUserLocale(userLocale);
|
||||
workbook.close();
|
||||
}
|
||||
|
||||
/* generating parameter instances */
|
||||
@Parameters(name="{0}")
|
||||
public static Collection<Object[]> data() throws Exception {
|
||||
// Function "Text" uses custom-formats which are locale specific
|
||||
// can't set the locale on a per-testrun execution, as some settings have been
|
||||
// already set, when we would try to change the locale by then
|
||||
userLocale = LocaleUtil.getUserLocale();
|
||||
LocaleUtil.setUserLocale(Locale.ROOT);
|
||||
|
||||
workbook = HSSFTestDataSamples.openSampleWorkbook(Navigator.FILENAME);
|
||||
sheet = workbook.getSheetAt(0);
|
||||
evaluator = new HSSFFormulaEvaluator(workbook);
|
||||
|
||||
List<Object[]> data = new ArrayList<Object[]>();
|
||||
|
||||
processFunctionGroup(data, Navigator.START_OPERATORS_ROW_INDEX, null);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param startRowIndex row index in the spreadsheet where the first function/operator is found
|
||||
* @param testFocusFunctionName name of a single function/operator to test alone.
|
||||
* Typically pass <code>null</code> to test all functions
|
||||
*/
|
||||
private static void processFunctionGroup(List<Object[]> data, int startRowIndex, String testFocusFunctionName) {
|
||||
for (int rowIndex = startRowIndex; true; rowIndex += Navigator.ROW_OFF_NEXT_OP) {
|
||||
Row r = sheet.getRow(rowIndex);
|
||||
String targetFunctionName = getTargetFunctionName(r);
|
||||
assertNotNull("Test spreadsheet cell empty on row ("
|
||||
+ (rowIndex) + "). Expected function name or '"
|
||||
+ Navigator.END_OF_TESTS + "'", targetFunctionName);
|
||||
if(targetFunctionName.equals(Navigator.END_OF_TESTS)) {
|
||||
// found end of functions list
|
||||
break;
|
||||
}
|
||||
if(testFocusFunctionName == null || targetFunctionName.equalsIgnoreCase(testFocusFunctionName)) {
|
||||
data.add(new Object[]{targetFunctionName, rowIndex});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void processFunctionRow() {
|
||||
|
||||
int endColNum = Navigator.START_RESULT_COL_INDEX + Navigator.COL_OFF_EXPECTED_RESULT;
|
||||
|
||||
for (int rowNum = formulasRowIdx; rowNum < formulasRowIdx + Navigator.ROW_OFF_NEXT_OP - 1; rowNum++) {
|
||||
for (int colNum = Navigator.START_RESULT_COL_INDEX; colNum < endColNum; colNum++) {
|
||||
Row r = sheet.getRow(rowNum);
|
||||
|
||||
/* mainly to escape row failures on MDETERM which only returns a scalar */
|
||||
if (r == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Cell c = sheet.getRow(rowNum).getCell(colNum);
|
||||
|
||||
if (c == null || c.getCellTypeEnum() != CellType.FORMULA) {
|
||||
continue;
|
||||
}
|
||||
|
||||
CellValue actValue = evaluator.evaluate(c);
|
||||
Cell expValue = sheet.getRow(rowNum).getCell(colNum + Navigator.COL_OFF_EXPECTED_RESULT);
|
||||
|
||||
String msg = String.format(Locale.ROOT, "Function '%s': Formula: %s @ %d:%d"
|
||||
, targetFunctionName, c.getCellFormula(), rowNum, colNum);
|
||||
|
||||
assertNotNull(msg + " - Bad setup data expected value is null", expValue);
|
||||
assertNotNull(msg + " - actual value was null", actValue);
|
||||
|
||||
final CellType cellType = expValue.getCellTypeEnum();
|
||||
switch (cellType) {
|
||||
case BLANK:
|
||||
assertEquals(msg, CellType.BLANK, actValue.getCellTypeEnum());
|
||||
break;
|
||||
case BOOLEAN:
|
||||
assertEquals(msg, CellType.BOOLEAN, actValue.getCellTypeEnum());
|
||||
assertEquals(msg, expValue.getBooleanCellValue(), actValue.getBooleanValue());
|
||||
break;
|
||||
case ERROR:
|
||||
assertEquals(msg, CellType.ERROR, actValue.getCellTypeEnum());
|
||||
assertEquals(msg, ErrorEval.getText(expValue.getErrorCellValue()), ErrorEval.getText(actValue.getErrorValue()));
|
||||
break;
|
||||
case FORMULA: // will never be used, since we will call method after formula evaluation
|
||||
fail("Cannot expect formula as result of formula evaluation: " + msg);
|
||||
case NUMERIC:
|
||||
assertEquals(msg, CellType.NUMERIC, actValue.getCellTypeEnum());
|
||||
TestMathX.assertEquals(msg, expValue.getNumericCellValue(), actValue.getNumberValue(), TestMathX.POS_ZERO, TestMathX.DIFF_TOLERANCE_FACTOR);
|
||||
break;
|
||||
case STRING:
|
||||
assertEquals(msg, CellType.STRING, actValue.getCellTypeEnum());
|
||||
assertEquals(msg, expValue.getRichStringCellValue().getString(), actValue.getStringValue());
|
||||
break;
|
||||
default:
|
||||
fail("Unexpected cell type: " + cellType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return <code>null</code> if cell is missing, empty or blank
|
||||
*/
|
||||
private static String getTargetFunctionName(Row r) {
|
||||
if(r == null) {
|
||||
System.err.println("Warning - given null row, can't figure out function name");
|
||||
return null;
|
||||
}
|
||||
Cell cell = r.getCell(Navigator.START_OPERATORS_COL_INDEX);
|
||||
System.err.println(String.valueOf(Navigator.START_OPERATORS_COL_INDEX));
|
||||
if(cell == null) {
|
||||
System.err.println("Warning - Row " + r.getRowNum() + " has no cell " + Navigator.START_OPERATORS_COL_INDEX + ", can't figure out function name");
|
||||
return null;
|
||||
}
|
||||
if(cell.getCellTypeEnum() == CellType.BLANK) {
|
||||
return null;
|
||||
}
|
||||
if(cell.getCellTypeEnum() == CellType.STRING) {
|
||||
return cell.getRichStringCellValue().getString();
|
||||
}
|
||||
|
||||
throw new AssertionFailedError("Bad cell type for 'function name' column: ("
|
||||
+ cell.getCellTypeEnum() + ") row (" + (r.getRowNum() +1) + ")");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@ -27,6 +27,7 @@ import java.io.IOException;
|
||||
|
||||
import org.apache.poi.hssf.HSSFTestDataSamples;
|
||||
import org.apache.poi.hssf.usermodel.HSSFCell;
|
||||
import org.apache.poi.hssf.usermodel.HSSFEvaluationWorkbook;
|
||||
import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator;
|
||||
import org.apache.poi.hssf.usermodel.HSSFRow;
|
||||
import org.apache.poi.hssf.usermodel.HSSFSheet;
|
||||
@ -58,7 +59,10 @@ public class TestWorkbookEvaluator {
|
||||
private static final double EPSILON = 0.0000001;
|
||||
|
||||
private static ValueEval evaluateFormula(Ptg[] ptgs) {
|
||||
OperationEvaluationContext ec = new OperationEvaluationContext(null, null, 0, 0, 0, null);
|
||||
HSSFWorkbook wb = new HSSFWorkbook();
|
||||
wb.createSheet().createRow(0).createCell(0);
|
||||
EvaluationWorkbook ewb = HSSFEvaluationWorkbook.create(wb);
|
||||
OperationEvaluationContext ec = new OperationEvaluationContext(null, ewb, 0, 0, 0, null);
|
||||
return new WorkbookEvaluator(null, null, null).evaluateFormula(ec, ptgs);
|
||||
}
|
||||
|
||||
|
BIN
test-data/spreadsheet/MatrixFormulaEvalTestData.xls
Normal file
BIN
test-data/spreadsheet/MatrixFormulaEvalTestData.xls
Normal file
Binary file not shown.
BIN
test-data/spreadsheet/MatrixFormulaEvalTestData.xlsx
Normal file
BIN
test-data/spreadsheet/MatrixFormulaEvalTestData.xlsx
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user