diff --git a/spreadsheet/formula/binaryexpr.go b/spreadsheet/formula/binaryexpr.go index 429a9d6752..3eddabedd1 100644 --- a/spreadsheet/formula/binaryexpr.go +++ b/spreadsheet/formula/binaryexpr.go @@ -63,6 +63,10 @@ func (b BinaryExpr) Eval(ctx Context, ev Evaluator) Result { return listOp(b.op, lhs.ValueList, rhs.ValueList) } + } else if lhs.Type == ResultTypeArray && (rhs.Type == ResultTypeNumber || rhs.Type == ResultTypeString) { + return arrayValueOp(b.op, lhs.ValueArray, rhs) + } else if lhs.Type == ResultTypeList && (rhs.Type == ResultTypeNumber || rhs.Type == ResultTypeString) { + return listValueOp(b.op, lhs.ValueList, rhs) } // TODO: check for and add support for binary operators on boolean values @@ -235,3 +239,91 @@ func listOp(op BinOpType, lhs, rhs []Result) Result { } return MakeListResult(res) } + +func arrayValueOp(op BinOpType, lhs [][]Result, rhs Result) Result { + // compare every item of array with a value + res := [][]Result{} + for i := range lhs { + lst := listValueOp(op, lhs[i], rhs) + if lst.Type == ResultTypeError { + return lst + } + res = append(res, lst.ValueList) + } + return MakeArrayResult(res) +} + +func listValueOp(op BinOpType, lhs []Result, rhs Result) Result { + res := []Result{} + // we can assume the arrays are the same size here + switch rhs.Type { + case ResultTypeNumber: + rv := rhs.ValueNumber + for i := range lhs { + l := lhs[i].AsNumber() + if l.Type != ResultTypeNumber { + return MakeErrorResult("non-nunmeric value in binary operation") + } + switch op { + case BinOpTypePlus: + res = append(res, MakeNumberResult(l.ValueNumber+rv)) + case BinOpTypeMinus: + res = append(res, MakeNumberResult(l.ValueNumber-rv)) + case BinOpTypeMult: + res = append(res, MakeNumberResult(l.ValueNumber*rv)) + case BinOpTypeDiv: + if rv == 0 { + return MakeErrorResultType(ErrorTypeDivideByZero, "") + } + res = append(res, MakeNumberResult(l.ValueNumber/rv)) + case BinOpTypeExp: + res = append(res, MakeNumberResult(math.Pow(l.ValueNumber, rv))) + case BinOpTypeLT: + res = append(res, MakeBoolResult(l.ValueNumber < rv)) + case BinOpTypeGT: + res = append(res, MakeBoolResult(l.ValueNumber > rv)) + case BinOpTypeEQ: + res = append(res, MakeBoolResult(l.ValueNumber == rv)) + case BinOpTypeLEQ: + res = append(res, MakeBoolResult(l.ValueNumber <= rv)) + case BinOpTypeGEQ: + res = append(res, MakeBoolResult(l.ValueNumber >= rv)) + case BinOpTypeNE: + res = append(res, MakeBoolResult(l.ValueNumber != rv)) + // TODO: support concat here + // case BinOpTypeConcat: + default: + return MakeErrorResult(fmt.Sprintf("unsupported list binary op %s", op)) + } + } + case ResultTypeString: + rv := rhs.ValueString + for i := range lhs { + l := lhs[i].AsString() + if l.Type != ResultTypeString { + return MakeErrorResult("non-nunmeric value in binary operation") + } + switch op { + case BinOpTypeLT: + res = append(res, MakeBoolResult(l.ValueString < rv)) + case BinOpTypeGT: + res = append(res, MakeBoolResult(l.ValueString > rv)) + case BinOpTypeEQ: + res = append(res, MakeBoolResult(l.ValueString == rv)) + case BinOpTypeLEQ: + res = append(res, MakeBoolResult(l.ValueString <= rv)) + case BinOpTypeGEQ: + res = append(res, MakeBoolResult(l.ValueString >= rv)) + case BinOpTypeNE: + res = append(res, MakeBoolResult(l.ValueString != rv)) + // TODO: support concat here + // case BinOpTypeConcat: + default: + return MakeErrorResult(fmt.Sprintf("unsupported list binary op %s", op)) + } + } + default: + return MakeErrorResult("non-nunmeric and non-string value in binary operation") + } + return MakeListResult(res) +} diff --git a/spreadsheet/formula/evaluator.go b/spreadsheet/formula/evaluator.go index c99332d2bc..9a4e6c7715 100644 --- a/spreadsheet/formula/evaluator.go +++ b/spreadsheet/formula/evaluator.go @@ -27,7 +27,6 @@ func NewEvaluator() Evaluator { type defEval struct { isRef bool - booleans []bool } func (d *defEval) Eval(ctx Context, formula string) Result { @@ -63,15 +62,6 @@ func (d *defEval) addInfo(ctx Context, expr Expression) { } } } - case "CONCAT", "_xlfn.CONCAT", "CONCATENATE": - d.booleans = []bool{} - for _, arg := range expr.(FunctionCall).args { - switch arg.(type) { - case CellRef: - cr := arg.(CellRef).s - d.booleans = append(d.booleans, ctx.IsBool(cr)) - } - } } } } @@ -81,7 +71,10 @@ var refRegexp *regexp.Regexp = regexp.MustCompile(`^([a-z]+)([0-9]+)$`) func validateRef(cr CellRef) bool { if submatch := refRegexp.FindStringSubmatch(strings.ToLower(cr.s)); len(submatch) > 2 { col := submatch[1] - row, _ := strconv.Atoi(submatch[2]) + row, err := strconv.Atoi(submatch[2]) + if err != nil { // for the case if the row number is bigger then int capacity + return false + } return row <= 1048576 && col <= "zz" } return false diff --git a/spreadsheet/formula/fndatetime.go b/spreadsheet/formula/fndatetime.go index 5544f2e635..c5b353a40d 100644 --- a/spreadsheet/formula/fndatetime.go +++ b/spreadsheet/formula/fndatetime.go @@ -18,6 +18,14 @@ func init() { initRegexpTime() RegisterFunction("DATE", Date) RegisterFunction("DATEDIF", DateDif) + RegisterFunction("DATEVALUE", DateValue) + RegisterFunction("DAY", Day) + RegisterFunction("DAYS", Days) + RegisterFunction("_xlfn.DAYS", Days) + RegisterFunction("EDATE", Edate) + RegisterFunction("EOMONTH", Eomonth) + RegisterFunction("MINUTE", Minute) + RegisterFunction("MONTH", Month) RegisterFunction("NOW", Now) RegisterFunction("TIME", Time) RegisterFunction("TIMEVALUE", TimeValue) @@ -26,17 +34,142 @@ func init() { RegisterFunctionComplex("YEARFRAC", YearFrac) } -var date1900 int64 = makeDateS(1900, time.January, 0) +var date1900 int64 = makeDateS(1900, time.January, 1) var daysTo1970 float64 = 25569.0 -var timeFormats map[string]*regexp.Regexp = map[string]*regexp.Regexp{} -const datePrefix = `^(([0-9])+/([0-9])+/([0-9])+ )?` +var daysInMonth = []int{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} + +var month2num = map[string]int{ + "january": 1, + "february": 2, + "march": 3, + "april": 4, + "may": 5, + "june": 6, + "july": 7, + "august": 8, + "septemper": 9, + "october": 10, + "november": 11, + "december": 12, + "jan": 1, + "feb": 2, + "mar": 3, + "apr": 4, + "jun": 6, + "jul": 7, + "aug": 8, + "sep": 9, + "oct": 10, + "nov": 11, + "dec": 12, +} + +var dateFormats = map[string]*regexp.Regexp{} +var timeFormats = map[string]*regexp.Regexp{} +var dateOnlyFormats = []*regexp.Regexp{} +var timeOnlyFormats = []*regexp.Regexp{} + +const monthRe = `((jan|january)|(feb|february)|(mar|march)|(apr|april)|(may)|(jun|june)|(jul|july)|(aug|august)|(sep|september)|(oct|october)|(nov|november)|(dec|december))` + +const df1 = `(([0-9])+)/(([0-9])+)/(([0-9])+)` +const df2 = monthRe + ` (([0-9])+), (([0-9])+)` +const df3 = `(([0-9])+)-(([0-9])+)-(([0-9])+)` +const df4 = `(([0-9])+)-` + monthRe + `-(([0-9])+)` +const datePrefix = `^((` + df1 + `|` + df2 + `|` + df3 + `|` + df4 + `) )?` + +const tfhh = `(([0-9])+) (am|pm)` +const tfhhmm = `(([0-9])+):(([0-9])+)( (am|pm))?` +const tfmmss = `(([0-9])+):(([0-9])+\.([0-9])+)( (am|pm))?` +const tfhhmmss = `(([0-9])+):(([0-9])+):(([0-9])+(\.([0-9])+)?)( (am|pm))?` +const timeSuffix = `( (` + tfhh + `|` + tfhhmm + `|` + tfmmss + `|` + tfhhmmss + `))?$` func initRegexpTime() { - timeFormats["hh"] = regexp.MustCompile(datePrefix + `(([0-9])+) (am|pm)$`) - timeFormats["hh:mm"] = regexp.MustCompile(datePrefix + `(([0-9])+):(([0-9])+)( (am|pm))?$`) - timeFormats["mm:ss"] = regexp.MustCompile(datePrefix + `(([0-9])+):(([0-9])+\.([0-9])+)( (am|pm))?$`) - timeFormats["hh:mm:ss"] = regexp.MustCompile(datePrefix + `(([0-9])+):(([0-9])+):(([0-9])+(\.([0-9])+)?)( (am|pm))?$`) + dateFormats["mm/dd/yy"] = regexp.MustCompile(`^` + df1 + timeSuffix) + dateFormats["mm dd, yy"] = regexp.MustCompile(`^` + df2 + timeSuffix) + dateFormats["yy-mm-dd"] = regexp.MustCompile(`^` + df3 + timeSuffix) + dateFormats["yy-mmStr-dd"] = regexp.MustCompile(`^` + df4 + timeSuffix) + timeFormats["hh"] = regexp.MustCompile(datePrefix + tfhh + `$`) + timeFormats["hh:mm"] = regexp.MustCompile(datePrefix + tfhhmm + `$`) + timeFormats["mm:ss"] = regexp.MustCompile(datePrefix + tfmmss + `$`) + timeFormats["hh:mm:ss"] = regexp.MustCompile(datePrefix + tfhhmmss + `$`) + dateOnlyFormats = []*regexp.Regexp{ + regexp.MustCompile(`^` + df1 + `$`), + regexp.MustCompile(`^` + df2 + `$`), + regexp.MustCompile(`^` + df3 + `$`), + regexp.MustCompile(`^` + df4 + `$`), + } + timeOnlyFormats = []*regexp.Regexp{ + regexp.MustCompile(`^` + tfhh + `$`), + regexp.MustCompile(`^` + tfhhmm + `$`), + regexp.MustCompile(`^` + tfmmss + `$`), + regexp.MustCompile(`^` + tfhhmmss + `$`), + } +} + +// Day is an implementation of the Excel DAY() function. +func Day(args []Result) Result { + if len(args) != 1 { + return MakeErrorResult("DAY requires one argument") + } + dateArg := args[0] + switch dateArg.Type { + case ResultTypeEmpty: + return MakeNumberResult(0) + case ResultTypeNumber: + date := dateFromDays(dateArg.ValueNumber) + return MakeNumberResult(float64(date.Day())) + case ResultTypeString: + dateString := strings.ToLower(dateArg.ValueString) + if !checkDateOnly(dateString) { // If time also presents in string, we should validate it first as Excel does + _, _, _, _, dateIsEmpty, errResult := timeValue(dateString) + if errResult.Type == ResultTypeError { + errResult.ErrorMessage = "Incorrect arguments for DAY" + return errResult + } + if dateIsEmpty { + return MakeNumberResult(0) + } + } + _, _, day, _, errResult := dateValue(dateString) + if errResult.Type == ResultTypeError { + return errResult + } + return MakeNumberResult(float64(day)) + default: + return MakeErrorResult("Incorrect argument for DAY") + } +} + +// Days is an implementation of the Excel DAYS() function. +func Days(args []Result) Result { + if len(args) != 2 { + return MakeErrorResult("DAYS requires two arguments") + } + var sd, ed float64 + if args[0].Type == ResultTypeNumber { + ed = args[0].ValueNumber + } else { + edResult := DateValue([]Result{args[0]}) + if edResult.Type == ResultTypeError { + return MakeErrorResult("Incorrect end date for DAYS") + } + ed = edResult.ValueNumber + } + if args[1].Type == ResultTypeNumber { + sd = args[1].ValueNumber + if sd < 62 && ed >= 62 { + sd-- + } + } else { + sdResult := DateValue([]Result{args[1]}) + if sdResult.Type == ResultTypeError { + return MakeErrorResult("Incorrect start date for DAYS") + } + sd = sdResult.ValueNumber + } + days := float64(int(ed - sd)) + return MakeNumberResult(days) } // Date is an implementation of the Excel DATE() function. @@ -74,7 +207,7 @@ func daysFromDate(y,m,d int) float64 { // DateDif is an implementation of the Excel DATEDIF() function. func DateDif(args []Result) Result { if len(args) != 3 || args[0].Type != ResultTypeNumber || args[1].Type != ResultTypeNumber || args[2].Type != ResultTypeString { - return MakeErrorResultType(ErrorTypeValue, "DATEDIF requires two number and one string argument") + return MakeErrorResult("DATEDIF requires two number and one string argument") } startDateDays := args[0].ValueNumber endDateDays := args[1].ValueNumber @@ -138,6 +271,304 @@ func DateDif(args []Result) Result { return MakeNumberResult(diff) } +const dvErrMsg = "Incorrect argument for DATEVALUE" + +// DateValue is an implementation of the Excel DATEVALUE() function. +func DateValue(args []Result) Result { + if len(args) != 1 || args[0].Type != ResultTypeString { + return MakeErrorResult("DATEVALUE requires a single string arguments") + } + dateString := strings.ToLower(args[0].ValueString) + if !checkDateOnly(dateString) { // If time also presents in string, we should validate it first as Excel does + _, _, _, _, dateIsEmpty, errResult := timeValue(dateString) + if errResult.Type == ResultTypeError { + errResult.ErrorMessage = "Incorrect arguments for DATEVALUE" + return errResult + } + if dateIsEmpty { + return MakeNumberResult(0) + } + } + year, month, day, _, errResult := dateValue(dateString) + if errResult.Type == ResultTypeError { + return errResult + } + return MakeNumberResult(daysFromDate(year, month, day)) +} + +func checkDateOnly(dateString string) bool { + for _, df := range dateOnlyFormats { + submatch := df.FindStringSubmatch(dateString) + if len(submatch) > 1 { + return true + } + } + return false +} + +// dateValue is a helper for DateValue which is used also by TimeValue to validate date part of the string. +// It returns output in a format of (hours, minutes, seconds, timeIsEmpty, errorResult), where timeIsEmpty is true if the input string contains only date (say, 11/11/2019, not 11/11/2019 12:14:18), errorResult is of ResultTypeError if an error occurs and ResultTypeEmpty if not. +func dateValue(dateString string) (int, int, int, bool, Result) { + pattern := "" + submatch := []string{} + for key, df := range dateFormats { + submatch = df.FindStringSubmatch(dateString) + if len(submatch) > 1 { + pattern = key + break + } + } + if pattern == "" { + return 0, 0, 0, false, MakeErrorResultType(ErrorTypeValue, dvErrMsg) + } + timeIsEmpty := false + + var year, month, day int + var err error + + switch pattern { + case "mm/dd/yy": + month, err = strconv.Atoi(submatch[1]) + if err != nil { + return 0, 0, 0, false, MakeErrorResultType(ErrorTypeValue, dvErrMsg) + } + day, err = strconv.Atoi(submatch[3]) + if err != nil { + return 0, 0, 0, false, MakeErrorResultType(ErrorTypeValue, dvErrMsg) + } + year, err = strconv.Atoi(submatch[5]) + if err != nil { + return 0, 0, 0, false, MakeErrorResultType(ErrorTypeValue, dvErrMsg) + } + if year < 0 || year > 9999 || (year > 99 && year < 1900) { + return 0, 0, 0, false, MakeErrorResultType(ErrorTypeValue, dvErrMsg) + } + year = modifyYear(year) + timeIsEmpty = submatch[8] == "" + case "mm dd, yy": + month = month2num[submatch[1]] + day, err = strconv.Atoi(submatch[14]) + if err != nil { + return 0, 0, 0, false, MakeErrorResultType(ErrorTypeValue, dvErrMsg) + } + year, err = strconv.Atoi(submatch[16]) + if err != nil { + return 0, 0, 0, false, MakeErrorResultType(ErrorTypeValue, dvErrMsg) + } + if year < 0 || year > 9999 || (year > 99 && year < 1900) { + return 0, 0, 0, false, MakeErrorResultType(ErrorTypeValue, dvErrMsg) + } + year = modifyYear(year) + timeIsEmpty = submatch[19] == "" + case "yy-mm-dd": + v1, err := strconv.Atoi(submatch[1]) + if err != nil { + return 0, 0, 0, false, MakeErrorResultType(ErrorTypeValue, dvErrMsg) + } + v2, err := strconv.Atoi(submatch[3]) + if err != nil { + return 0, 0, 0, false, MakeErrorResultType(ErrorTypeValue, dvErrMsg) + } + v3, err := strconv.Atoi(submatch[5]) + if err != nil { + return 0, 0, 0, false, MakeErrorResultType(ErrorTypeValue, dvErrMsg) + } + if v1 >= 1900 && v1 < 10000 { + year = v1 + month = v2 + day = v3 + } else if v1 > 0 && v1 < 13 { + month = v1 + day = v2 + year = v3 + } else { + return 0, 0, 0, false, MakeErrorResultType(ErrorTypeValue, dvErrMsg) + } + timeIsEmpty = submatch[8] == "" + case "yy-mmStr-dd": + year, err = strconv.Atoi(submatch[16]) + if err != nil { + return 0, 0, 0, false, MakeErrorResultType(ErrorTypeValue, dvErrMsg) + } + month = month2num[submatch[3]] + day, err = strconv.Atoi(submatch[1]) + if err != nil { + return 0, 0, 0, false, MakeErrorResultType(ErrorTypeValue, dvErrMsg) + } + timeIsEmpty = submatch[19] == "" + } + if !validateDate(year, month, day) { + return 0, 0, 0, false, MakeErrorResultType(ErrorTypeValue, dvErrMsg) + } + return year, month, day, timeIsEmpty, MakeEmptyResult() +} + +func validateDate(year, month, day int) bool { + if month < 1 || month > 12 { + return false + } + if day < 1 { + return false + } + if month == 2 && isLeapYear(year) { + return day <= 29 + } else { + return day <= daysInMonth[month-1] + } +} + +func modifyYear(year int) int { + if year < 1900 { + if year < 30 { + year += 2000 + } else { + year += 1900 + } + } + return year +} + +// Edate is an implementation of the Excel EDATE() function. +func Edate(args []Result) Result { + if len(args) != 2 { + return MakeErrorResult("EDATE requires two arguments") + } + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("Incorrect argument for EDATE") + } + mDelta := args[1].ValueNumber + initialDateArg := args[0] + var initialDateDay float64 + switch initialDateArg.Type { + case ResultTypeEmpty: + return MakeErrorResultType(ErrorTypeNum, "Incorrect argument for EDATE") + case ResultTypeNumber: + initialDateDay = initialDateArg.ValueNumber + case ResultTypeString: + initialDateDayResult := DateValue([]Result{args[0]}) + if initialDateDayResult.Type == ResultTypeError { + return MakeErrorResult("Incorrect argument for EDATE") + } + initialDateDay = initialDateDayResult.ValueNumber + default: + return MakeErrorResult("Incorrect argument for EDATE") + } + initialDate := dateFromDays(initialDateDay) + newDate := initialDate.AddDate(0, int(mDelta), 0) + y, m, d := newDate.Date() + newDays := daysFromDate(y, int(m), d) + if newDays < 1 { + return MakeErrorResultType(ErrorTypeNum, "Incorrect argument for EDATE") + } + return MakeNumberResult(newDays) +} + +// Eomonth is an implementation of the Excel EOMONTH() function. +func Eomonth(args []Result) Result { + if len(args) != 2 { + return MakeErrorResult("EOMONTH requires two arguments") + } + if args[1].Type != ResultTypeNumber { + return MakeErrorResult("Incorrect argument for EOMONTH") + } + mDelta := args[1].ValueNumber + initialDateArg := args[0] + var initialDateDay float64 + switch initialDateArg.Type { + case ResultTypeEmpty: + initialDateDay = 0 + case ResultTypeNumber: + initialDateDay = initialDateArg.ValueNumber + case ResultTypeString: + initialDateDayResult := DateValue([]Result{args[0]}) + if initialDateDayResult.Type == ResultTypeError { + return MakeErrorResult("Incorrect argument for EOMONTH") + } + initialDateDay = initialDateDayResult.ValueNumber + default: + return MakeErrorResult("Incorrect argument for EOMONTH") + } + initialDate := dateFromDays(initialDateDay) + newDate := initialDate.AddDate(0, int(mDelta+1), 0) + y, m, _ := newDate.Date() + eomonth := daysFromDate(y, int(m), 0) + if eomonth < 1 { + return MakeErrorResultType(ErrorTypeNum, "Incorrect argument for EOMONTH") + } + if y == 1900 && m == 3 { + eomonth-- + } + return MakeNumberResult(eomonth) +} + +// Minute is an implementation of the Excel MINUTE() function. +func Minute(args []Result) Result { + if len(args) != 1 { + return MakeErrorResult("MINUTE requires one argument") + } + timeArg := args[0] + switch timeArg.Type { + case ResultTypeEmpty: + return MakeNumberResult(0) + case ResultTypeNumber: + date := dateFromDays(timeArg.ValueNumber) + return MakeNumberResult(float64(date.Minute())) + case ResultTypeString: + timeString := strings.ToLower(timeArg.ValueString) + if !checkTimeOnly(timeString) { // If date also presents in string, we should validate it first as Excel does + _, _, _, timeIsEmpty, errResult := dateValue(timeString) + if errResult.Type == ResultTypeError { + errResult.ErrorMessage = "Incorrect arguments for MINUTE" + return errResult + } + if timeIsEmpty { + return MakeNumberResult(0) + } + } + _, minute, _, _, _, errResult := timeValue(timeString) + if errResult.Type == ResultTypeError { + return errResult + } + return MakeNumberResult(float64(minute)) + default: + return MakeErrorResult("Incorrect argument for MINUTE") + } +} + +// Month is an implementation of the Excel MONTH() function. +func Month(args []Result) Result { + if len(args) != 1 { + return MakeErrorResult("MONTH requires one argument") + } + dateArg := args[0] + switch dateArg.Type { + case ResultTypeEmpty: + return MakeNumberResult(1) + case ResultTypeNumber: + date := dateFromDays(dateArg.ValueNumber) + return MakeNumberResult(float64(date.Month())) + case ResultTypeString: + dateString := strings.ToLower(dateArg.ValueString) + if !checkDateOnly(dateString) { // If time also presents in string, we should validate it first as Excel does + _, _, _, _, dateIsEmpty, errResult := timeValue(dateString) + if errResult.Type == ResultTypeError { + errResult.ErrorMessage = "Incorrect arguments for MONTH" + return errResult + } + if dateIsEmpty { + return MakeNumberResult(1) + } + } + _, month, _, _, errResult := dateValue(dateString) + if errResult.Type == ResultTypeError { + return errResult + } + return MakeNumberResult(float64(month)) + default: + return MakeErrorResult("Incorrect argument for MONTH") + } +} + // Now is an implementation of the Excel NOW() function. func Now(args []Result) Result { if len(args) > 0 { @@ -188,6 +619,42 @@ func TimeValue(args []Result) Result { return MakeErrorResult("TIMEVALUE requires a single string arguments") } timeString := strings.ToLower(args[0].ValueString) + if !checkTimeOnly(timeString) { // If date also presents in string, we should validate it first as Excel does + _, _, _, timeIsEmpty, errResult := dateValue(timeString) + if errResult.Type == ResultTypeError { + errResult.ErrorMessage = "Incorrect arguments for TIMEVALUE" + return errResult + } + if timeIsEmpty { + return MakeNumberResult(0) + } + } + hours, minutes, seconds, pm, _, errResult := timeValue(timeString) + if errResult.Type == ResultTypeError { + return errResult + } + resultValue := daysFromTime(float64(hours), float64(minutes), seconds) + if pm { + resultValue += 0.5 + } else if resultValue >= 1 { + resultValue -= float64(int(resultValue)) + } + return MakeNumberResult(resultValue) +} + +func checkTimeOnly(timeString string) bool { + for _, tf := range timeOnlyFormats { + submatch := tf.FindStringSubmatch(timeString) + if len(submatch) > 1 { + return true + } + } + return false +} + +// timeValue is a helper for TimeValue which is used also by DateValue to validate time part of the string. +// It returns output in a format of (year, month, day, pm, dateIsEmpty, errorResult), where pm is true if the time is marked as PM, dateIsEmpty is true if the input string contains only time (say, 12:14:18, not 11/11/2019 12:14:18), errorResult is of ResultTypeError if an error occurs and ResultTypeEmpty if not. +func timeValue(timeString string) (int, int, float64, bool, bool, Result) { pattern := "" submatch := []string{} for key, tf := range timeFormats { @@ -198,9 +665,10 @@ func TimeValue(args []Result) Result { } } if pattern == "" { - return MakeErrorResult(tvErrMsg) + return 0, 0, 0, false, false, MakeErrorResultType(ErrorTypeValue, tvErrMsg) } - submatch = submatch[5:] // cut off date + dateIsEmpty := submatch[1] == "" + submatch = submatch[49:] // cut off date l := len(submatch) last := submatch[l-1] @@ -215,63 +683,57 @@ func TimeValue(args []Result) Result { case "hh": hours, err = strconv.Atoi(submatch[0]) if err != nil { - return MakeErrorResult(tvErrMsg) + return 0, 0, 0, false, false, MakeErrorResultType(ErrorTypeValue, tvErrMsg) } minutes = 0 seconds = 0 case "hh:mm": hours, err = strconv.Atoi(submatch[0]) if err != nil { - return MakeErrorResult(tvErrMsg) + return 0, 0, 0, false, false, MakeErrorResultType(ErrorTypeValue, tvErrMsg) } minutes, err = strconv.Atoi(submatch[2]) if err != nil { - return MakeErrorResult(tvErrMsg) + return 0, 0, 0, false, false, MakeErrorResultType(ErrorTypeValue, tvErrMsg) } seconds = 0 case "mm:ss": hours = 0 minutes, err = strconv.Atoi(submatch[0]) if err != nil { - return MakeErrorResult(tvErrMsg) + return 0, 0, 0, false, false, MakeErrorResultType(ErrorTypeValue, tvErrMsg) } seconds, err = strconv.ParseFloat(submatch[2], 64) if err != nil { - return MakeErrorResult(tvErrMsg) + return 0, 0, 0, false, false, MakeErrorResultType(ErrorTypeValue, tvErrMsg) } case "hh:mm:ss": hours, err = strconv.Atoi(submatch[0]) if err != nil { - return MakeErrorResult(tvErrMsg) + return 0, 0, 0, false, false, MakeErrorResultType(ErrorTypeValue, tvErrMsg) } minutes, err = strconv.Atoi(submatch[2]) if err != nil { - return MakeErrorResult(tvErrMsg) + return 0, 0, 0, false, false, MakeErrorResultType(ErrorTypeValue, tvErrMsg) } seconds, err = strconv.ParseFloat(submatch[4], 64) if err != nil { - return MakeErrorResult(tvErrMsg) + return 0, 0, 0, false, false, MakeErrorResultType(ErrorTypeValue, tvErrMsg) } } if minutes >= 60 { - return MakeErrorResult(tvErrMsg) + return 0, 0, 0, false, false, MakeErrorResultType(ErrorTypeValue, tvErrMsg) } if am || pm { if hours > 12 || seconds >= 60 { - return MakeErrorResult(tvErrMsg) + return 0, 0, 0, false, false, MakeErrorResultType(ErrorTypeValue, tvErrMsg) } else if hours == 12 { hours = 0 } } else if hours >= 24 || seconds >= 10000 { - return MakeErrorResult(tvErrMsg) + return 0, 0, 0, false, false, MakeErrorResultType(ErrorTypeValue, tvErrMsg) } - resultValue := daysFromTime(float64(hours), float64(minutes), seconds) - if pm { - resultValue += 0.5 - } else if resultValue >= 1 { - resultValue -= float64(int(resultValue)) - } - return MakeNumberResult(resultValue) + return hours, minutes, seconds, pm, dateIsEmpty, MakeEmptyResult() } // Year is an implementation of the Excel YEAR() function. @@ -350,6 +812,9 @@ func YearFrac(ctx Context, ev Evaluator, args []Result) Result { } func makeDateS(y int, m time.Month, d int) int64 { + if y == 1900 && int(m) <= 2 { + d-- + } date := time.Date(y, m, d, 0, 0, 0, 0, time.UTC) return date.Unix() } diff --git a/spreadsheet/formula/fnlogical.go b/spreadsheet/formula/fnlogical.go index 58026d5335..4c73f50c02 100644 --- a/spreadsheet/formula/fnlogical.go +++ b/spreadsheet/formula/fnlogical.go @@ -73,31 +73,177 @@ func If(args []Result) Result { if len(args) > 3 { return MakeErrorResult("IF accepts at most three arguments") } + cond := args[0] switch cond.Type { - case ResultTypeNumber: case ResultTypeError: return cond + case ResultTypeNumber: + // single argument just returns the condition value + if len(args) == 1 { + return MakeBoolResult(cond.ValueNumber != 0) + } + + // true case + if cond.ValueNumber != 0 { + return args[1] + } + + // false case + if len(args) == 3 { + return args[2] + } + case ResultTypeList: + return MakeListResult(ifList(args)) + case ResultTypeArray: + return ifArray(args) + default: - return MakeErrorResult("IF initial argument must be numeric") + return MakeErrorResult("IF initial argument must be numeric or array") + } + return MakeBoolResult(false) +} + +func fillArray(arg Result, rows, cols int) [][]Result { + array := [][]Result{} + switch arg.Type { + case ResultTypeArray: + return arg.ValueArray + case ResultTypeNumber, ResultTypeString: + for r := 0; r < rows; r++ { + list := []Result{} + for c := 0; c < cols; c++ { + list = append(list, arg) + } + array = append(array, list) + } + return array + case ResultTypeList: + return [][]Result{arg.ValueList} + } + return [][]Result{} +} + +func ifArray(args []Result) Result { + condArray := args[0].ValueArray + // single argument returns list of contitions + if len(args) == 1 { + result := [][]Result{} + for _, v := range condArray { + result = append(result, ifList([]Result{MakeListResult(v)})) + } + return MakeArrayResult(result) + } else if len(args) == 2 { + rows := len(condArray) + cols := len(condArray[0]) + truesArray := fillArray(args[1], rows, cols) + tl := len(truesArray) + result := [][]Result{} + var truesList []Result + for i, v := range condArray { + if i < tl { + truesList = truesArray[i] + } else { + truesList = []Result{} + for j := 0; j < cols; j++ { + truesList = append(truesList, MakeErrorResultType(ErrorTypeValue, "")) + } + } + result = append(result, ifList([]Result{MakeListResult(v), MakeListResult(truesList)})) + } + return MakeArrayResult(result) + } else if len(args) == 3 { + rows := len(condArray) + cols := len(condArray[0]) + truesArray := fillArray(args[1], rows, cols) + falsesArray := fillArray(args[2], rows, cols) + tl := len(truesArray) + fl := len(falsesArray) + result := [][]Result{} + var truesList, falsesList []Result + for i, v := range condArray { + if i < tl { + truesList = truesArray[i] + } else { + truesList = []Result{} + for j := 0; j < cols; j++ { + truesList = append(truesList, MakeErrorResultType(ErrorTypeValue, "")) + } + } + if i < fl { + falsesList = falsesArray[i] + } else { + falsesList = []Result{} + for j := 0; j < cols; j++ { + falsesList = append(falsesList, MakeErrorResultType(ErrorTypeValue, "")) + } + } + result = append(result, ifList([]Result{MakeListResult(v), MakeListResult(truesList), MakeListResult(falsesList)})) + } + return MakeArrayResult(result) } + return MakeErrorResultType(ErrorTypeValue, "") +} - // single argument just returns the condition value +func ifList(args []Result) []Result { + condList := args[0].ValueList + // single argument returns list of contitions if len(args) == 1 { - return MakeBoolResult(cond.ValueNumber != 0) + result := []Result{} + for _, v := range condList { + result = append(result, MakeBoolResult(v.ValueNumber != 0)) + } + return result } - // true case - if cond.ValueNumber != 0 { - return args[1] + // two arguments case + if len(args) == 2 { + trues := args[1].ValueList + tl := len(trues) + result := []Result{} + for i, v := range condList { + var newValue Result + if v.ValueNumber == 0 { + newValue = MakeBoolResult(false) + } else { + if i < tl { + newValue = trues[i] + } else { + newValue = MakeErrorResultType(ErrorTypeValue, "") + } + } + result = append(result, newValue) + } + return result } // false case if len(args) == 3 { - return args[2] + trues := args[1].ValueList + falses := args[2].ValueList + tl := len(trues) + tf := len(falses) + result := []Result{} + for i, v := range condList { + var newValue Result + if v.ValueNumber != 0 { + if i < tl { + newValue = trues[i] + } else { + newValue = MakeErrorResultType(ErrorTypeValue, "") + } + } else { + if i < tf { + newValue = falses[i] + } else { + newValue = MakeErrorResultType(ErrorTypeValue, "") + } + } + result = append(result, newValue) + } + return result } - return MakeBoolResult(false) - + return []Result{} } // IfError is an implementation of the Excel IFERROR() function. It takes two arguments. diff --git a/spreadsheet/formula/fnstatistical.go b/spreadsheet/formula/fnstatistical.go index 42254ac165..e94c4bf526 100644 --- a/spreadsheet/formula/fnstatistical.go +++ b/spreadsheet/formula/fnstatistical.go @@ -55,8 +55,10 @@ func sumCount(args []Result, countText bool) (float64, float64) { for _, arg := range args { switch arg.Type { case ResultTypeNumber: - sum += arg.ValueNumber - cnt++ + if countText || !arg.IsBoolean { + sum += arg.ValueNumber + cnt++ + } case ResultTypeList, ResultTypeArray: s, c := sumCount(arg.ListValues(), countText) sum += s @@ -107,7 +109,7 @@ func count(args []Result, m countMode) float64 { for _, arg := range args { switch arg.Type { case ResultTypeNumber: - if m != countEmpty { + if m == countText || (m == countNormal && !arg.IsBoolean) { cnt++ } case ResultTypeList, ResultTypeArray: @@ -381,7 +383,7 @@ func max(args []Result, isMaxA bool) Result { for _, a := range args { switch a.Type { case ResultTypeNumber: - if a.ValueNumber > v { + if (isMaxA || !a.IsBoolean) && a.ValueNumber > v { v = a.ValueNumber } case ResultTypeList, ResultTypeArray: @@ -432,7 +434,7 @@ func min(args []Result, isMinA bool) Result { for _, a := range args { switch a.Type { case ResultTypeNumber: - if a.ValueNumber < v { + if (isMinA || !a.IsBoolean) && a.ValueNumber < v { v = a.ValueNumber } case ResultTypeList, ResultTypeArray: @@ -480,7 +482,9 @@ func extractNumbers(args []Result) []float64 { a = a.AsNumber() switch a.Type { case ResultTypeNumber: - values = append(values, a.ValueNumber) + if !a.IsBoolean { + values = append(values, a.ValueNumber) + } case ResultTypeList, ResultTypeArray: values = append(values, extractNumbers(a.ListValues())...) case ResultTypeString: @@ -510,6 +514,9 @@ func Median(args []Result) Result { } func compare(value Result, criteria *criteriaParsed) bool { + if value.IsBoolean { + return false + } t := value.Type if criteria.isNumber { return t == ResultTypeNumber && value.ValueNumber == criteria.cNum diff --git a/spreadsheet/formula/fntext.go b/spreadsheet/formula/fntext.go index a91d89eefd..7c74cc2ea5 100644 --- a/spreadsheet/formula/fntext.go +++ b/spreadsheet/formula/fntext.go @@ -23,9 +23,9 @@ func init() { RegisterFunction("CHAR", Char) RegisterFunction("CLEAN", Clean) RegisterFunction("CODE", Code) - RegisterFunctionComplex("CONCATENATE", Concat) - RegisterFunctionComplex("CONCAT", Concat) - RegisterFunctionComplex("_xlfn.CONCAT", Concat) + RegisterFunction("CONCATENATE", Concat) + RegisterFunction("CONCAT", Concat) + RegisterFunction("_xlfn.CONCAT", Concat) // RegisterFunction("DBCS") // RegisterFunction("DOLLAR") Need to test with Excel RegisterFunction("EXACT", Exact) @@ -130,16 +130,15 @@ func Unicode(args []Result) Result { } // Concat is an implementation of the Excel CONCAT() and deprecated CONCATENATE() function. -func Concat(ctx Context, ev Evaluator, args []Result) Result { - eval := ev.(*defEval) +func Concat(args []Result) Result { buf := bytes.Buffer{} - for i, a := range args { + for _, a := range args { switch a.Type { case ResultTypeString: buf.WriteString(a.ValueString) case ResultTypeNumber: var str string - if eval.booleans[i] { + if a.IsBoolean { if a.ValueNumber == 0 { str = "FALSE" } else { diff --git a/spreadsheet/formula/functions_test.go b/spreadsheet/formula/functions_test.go index 439007b65f..f192c34add 100644 --- a/spreadsheet/formula/functions_test.go +++ b/spreadsheet/formula/functions_test.go @@ -1133,6 +1133,8 @@ func TestTimeValue(t *testing.T) { td := []testStruct{ {`=TIMEVALUE("1/1/1900 00:00:00")`, `0 ResultTypeNumber`}, {`=TIMEVALUE("1/1/1900 12:00:00")`, `0.5 ResultTypeNumber`}, + {`=TIMEVALUE("1/1/1900")`, `0 ResultTypeNumber`}, + {`=TIMEVALUE("02:12:18 PM")`, `0.591875 ResultTypeNumber`}, {`=TIMEVALUE("a")`, `#VALUE! ResultTypeError`}, } @@ -1140,6 +1142,40 @@ func TestTimeValue(t *testing.T) { runTests(t, ctx, td) } +func TestDay(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + td := []testStruct{ + {`=DAY("02-29-2019")`, `#VALUE! ResultTypeError`}, + {`=DAY("02-29-2020")`, `29 ResultTypeNumber`}, + {`=DAY("01/03/2019 12:14:16")`, `3 ResultTypeNumber`}, + {`=DAY("January 25, 2020 01:03 AM")`, `25 ResultTypeNumber`}, + } + + ctx := sheet.FormulaContext() + runTests(t, ctx, td) +} + +func TestDays(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + sheet.Cell("A1").SetTime(time.Date(2021, time.February, 28, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A2").SetTime(time.Date(1900, time.January, 25, 0, 0, 0, 0, time.UTC)) + sheet.Cell("A3").SetString("02/28/2021") + sheet.Cell("A4").SetString("01/25/1900") + + td := []testStruct{ + {`=DAYS(A1,A2)`, `44230 ResultTypeNumber`}, + {`=DAYS(A3,A4)`, `44230 ResultTypeNumber`}, + {`=DAYS(A3,"02/29/1900")`, `#VALUE! ResultTypeError`}, + } + + ctx := sheet.FormulaContext() + runTests(t, ctx, td) +} + func TestDate(t *testing.T) { ss := spreadsheet.New() sheet := ss.AddSheet() @@ -1154,6 +1190,24 @@ func TestDate(t *testing.T) { runTests(t, ctx, td) } +func TestDateValue(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + td := []testStruct{ + {`=DATEVALUE("1/1/1900 00:00:00")`, `1 ResultTypeNumber`}, + {`=DATEVALUE("1/1/1900 7:00 AM")`, `1 ResultTypeNumber`}, + {`=DATEVALUE("1/1/1900")`, `1 ResultTypeNumber`}, + {`=DATEVALUE("11/27/2019")`, `43796 ResultTypeNumber`}, + {`=DATEVALUE("25-Jan-2019")`, `43490 ResultTypeNumber`}, + {`=DATEVALUE("02:12:18 PM")`, `0 ResultTypeNumber`}, + {`=DATEVALUE("a")`, `#VALUE! ResultTypeError`}, + } + + ctx := sheet.FormulaContext() + runTests(t, ctx, td) +} + func TestDateDif(t *testing.T) { ss := spreadsheet.New() sheet := ss.AddSheet() @@ -1175,3 +1229,66 @@ func TestDateDif(t *testing.T) { ctx := sheet.FormulaContext() runTests(t, ctx, td) } + +func TestMinute(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + td := []testStruct{ + {`=MINUTE("02-29-2019 14:18:16")`, `#VALUE! ResultTypeError`}, + {`=MINUTE("02-29-2020 14:18:16")`, `18 ResultTypeNumber`}, + {`=MINUTE("01/03/2019 12:14")`, `14 ResultTypeNumber`}, + {`=MINUTE("January 25, 2020 01:03 AM")`, `3 ResultTypeNumber`}, + } + + ctx := sheet.FormulaContext() + runTests(t, ctx, td) +} + +func TestMonth(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + td := []testStruct{ + {`=MONTH("02-29-2019")`, `#VALUE! ResultTypeError`}, + {`=MONTH("02-29-2020")`, `2 ResultTypeNumber`}, + {`=MONTH("01/03/2019 12:14:16")`, `1 ResultTypeNumber`}, + {`=MONTH("February 25, 2020 01:03 AM")`, `2 ResultTypeNumber`}, + {`=MONTH("12:14:16")`, `1 ResultTypeNumber`}, + } + + ctx := sheet.FormulaContext() + runTests(t, ctx, td) +} + +func TestEdate(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + td := []testStruct{ + {`=EDATE("02-29-2019",-6)`, `#VALUE! ResultTypeError`}, + {`=EDATE("02-29-2020",-6)`, `43706 ResultTypeNumber`}, + {`=EDATE("06/30/1900 12:14:16",-6)`, `#NUM! ResultTypeError`}, + {`=EDATE("07/01/1900 12:14:16",-6)`, `1 ResultTypeNumber`}, + {`=EDATE("01:03 AM",-6)`, `#NUM! ResultTypeError`}, + } + + ctx := sheet.FormulaContext() + runTests(t, ctx, td) +} + +func TestEomonth(t *testing.T) { + ss := spreadsheet.New() + sheet := ss.AddSheet() + + td := []testStruct{ + {`=EOMONTH("02-29-2019",-6)`, `#VALUE! ResultTypeError`}, + {`=EOMONTH("02-29-2020",-6)`, `43708 ResultTypeNumber`}, + {`=EOMONTH("06/30/1900 12:14:16",-6)`, `#NUM! ResultTypeError`}, + {`=EOMONTH("07/01/1900 12:14:16",-6)`, `31 ResultTypeNumber`}, + {`=EOMONTH("01:03 AM",-6)`, `#NUM! ResultTypeError`}, + } + + ctx := sheet.FormulaContext() + runTests(t, ctx, td) +} diff --git a/spreadsheet/formula/result.go b/spreadsheet/formula/result.go index be4f2a96c1..647712da62 100644 --- a/spreadsheet/formula/result.go +++ b/spreadsheet/formula/result.go @@ -33,6 +33,7 @@ type Result struct { ValueString string ValueList []Result ValueArray [][]Result + IsBoolean bool ErrorMessage string Type ResultType @@ -135,9 +136,9 @@ func MakeNumberResult(v float64) Result { // MakeBoolResult constructs a boolean result (internally a number). func MakeBoolResult(b bool) Result { if b { - return Result{Type: ResultTypeNumber, ValueNumber: 1} + return Result{Type: ResultTypeNumber, ValueNumber: 1, IsBoolean: true} } - return Result{Type: ResultTypeNumber, ValueNumber: 0} + return Result{Type: ResultTypeNumber, ValueNumber: 0, IsBoolean: true} } // MakeErrorResult constructs a #VALUE! error with a given extra error message. diff --git a/spreadsheet/formula/testdata/formulareference.xlsx b/spreadsheet/formula/testdata/formulareference.xlsx index 3bfc557073..c005798c02 100644 Binary files a/spreadsheet/formula/testdata/formulareference.xlsx and b/spreadsheet/formula/testdata/formulareference.xlsx differ diff --git a/spreadsheet/row.go b/spreadsheet/row.go index 9b3dcaba87..c59ea60a2d 100644 --- a/spreadsheet/row.go +++ b/spreadsheet/row.go @@ -98,7 +98,24 @@ func (r Row) AddCell() Cell { // to the slice will have no effect. func (r Row) Cells() []Cell { ret := []Cell{} + lastIndex := -1 for _, c := range r.x.C { + if c.RAttr == nil { + unioffice.Log("RAttr is nil for a cell, skipping.") + continue + } + ref, err := reference.ParseCellReference(*c.RAttr) + if err != nil { + unioffice.Log("RAttr is incorrect for a cell: " + *c.RAttr + ", skipping.") + continue + } + currentIndex := int(ref.ColumnIdx) + if currentIndex - lastIndex > 1 { + for col := lastIndex + 1; col < currentIndex; col++ { + ret = append(ret, r.AddNamedCell(reference.IndexToColumn(uint32(col)))) + } + } + lastIndex = currentIndex ret = append(ret, Cell{r.w, r.s, r.x, c}) } return ret diff --git a/spreadsheet/workbook.go b/spreadsheet/workbook.go index 6f997180cf..122726ebba 100644 --- a/spreadsheet/workbook.go +++ b/spreadsheet/workbook.go @@ -263,7 +263,9 @@ func (wb *Workbook) Save(w io.Writer) error { fmt.Println("Unlicensed version of UniOffice") fmt.Println("- Get a license on https://unidoc.io") for _, sheet := range wb.Sheets() { - a1 := sheet.Cell("A1") + row1 := sheet.Row(1) + row1.SetHeight(50) + a1 := row1.Cell("A") rt := a1.SetRichTextString() run := rt.AddRun()