From ecbc6e2fde1941cb5ac9e5f3bfce329e7bfa8825 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 4 Apr 2022 00:21:33 +0800 Subject: [PATCH] ref #65, new formula functions: T.INV and T.INV.2T - Typo fixed --- calc.go | 111 +++++++++++++++++++++++++++++++++++++-------------- calc_test.go | 19 +++++++++ file.go | 4 +- lib.go | 47 ++++++++++++++++------ numfmt.go | 10 ++--- 5 files changed, 140 insertions(+), 51 deletions(-) diff --git a/calc.go b/calc.go index ad62596..c375b71 100644 --- a/calc.go +++ b/calc.go @@ -664,6 +664,8 @@ type formulaFuncs struct { // TEXTJOIN // TIME // TIMEVALUE +// T.INV +// T.INV.2T // TODAY // TRANSPOSE // TRIM @@ -1265,27 +1267,6 @@ func isOperand(token efp.Token) bool { return token.TType == efp.TokenTypeOperand && (token.TSubType == efp.TokenSubTypeNumber || token.TSubType == efp.TokenSubTypeText) } -// getDefinedNameRefTo convert defined name to reference range. -func (f *File) getDefinedNameRefTo(definedNameName string, currentSheet string) (refTo string) { - var workbookRefTo, worksheetRefTo string - for _, definedName := range f.GetDefinedName() { - if definedName.Name == definedNameName { - // worksheet scope takes precedence over scope workbook when both definedNames exist - if definedName.Scope == "Workbook" { - workbookRefTo = definedName.RefersTo - } - if definedName.Scope == currentSheet { - worksheetRefTo = definedName.RefersTo - } - } - } - refTo = workbookRefTo - if worksheetRefTo != "" { - refTo = worksheetRefTo - } - return -} - // parseToken parse basic arithmetic operator priority and evaluate based on // operators and operands. func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Stack) error { @@ -6647,14 +6628,16 @@ func hasChangeOfSign(u, w float64) bool { // calcInverseIterator directly maps the required parameters for inverse // distribution functions. type calcInverseIterator struct { - name string - fp, fDF float64 + name string + fp, fDF, nT float64 } -// chiSqDist implements inverse distribution with left tail for the Chi-Square -// distribution. -func (iterator *calcInverseIterator) chiSqDist(x float64) float64 { - return iterator.fp - getChiSqDistCDF(x, iterator.fDF) +// callBack implements the callback function for the inverse iterator. +func (iterator *calcInverseIterator) callBack(x float64) float64 { + if iterator.name == "CHISQ.INV" { + return iterator.fp - getChiSqDistCDF(x, iterator.fDF) + } + return iterator.fp - getTDist(x, iterator.fDF, iterator.nT) } // inverseQuadraticInterpolation inverse quadratic interpolation with @@ -6682,7 +6665,7 @@ func inverseQuadraticInterpolation(iterator calcInverseIterator, fAx, fAy, fBx, bHasToInterpolate = true } fPx, fQx, fRx, fPy, fQy = fQx, fRx, fSx, fQy, fRy - fRy = iterator.chiSqDist(fSx) + fRy = iterator.callBack(fSx) if hasChangeOfSign(fAy, fRy) { fBx, fBy = fRx, fRy } else { @@ -6697,7 +6680,7 @@ func inverseQuadraticInterpolation(iterator calcInverseIterator, fAx, fAy, fBx, // calcIterateInverse function calculates the iteration for inverse // distributions. func calcIterateInverse(iterator calcInverseIterator, fAx, fBx float64) float64 { - fAy, fBy := iterator.chiSqDist(fAx), iterator.chiSqDist(fBx) + fAy, fBy := iterator.callBack(fAx), iterator.callBack(fBx) var fTemp float64 var nCount int for nCount = 0; nCount < 1000 && !hasChangeOfSign(fAy, fBy); nCount++ { @@ -6709,13 +6692,13 @@ func calcIterateInverse(iterator calcInverseIterator, fAx, fBx float64) float64 } fBx = fTemp fBy = fAy - fAy = iterator.chiSqDist(fAx) + fAy = iterator.callBack(fAx) } else { fTemp = fBx fBx += 2 * (fBx - fAx) fAx = fTemp fAy = fBy - fBy = iterator.chiSqDist(fBx) + fBy = iterator.callBack(fBx) } } if fAy == 0 || fBy == 0 { @@ -9152,6 +9135,72 @@ func (fn *formulaFuncs) TDIST(argsList *list.List) formulaArg { return newNumberFormulaArg(getTDist(x.Number, degrees.Number, tails.Number)) } +// TdotINV function calculates the left-tailed inverse of the Student's T +// Distribution, which is a continuous probability distribution that is +// frequently used for testing hypotheses on small sample data sets. The +// syntax of the function is: +// +// T.INV(probability,degrees_freedom) +// +func (fn *formulaFuncs) TdotINV(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "T.INV requires 2 arguments") + } + var probability, degrees formulaArg + if probability = argsList.Front().Value.(formulaArg).ToNumber(); probability.Type != ArgNumber { + return probability + } + if degrees = argsList.Back().Value.(formulaArg).ToNumber(); degrees.Type != ArgNumber { + return degrees + } + if probability.Number <= 0 || probability.Number >= 1 || degrees.Number < 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if probability.Number < 0.5 { + return newNumberFormulaArg(-calcIterateInverse(calcInverseIterator{ + name: "T.INV", + fp: 1 - probability.Number, + fDF: degrees.Number, + nT: 4, + }, degrees.Number/2, degrees.Number)) + } + return newNumberFormulaArg(calcIterateInverse(calcInverseIterator{ + name: "T.INV", + fp: probability.Number, + fDF: degrees.Number, + nT: 4, + }, degrees.Number/2, degrees.Number)) +} + +// TdotINVdot2T function calculates the inverse of the two-tailed Student's T +// Distribution, which is a continuous probability distribution that is +// frequently used for testing hypotheses on small sample data sets. The +// syntax of the function is: +// +// T.INV.2T(probability,degrees_freedom) +// +func (fn *formulaFuncs) TdotINVdot2T(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "T.INV.2T requires 2 arguments") + } + var probability, degrees formulaArg + if probability = argsList.Front().Value.(formulaArg).ToNumber(); probability.Type != ArgNumber { + return probability + } + if degrees = argsList.Back().Value.(formulaArg).ToNumber(); degrees.Type != ArgNumber { + return degrees + } + if probability.Number <= 0 || probability.Number > 1 || degrees.Number < 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg(calcIterateInverse(calcInverseIterator{ + name: "T.INV.2T", + fp: probability.Number, + fDF: degrees.Number, + nT: 2, + }, degrees.Number/2, degrees.Number)) +} + // TRIMMEAN function calculates the trimmed mean (or truncated mean) of a // supplied set of values. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 6324344..8565038 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1167,6 +1167,12 @@ func TestCalcCellValue(t *testing.T) { // TDIST "=TDIST(1,10,1)": "0.17044656615103", "=TDIST(1,10,2)": "0.34089313230206", + // T.INV + "=T.INV(0.25,10)": "-0.699812061312432", + "=T.INV(0.75,10)": "0.699812061312432", + // T.INV.2T + "=T.INV.2T(1,10)": "0", + "=T.INV.2T(0.5,10)": "0.699812061312432", // TRIMMEAN "=TRIMMEAN(A1:B4,10%)": "2.5", "=TRIMMEAN(A1:B4,70%)": "2.5", @@ -3048,6 +3054,19 @@ func TestCalcCellValue(t *testing.T) { "=TDIST(-1,10,1)": "#NUM!", "=TDIST(1,0,1)": "#NUM!", "=TDIST(1,10,0)": "#NUM!", + // T.INV + "=T.INV()": "T.INV requires 2 arguments", + "=T.INV(\"\",10)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=T.INV(0.25,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=T.INV(0,10)": "#NUM!", + "=T.INV(1,10)": "#NUM!", + "=T.INV(0.25,0.5)": "#NUM!", + // T.INV.2T + "=T.INV.2T()": "T.INV.2T requires 2 arguments", + "=T.INV.2T(\"\",10)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=T.INV.2T(0.25,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=T.INV.2T(0,10)": "#NUM!", + "=T.INV.2T(0.25,0.5)": "#NUM!", // TRIMMEAN "=TRIMMEAN()": "TRIMMEAN requires 2 arguments", "=TRIMMEAN(A1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", diff --git a/file.go b/file.go index 0135e20..9707a79 100644 --- a/file.go +++ b/file.go @@ -63,7 +63,7 @@ func (f *File) Save() error { return f.SaveAs(f.Path) } -// SaveAs provides a function to create or update to an spreadsheet at the +// SaveAs provides a function to create or update to a spreadsheet at the // provided path. func (f *File) SaveAs(name string, opt ...Options) error { if len(name) > MaxFileNameLength { @@ -81,7 +81,7 @@ func (f *File) SaveAs(name string, opt ...Options) error { return ErrWorkbookExt } f.setContentTypePartProjectExtensions(contentType) - file, err := os.OpenFile(filepath.Clean(name), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o600) + file, err := os.OpenFile(filepath.Clean(name), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm) if err != nil { return err } diff --git a/lib.go b/lib.go index 4205e08..723b976 100644 --- a/lib.go +++ b/lib.go @@ -132,7 +132,7 @@ func (f *File) saveFileList(name string, content []byte) { f.Pkg.Store(name, append([]byte(xml.Header), content...)) } -// Read file content as string in a archive file. +// Read file content as string in an archive file. func readFile(file *zip.File) ([]byte, error) { rc, err := file.Open() if err != nil { @@ -157,8 +157,8 @@ func SplitCellName(cell string) (string, int, error) { if strings.IndexFunc(cell, alpha) == 0 { i := strings.LastIndexFunc(cell, alpha) if i >= 0 && i < len(cell)-1 { - col, rowstr := strings.ReplaceAll(cell[:i+1], "$", ""), cell[i+1:] - if row, err := strconv.Atoi(rowstr); err == nil && row > 0 { + col, rowStr := strings.ReplaceAll(cell[:i+1], "$", ""), cell[i+1:] + if row, err := strconv.Atoi(rowStr); err == nil && row > 0 { return col, row, nil } } @@ -187,7 +187,7 @@ func JoinCellName(col string, row int) (string, error) { } // ColumnNameToNumber provides a function to convert Excel sheet column name -// to int. Column name case insensitive. The function returns an error if +// to int. Column name case-insensitive. The function returns an error if // column name incorrect. // // Example: @@ -248,14 +248,14 @@ func ColumnNumberToName(num int) (string, error) { // excelize.CellNameToCoordinates("Z3") // returns 26, 3, nil // func CellNameToCoordinates(cell string) (int, int, error) { - colname, row, err := SplitCellName(cell) + colName, row, err := SplitCellName(cell) if err != nil { return -1, -1, newCellNameToCoordinatesError(cell, err) } if row > TotalRows { return -1, -1, ErrMaxRows } - col, err := ColumnNameToNumber(colname) + col, err := ColumnNameToNumber(colName) return col, row, err } @@ -277,8 +277,8 @@ func CoordinatesToCellName(col, row int, abs ...bool) (string, error) { sign = "$" } } - colname, err := ColumnNumberToName(col) - return sign + colname + sign + strconv.Itoa(row), err + colName, err := ColumnNumberToName(col) + return sign + colName + sign + strconv.Itoa(row), err } // areaRefToCoordinates provides a function to convert area reference to a @@ -336,6 +336,27 @@ func (f *File) coordinatesToAreaRef(coordinates []int) (string, error) { return firstCell + ":" + lastCell, err } +// getDefinedNameRefTo convert defined name to reference range. +func (f *File) getDefinedNameRefTo(definedNameName string, currentSheet string) (refTo string) { + var workbookRefTo, worksheetRefTo string + for _, definedName := range f.GetDefinedName() { + if definedName.Name == definedNameName { + // worksheet scope takes precedence over scope workbook when both definedNames exist + if definedName.Scope == "Workbook" { + workbookRefTo = definedName.RefersTo + } + if definedName.Scope == currentSheet { + worksheetRefTo = definedName.RefersTo + } + } + } + refTo = workbookRefTo + if worksheetRefTo != "" { + refTo = worksheetRefTo + } + return +} + // flatSqref convert reference sequence to cell coordinates list. func (f *File) flatSqref(sqref string) (cells map[int][][]int, err error) { var coordinates []int @@ -365,7 +386,7 @@ func (f *File) flatSqref(sqref string) (cells map[int][][]int, err error) { return } -// inCoordinates provides a method to check if an coordinate is present in +// inCoordinates provides a method to check if a coordinate is present in // coordinates array, and return the index of its location, otherwise // return -1. func inCoordinates(a [][]int, x []int) int { @@ -391,7 +412,7 @@ func inStrSlice(a []string, x string, caseSensitive bool) int { return -1 } -// inFloat64Slice provides a method to check if an element is present in an +// inFloat64Slice provides a method to check if an element is present in a // float64 array, and return the index of its location, otherwise return -1. func inFloat64Slice(a []float64, x float64) int { for idx, n := range a { @@ -405,7 +426,7 @@ func inFloat64Slice(a []float64, x float64) int { // boolPtr returns a pointer to a bool with the given value. func boolPtr(b bool) *bool { return &b } -// intPtr returns a pointer to a int with the given value. +// intPtr returns a pointer to an int with the given value. func intPtr(i int) *int { return &i } // float64Ptr returns a pointer to a float64 with the given value. @@ -626,7 +647,7 @@ func (f *File) replaceNameSpaceBytes(path string, contentMarshal []byte) []byte return bytesReplace(contentMarshal, oldXmlns, newXmlns, -1) } -// addNameSpaces provides a function to add a XML attribute by the given +// addNameSpaces provides a function to add an XML attribute by the given // component part path. func (f *File) addNameSpaces(path string, ns xml.Attr) { exist := false @@ -715,7 +736,7 @@ var ( // bstrUnmarshal parses the binary basic string, this will trim escaped string // literal which not permitted in an XML 1.0 document. The basic string -// variant type can store any valid Unicode character. Unicode characters +// variant type can store any valid Unicode character. Unicode's characters // that cannot be directly represented in XML as defined by the XML 1.0 // specification, shall be escaped using the Unicode numerical character // representation escape character format _xHHHH_, where H represents a diff --git a/numfmt.go b/numfmt.go index 685005f..b48c36a 100644 --- a/numfmt.go +++ b/numfmt.go @@ -33,9 +33,9 @@ type languageInfo struct { type numberFormat struct { section []nfp.Section t time.Time - sectionIdx int - isNumberic, hours, seconds bool - number float64 + sectionIdx int + isNumeric, hours, seconds bool + number float64 ap, afterPoint, beforePoint, localCode, result, value, valueSectionType string } @@ -279,7 +279,7 @@ var ( // prepareNumberic split the number into two before and after parts by a // decimal point. func (nf *numberFormat) prepareNumberic(value string) { - if nf.isNumberic, _ = isNumeric(value); !nf.isNumberic { + if nf.isNumeric, _ = isNumeric(value); !nf.isNumeric { return } } @@ -297,7 +297,7 @@ func format(value, numFmt string) string { if section.Type != nf.valueSectionType { continue } - if nf.isNumberic { + if nf.isNumeric { switch section.Type { case nfp.TokenSectionPositive: return nf.positiveHandler()