This closes #1002, new fn: DAY ref #65

Co-authored-by: Stani Michiels <git@rchtct.com>
Co-authored-by: xuri <xuri.me@gmail.com>
This commit is contained in:
Stani 2021-08-21 05:50:49 +02:00 committed by GitHub
parent dca03c6230
commit 935af2e356
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 447 additions and 40 deletions

385
calc.go
View File

@ -34,29 +34,111 @@ import (
"golang.org/x/text/message"
)
// Excel formula errors
const (
// Excel formula errors
formulaErrorDIV = "#DIV/0!"
formulaErrorNAME = "#NAME?"
formulaErrorNA = "#N/A"
formulaErrorNUM = "#NUM!"
formulaErrorVALUE = "#VALUE!"
formulaErrorREF = "#REF!"
formulaErrorNULL = "#NULL"
formulaErrorNULL = "#NULL!"
formulaErrorSPILL = "#SPILL!"
formulaErrorCALC = "#CALC!"
formulaErrorGETTINGDATA = "#GETTING_DATA"
// formula criteria condition enumeration.
_ byte = iota
criteriaEq
criteriaLe
criteriaGe
criteriaL
criteriaG
criteriaBeg
criteriaEnd
criteriaErr
// Numeric precision correct numeric values as legacy Excel application
// https://en.wikipedia.org/wiki/Numeric_precision_in_Microsoft_Excel In the
// top figure the fraction 1/9000 in Excel is displayed. Although this number
// has a decimal representation that is an infinite string of ones, Excel
// displays only the leading 15 figures. In the second line, the number one
// is added to the fraction, and again Excel displays only 15 figures.
numericPrecision = 1000000000000000
maxFinancialIterations = 128
financialPercision = 1.0e-08
// Date and time format regular expressions
monthRe = `((jan|january)|(feb|february)|(mar|march)|(apr|april)|(may)|(jun|june)|(jul|july)|(aug|august)|(sep|september)|(oct|october)|(nov|november)|(dec|december))`
df1 = `(([0-9])+)/(([0-9])+)/(([0-9])+)`
df2 = monthRe + ` (([0-9])+), (([0-9])+)`
df3 = `(([0-9])+)-(([0-9])+)-(([0-9])+)`
df4 = `(([0-9])+)-` + monthRe + `-(([0-9])+)`
datePrefix = `^((` + df1 + `|` + df2 + `|` + df3 + `|` + df4 + `) )?`
tfhh = `(([0-9])+) (am|pm)`
tfhhmm = `(([0-9])+):(([0-9])+)( (am|pm))?`
tfmmss = `(([0-9])+):(([0-9])+\.([0-9])+)( (am|pm))?`
tfhhmmss = `(([0-9])+):(([0-9])+):(([0-9])+(\.([0-9])+)?)( (am|pm))?`
timeSuffix = `( (` + tfhh + `|` + tfhhmm + `|` + tfmmss + `|` + tfhhmmss + `))?$`
)
// Numeric precision correct numeric values as legacy Excel application
// https://en.wikipedia.org/wiki/Numeric_precision_in_Microsoft_Excel In the
// top figure the fraction 1/9000 in Excel is displayed. Although this number
// has a decimal representation that is an infinite string of ones, Excel
// displays only the leading 15 figures. In the second line, the number one
// is added to the fraction, and again Excel displays only 15 figures.
const numericPrecision = 1000000000000000
const maxFinancialIterations = 128
const financialPercision = 1.0e-08
var (
// tokenPriority defined basic arithmetic operator priority.
tokenPriority = map[string]int{
"^": 5,
"*": 4,
"/": 4,
"+": 3,
"-": 3,
"=": 2,
"<>": 2,
"<": 2,
"<=": 2,
">": 2,
">=": 2,
"&": 1,
}
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,
}
dateFormats = map[string]*regexp.Regexp{
"mm/dd/yy": regexp.MustCompile(`^` + df1 + timeSuffix),
"mm dd, yy": regexp.MustCompile(`^` + df2 + timeSuffix),
"yy-mm-dd": regexp.MustCompile(`^` + df3 + timeSuffix),
"yy-mmStr-dd": regexp.MustCompile(`^` + df4 + timeSuffix),
}
timeFormats = map[string]*regexp.Regexp{
"hh": regexp.MustCompile(datePrefix + tfhh + `$`),
"hh:mm": regexp.MustCompile(datePrefix + tfhhmm + `$`),
"mm:ss": regexp.MustCompile(datePrefix + tfmmss + `$`),
"hh:mm:ss": regexp.MustCompile(datePrefix + tfhhmmss + `$`),
}
dateOnlyFormats = []*regexp.Regexp{
regexp.MustCompile(`^` + df1 + `$`),
regexp.MustCompile(`^` + df2 + `$`),
regexp.MustCompile(`^` + df3 + `$`),
regexp.MustCompile(`^` + df4 + `$`),
}
)
// cellRef defines the structure of a cell reference.
type cellRef struct {
@ -71,19 +153,6 @@ type cellRange struct {
To cellRef
}
// formula criteria condition enumeration.
const (
_ byte = iota
criteriaEq
criteriaLe
criteriaGe
criteriaL
criteriaG
criteriaBeg
criteriaEnd
criteriaErr
)
// formulaCriteria defined formula criteria parser result.
type formulaCriteria struct {
Type byte
@ -193,22 +262,6 @@ type formulaFuncs struct {
sheet, cell string
}
// tokenPriority defined basic arithmetic operator priority.
var tokenPriority = map[string]int{
"^": 5,
"*": 4,
"/": 4,
"+": 3,
"-": 3,
"=": 2,
"<>": 2,
"<": 2,
"<=": 2,
">": 2,
">=": 2,
"&": 1,
}
// CalcCellValue provides a function to get calculated cell value. This
// feature is currently in working processing. Array formula, table formula
// and some other formulas are not supported currently.
@ -269,6 +322,7 @@ var tokenPriority = map[string]int{
// CUMPRINC
// DATE
// DATEDIF
// DAY
// DB
// DDB
// DEC2BIN
@ -6108,6 +6162,257 @@ func (fn *formulaFuncs) DATEDIF(argsList *list.List) formulaArg {
return newNumberFormulaArg(diff)
}
// isDateOnlyFmt check if the given string matches date-only format regular expressions.
func isDateOnlyFmt(dateString string) bool {
for _, df := range dateOnlyFormats {
submatch := df.FindStringSubmatch(dateString)
if len(submatch) > 1 {
return true
}
}
return false
}
// strToTimePatternHandler1 parse and convert the given string in pattern
// hh to the time.
func strToTimePatternHandler1(submatch []string) (h, m int, s float64, err error) {
h, err = strconv.Atoi(submatch[0])
return
}
// strToTimePatternHandler2 parse and convert the given string in pattern
// hh:mm to the time.
func strToTimePatternHandler2(submatch []string) (h, m int, s float64, err error) {
if h, err = strconv.Atoi(submatch[0]); err != nil {
return
}
m, err = strconv.Atoi(submatch[2])
return
}
// strToTimePatternHandler3 parse and convert the given string in pattern
// mm:ss to the time.
func strToTimePatternHandler3(submatch []string) (h, m int, s float64, err error) {
if m, err = strconv.Atoi(submatch[0]); err != nil {
return
}
s, err = strconv.ParseFloat(submatch[2], 64)
return
}
// strToTimePatternHandler4 parse and convert the given string in pattern
// hh:mm:ss to the time.
func strToTimePatternHandler4(submatch []string) (h, m int, s float64, err error) {
if h, err = strconv.Atoi(submatch[0]); err != nil {
return
}
if m, err = strconv.Atoi(submatch[2]); err != nil {
return
}
s, err = strconv.ParseFloat(submatch[4], 64)
return
}
// strToTime parse and convert the given string to the time.
func strToTime(str string) (int, int, float64, bool, bool, formulaArg) {
pattern, submatch := "", []string{}
for key, tf := range timeFormats {
submatch = tf.FindStringSubmatch(str)
if len(submatch) > 1 {
pattern = key
break
}
}
if pattern == "" {
return 0, 0, 0, false, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
}
dateIsEmpty := submatch[1] == ""
submatch = submatch[49:]
var (
l = len(submatch)
last = submatch[l-1]
am = last == "am"
pm = last == "pm"
hours, minutes int
seconds float64
err error
)
if handler, ok := map[string]func(subsubmatch []string) (int, int, float64, error){
"hh": strToTimePatternHandler1,
"hh:mm": strToTimePatternHandler2,
"mm:ss": strToTimePatternHandler3,
"hh:mm:ss": strToTimePatternHandler4,
}[pattern]; ok {
if hours, minutes, seconds, err = handler(submatch); err != nil {
return 0, 0, 0, false, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
}
}
if minutes >= 60 {
return 0, 0, 0, false, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
}
if am || pm {
if hours > 12 || seconds >= 60 {
return 0, 0, 0, false, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
} else if hours == 12 {
hours = 0
}
} else if hours >= 24 || seconds >= 10000 {
return 0, 0, 0, false, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
}
return hours, minutes, seconds, pm, dateIsEmpty, newEmptyFormulaArg()
}
// strToDatePatternHandler1 parse and convert the given string in pattern
// mm/dd/yy to the date.
func strToDatePatternHandler1(submatch []string) (int, int, int, bool, error) {
var year, month, day int
var err error
if month, err = strconv.Atoi(submatch[1]); err != nil {
return 0, 0, 0, false, err
}
if day, err = strconv.Atoi(submatch[3]); err != nil {
return 0, 0, 0, false, err
}
if year, err = strconv.Atoi(submatch[5]); err != nil {
return 0, 0, 0, false, err
}
if year < 0 || year > 9999 || (year > 99 && year < 1900) {
return 0, 0, 0, false, ErrParameterInvalid
}
return formatYear(year), month, day, submatch[8] == "", err
}
// strToDatePatternHandler2 parse and convert the given string in pattern mm
// dd, yy to the date.
func strToDatePatternHandler2(submatch []string) (int, int, int, bool, error) {
var year, month, day int
var err error
month = month2num[submatch[1]]
if day, err = strconv.Atoi(submatch[14]); err != nil {
return 0, 0, 0, false, err
}
if year, err = strconv.Atoi(submatch[16]); err != nil {
return 0, 0, 0, false, err
}
if year < 0 || year > 9999 || (year > 99 && year < 1900) {
return 0, 0, 0, false, ErrParameterInvalid
}
return formatYear(year), month, day, submatch[19] == "", err
}
// strToDatePatternHandler3 parse and convert the given string in pattern
// yy-mm-dd to the date.
func strToDatePatternHandler3(submatch []string) (int, int, int, bool, error) {
var year, month, day int
v1, err := strconv.Atoi(submatch[1])
if err != nil {
return 0, 0, 0, false, err
}
v2, err := strconv.Atoi(submatch[3])
if err != nil {
return 0, 0, 0, false, err
}
v3, err := strconv.Atoi(submatch[5])
if err != nil {
return 0, 0, 0, false, err
}
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, ErrParameterInvalid
}
return year, month, day, submatch[8] == "", err
}
// strToDatePatternHandler4 parse and convert the given string in pattern
// yy-mmStr-dd, yy to the date.
func strToDatePatternHandler4(submatch []string) (int, int, int, bool, error) {
var year, month, day int
var err error
if year, err = strconv.Atoi(submatch[16]); err != nil {
return 0, 0, 0, false, err
}
month = month2num[submatch[3]]
if day, err = strconv.Atoi(submatch[1]); err != nil {
return 0, 0, 0, false, err
}
return formatYear(year), month, day, submatch[19] == "", err
}
// strToDate parse and convert the given string to the date.
func strToDate(str string) (int, int, int, bool, formulaArg) {
pattern, submatch := "", []string{}
for key, df := range dateFormats {
submatch = df.FindStringSubmatch(str)
if len(submatch) > 1 {
pattern = key
break
}
}
if pattern == "" {
return 0, 0, 0, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
}
var (
timeIsEmpty bool
year, month, day int
err error
)
if handler, ok := map[string]func(subsubmatch []string) (int, int, int, bool, error){
"mm/dd/yy": strToDatePatternHandler1,
"mm dd, yy": strToDatePatternHandler2,
"yy-mm-dd": strToDatePatternHandler3,
"yy-mmStr-dd": strToDatePatternHandler4,
}[pattern]; ok {
if year, month, day, timeIsEmpty, err = handler(submatch); err != nil {
return 0, 0, 0, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
}
}
if !validateDate(year, month, day) {
return 0, 0, 0, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)
}
return year, month, day, timeIsEmpty, newEmptyFormulaArg()
}
// DAY function returns the day of a date, represented by a serial number. The
// day is given as an integer ranging from 1 to 31. The syntax of the
// function is:
//
// DAY(serial_number)
//
func (fn *formulaFuncs) DAY(argsList *list.List) formulaArg {
if argsList.Len() != 1 {
return newErrorFormulaArg(formulaErrorVALUE, "DAY requires exactly 1 argument")
}
arg := argsList.Front().Value.(formulaArg)
num := arg.ToNumber()
if num.Type != ArgNumber {
dateString := strings.ToLower(arg.Value())
if !isDateOnlyFmt(dateString) {
if _, _, _, _, _, err := strToTime(dateString); err.Type == ArgError {
return err
}
}
_, _, day, _, err := strToDate(dateString)
if err.Type == ArgError {
return err
}
return newNumberFormulaArg(float64(day))
}
if num.Number < 0 {
return newErrorFormulaArg(formulaErrorNUM, "DAY only accepts positive argument")
}
if num.Number <= 60 {
return newNumberFormulaArg(math.Mod(num.Number, 31.0))
}
return newNumberFormulaArg(float64(timeFromExcelTime(num.Number, false).Day()))
}
// NOW function returns the current date and time. The function receives no
// arguments and therefore. The syntax of the function is:
//

View File

@ -944,6 +944,24 @@ func TestCalcCellValue(t *testing.T) {
"=DATEDIF(43101,43891,\"YD\")": "59",
"=DATEDIF(36526,73110,\"YD\")": "60",
"=DATEDIF(42171,44242,\"yd\")": "244",
// DAY
"=DAY(0)": "0",
"=DAY(INT(7))": "7",
"=DAY(\"35\")": "4",
"=DAY(42171)": "16",
"=DAY(\"2-28-1900\")": "28",
"=DAY(\"31-May-2015\")": "31",
"=DAY(\"01/03/2019 12:14:16\")": "3",
"=DAY(\"January 25, 2020 01 AM\")": "25",
"=DAY(\"January 25, 2020 01:03 AM\")": "25",
"=DAY(\"January 25, 2020 12:00:00 AM\")": "25",
"=DAY(\"1900-1-1\")": "1",
"=DAY(\"12-1-1900\")": "1",
"=DAY(\"3-January-1900\")": "3",
"=DAY(\"3-February-2000\")": "3",
"=DAY(\"3-February-2008\")": "3",
"=DAY(\"01/25/20\")": "25",
"=DAY(\"01/25/31\")": "25",
// Text Functions
// CHAR
"=CHAR(65)": "A",
@ -1927,6 +1945,40 @@ func TestCalcCellValue(t *testing.T) {
"=DATEDIF(\"\",\"\",\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax",
"=DATEDIF(43891,43101,\"Y\")": "start_date > end_date",
"=DATEDIF(43101,43891,\"x\")": "DATEDIF has invalid unit",
// DAY
"=DAY()": "DAY requires exactly 1 argument",
"=DAY(-1)": "DAY only accepts positive argument",
"=DAY(0,0)": "DAY requires exactly 1 argument",
"=DAY(\"text\")": "#VALUE!",
"=DAY(\"January 25, 2020 9223372036854775808 AM\")": "#VALUE!",
"=DAY(\"January 25, 2020 9223372036854775808:00 AM\")": "#VALUE!",
"=DAY(\"January 25, 2020 00:9223372036854775808 AM\")": "#VALUE!",
"=DAY(\"January 25, 2020 9223372036854775808:00.0 AM\")": "#VALUE!",
"=DAY(\"January 25, 2020 0:1" + strings.Repeat("0", 309) + ".0 AM\")": "#VALUE!",
"=DAY(\"January 25, 2020 9223372036854775808:00:00 AM\")": "#VALUE!",
"=DAY(\"January 25, 2020 0:9223372036854775808:0 AM\")": "#VALUE!",
"=DAY(\"January 25, 2020 0:0:1" + strings.Repeat("0", 309) + " AM\")": "#VALUE!",
"=DAY(\"January 25, 2020 0:61:0 AM\")": "#VALUE!",
"=DAY(\"January 25, 2020 0:00:60 AM\")": "#VALUE!",
"=DAY(\"January 25, 2020 24:00:00\")": "#VALUE!",
"=DAY(\"January 25, 2020 00:00:10001\")": "#VALUE!",
"=DAY(\"9223372036854775808/25/2020\")": "#VALUE!",
"=DAY(\"01/9223372036854775808/2020\")": "#VALUE!",
"=DAY(\"01/25/9223372036854775808\")": "#VALUE!",
"=DAY(\"01/25/10000\")": "#VALUE!",
"=DAY(\"01/25/100\")": "#VALUE!",
"=DAY(\"January 9223372036854775808, 2020\")": "#VALUE!",
"=DAY(\"January 25, 9223372036854775808\")": "#VALUE!",
"=DAY(\"January 25, 10000\")": "#VALUE!",
"=DAY(\"January 25, 100\")": "#VALUE!",
"=DAY(\"9223372036854775808-25-2020\")": "#VALUE!",
"=DAY(\"01-9223372036854775808-2020\")": "#VALUE!",
"=DAY(\"01-25-9223372036854775808\")": "#VALUE!",
"=DAY(\"1900-0-0\")": "#VALUE!",
"=DAY(\"14-25-1900\")": "#VALUE!",
"=DAY(\"3-January-9223372036854775808\")": "#VALUE!",
"=DAY(\"9223372036854775808-January-1900\")": "#VALUE!",
"=DAY(\"0-January-1900\")": "#VALUE!",
// NOW
"=NOW(A1)": "NOW accepts no arguments",
// TODAY
@ -2614,3 +2666,8 @@ func TestCalcMIRR(t *testing.T) {
assert.Equal(t, "", result, formula)
}
}
func TestStrToDate(t *testing.T) {
_, _, _, _, err := strToDate("")
assert.Equal(t, formulaErrorVALUE, err.Error)
}

45
date.go
View File

@ -23,6 +23,7 @@ const (
)
var (
daysInMonth = []int{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
excel1900Epoc = time.Date(1899, time.December, 30, 0, 0, 0, 0, time.UTC)
excel1904Epoc = time.Date(1904, time.January, 1, 0, 0, 0, 0, time.UTC)
excelMinTime1900 = time.Date(1899, time.December, 31, 0, 0, 0, 0, time.UTC)
@ -167,3 +168,47 @@ func ExcelDateToTime(excelDate float64, use1904Format bool) (time.Time, error) {
}
return timeFromExcelTime(excelDate, use1904Format), nil
}
// isLeapYear determine if leap year for a given year.
func isLeapYear(y int) bool {
if y == y/400*400 {
return true
}
if y == y/100*100 {
return false
}
return y == y/4*4
}
// getDaysInMonth provides a function to get the days by a given year and
// month number.
func getDaysInMonth(y, m int) int {
if m == 2 && isLeapYear(y) {
return 29
}
return daysInMonth[m-1]
}
// validateDate provides a function to validate if a valid date by a given
// year, month, and day number.
func validateDate(y, m, d int) bool {
if m < 1 || m > 12 {
return false
}
if d < 1 {
return false
}
return d <= getDaysInMonth(y, m)
}
// formatYear converts the given year number into a 4-digit format.
func formatYear(y int) int {
if y < 1900 {
if y < 30 {
y += 2000
} else {
y += 1900
}
}
return y
}