diff --git a/internal/entity/folder.go b/internal/entity/folder.go index 5c477e49c..08ac8e52e 100644 --- a/internal/entity/folder.go +++ b/internal/entity/folder.go @@ -114,7 +114,7 @@ func (m *Folder) SetValuesFromPath() { } if len(m.Path) >= 6 { - if date := txt.Time(m.Path); !date.IsZero() { + if date := txt.DateFromFilePath(m.Path); !date.IsZero() { if txt.IsUInt(s) || txt.IsTime(s) { if date.Day() > 1 { m.FolderTitle = date.Format("January 2, 2006") diff --git a/internal/meta/exif.go b/internal/meta/exif.go index bf29d1dbc..0121b4af7 100644 --- a/internal/meta/exif.go +++ b/internal/meta/exif.go @@ -11,18 +11,20 @@ import ( "time" "github.com/dsoprea/go-exif/v3" + "gopkg.in/photoprism/go-tz.v2/tz" + exifcommon "github.com/dsoprea/go-exif/v3/common" + "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/sanitize" - "gopkg.in/photoprism/go-tz.v2/tz" + "github.com/photoprism/photoprism/pkg/txt" ) var exifIfdMapping *exifcommon.IfdMapping var exifTagIndex = exif.NewTagIndex() var exifMutex = sync.Mutex{} - -const DateTimeZero = "0000:00:00 00:00:00" +var exifDateFields = []string{"DateTimeOriginal", "DateTimeDigitized", "CreateDate", "DateTime"} func init() { exifIfdMapping = exifcommon.NewIfdMapping() @@ -32,11 +34,6 @@ func init() { } } -// ValidDateTime returns true if a date string looks valid and is not zero. -func ValidDateTime(s string) bool { - return len(s) == len(DateTimeZero) && s != DateTimeZero -} - // Exif parses an image file for Exif meta data and returns as Data struct. func Exif(fileName string, fileType fs.FileFormat) (data Data, err error) { err = data.Exif(fileName, fileType) @@ -236,36 +233,24 @@ func (data *Data) Exif(fileName string, fileType fs.FileFormat) (err error) { } } - var takenAt string + takenAt := time.Time{} - if value, ok := tags["DateTimeOriginal"]; ok && ValidDateTime(value) { - takenAt = value - } else if value, ok := tags["DateTimeDigitized"]; ok && ValidDateTime(value) { - takenAt = value - } else if value, ok := tags["CreateDate"]; ok && ValidDateTime(value) { - takenAt = value - } else if value, ok := tags["DateTime"]; ok && ValidDateTime(value) { - takenAt = value + for _, name := range exifDateFields { + if dateTime := txt.DateTime(tags[name], data.TimeZone); !dateTime.IsZero() { + takenAt = dateTime + break + } } - if ValidDateTime(takenAt) { - takenAt = strings.ReplaceAll(takenAt, "/", ":") - takenAt = strings.ReplaceAll(takenAt, "-", ":") - - if taken, err := time.Parse("2006:01:02 15:04:05", takenAt); err == nil { - data.TakenAtLocal = taken.Round(time.Second) - data.TakenAt = data.TakenAtLocal - - if loc, err := time.LoadLocation(data.TimeZone); err != nil { - log.Warnf("metadata: unknown time zone %s in %s (exif)", data.TimeZone, logName) - } else if tl, err := time.ParseInLocation("2006:01:02 15:04:05", takenAt, loc); err == nil { - data.TakenAt = tl.Round(time.Second).UTC() - } else { - log.Errorf("metadata: %s in %s (exif time)", err.Error(), logName) // this should never happen - } + // Valid time found in Exif metadata? + if !takenAt.IsZero() { + if takenAtLocal, err := time.ParseInLocation("2006-01-02T15:04:05", takenAt.Format("2006-01-02T15:04:05"), time.UTC); err == nil { + data.TakenAtLocal = takenAtLocal } else { - log.Warnf("metadata: invalid time %s in %s (exif)", takenAt, logName) + data.TakenAtLocal = takenAt } + + data.TakenAt = takenAt.UTC() } if value, ok := tags["Flash"]; ok { diff --git a/internal/meta/json_exiftool.go b/internal/meta/json_exiftool.go index efe1df674..266e4fae4 100644 --- a/internal/meta/json_exiftool.go +++ b/internal/meta/json_exiftool.go @@ -79,13 +79,8 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) { continue } - s := strings.TrimSpace(jsonValue.String()) - s = strings.ReplaceAll(s, "/", ":") - - if tv, err := time.Parse("2006:01:02 15:04:05", strings.ReplaceAll(s, "-", ":")); err == nil { - fieldValue.Set(reflect.ValueOf(tv.Round(time.Second).UTC())) - } else if tv, err := time.Parse("2006:01:02 15:04:05Z07:00", s); err == nil { - fieldValue.Set(reflect.ValueOf(tv.Round(time.Second))) + if dateTime := txt.DateTime(jsonValue.String(), ""); !dateTime.IsZero() { + fieldValue.Set(reflect.ValueOf(dateTime)) } case time.Duration: if !fieldValue.IsZero() { diff --git a/internal/photoprism/index_mediafile.go b/internal/photoprism/index_mediafile.go index 240979709..361cf442c 100644 --- a/internal/photoprism/index_mediafile.go +++ b/internal/photoprism/index_mediafile.go @@ -562,7 +562,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID // Set taken date based on file mod time or name if other metadata is missing. if m.IsMedia() && entity.SrcPriority[photo.TakenSrc] <= entity.SrcPriority[entity.SrcName] { // Try to extract time from original file name first. - if taken := txt.Time(photo.OriginalName); !taken.IsZero() { + if taken := txt.DateFromFilePath(photo.OriginalName); !taken.IsZero() { photo.SetTakenAt(taken, taken, "", entity.SrcName) } else if taken, takenSrc := m.TakenAt(); takenSrc == entity.SrcName { photo.SetTakenAt(taken, taken, "", entity.SrcName) diff --git a/internal/photoprism/mediafile.go b/internal/photoprism/mediafile.go index facb9365c..d42741dc0 100644 --- a/internal/photoprism/mediafile.go +++ b/internal/photoprism/mediafile.go @@ -128,7 +128,7 @@ func (m *MediaFile) TakenAt() (time.Time, string) { return m.takenAt, m.takenAtSrc } - if nameTime := txt.Time(m.fileName); !nameTime.IsZero() { + if nameTime := txt.DateFromFilePath(m.fileName); !nameTime.IsZero() { m.takenAt = nameTime m.takenAtSrc = entity.SrcName diff --git a/pkg/txt/time.go b/pkg/txt/datetime.go similarity index 54% rename from pkg/txt/time.go rename to pkg/txt/datetime.go index 4336649db..0b84e1c6a 100644 --- a/pkg/txt/time.go +++ b/pkg/txt/datetime.go @@ -1,11 +1,14 @@ package txt import ( + "fmt" "regexp" "strings" "time" ) +// Go regular expression tester: https://regoio.herokuapp.com/ + var DateRegexp = regexp.MustCompile("\\D\\d{4}[\\-_]\\d{2}[\\-_]\\d{2,}") var DatePathRegexp = regexp.MustCompile("\\D\\d{4}/\\d{1,2}/?\\d*") var DateTimeRegexp = regexp.MustCompile("\\D\\d{4}[\\-_]\\d{2}[\\-_]\\d{2}.{1,4}\\d{2}\\D\\d{2}\\D\\d{2,}") @@ -13,6 +16,17 @@ var DateIntRegexp = regexp.MustCompile("\\d{1,4}") var YearRegexp = regexp.MustCompile("\\d{4,5}") var IsDateRegexp = regexp.MustCompile("\\d{4}[\\-_]?\\d{2}[\\-_]?\\d{2}") var IsDateTimeRegexp = regexp.MustCompile("\\d{4}[\\-_]?\\d{2}[\\-_]?\\d{2}.{1,4}\\d{2}\\D?\\d{2}\\D?\\d{2}") +var ExifDateTimeRegexp = regexp.MustCompile("((?P\\d{4})|\\D{4})\\D((?P\\d{2})|\\D{2})\\D((?P\\d{2})|\\D{2})\\D((?P\\d{2})|\\D{2})\\D((?P\\d{2})|\\D{2})\\D((?P\\d{2})|\\D{2})(?P\\D)?(?P\\d{2})?\\D?(?P\\d{2})?") +var ExifDateTimeMatch = make(map[string]int) + +func init() { + names := ExifDateTimeRegexp.SubexpNames() + for i := 0; i < len(names); i++ { + if name := names[i]; name != "" { + ExifDateTimeMatch[name] = i + } + } +} var ( YearMin = 1990 @@ -32,8 +46,99 @@ const ( SecMax = 59 ) -// Time returns a string as time or the zero time instant in case it can not be converted. -func Time(s string) (result time.Time) { +// DateTime parses a string and returns a valid Exif timestamp if possible. +func DateTime(s, timeZone string) (t time.Time) { + defer func() { + if r := recover(); r != nil { + // Panic? Return unknown time. + t = time.Time{} + } + }() + + // Empty timestamp? Return unknown time. + if s == "" { + return time.Time{} + } + + s = strings.TrimLeft(s, " ") + + // Timestamp too short or much too long? Return unknown time. + if len(s) < 4 || len(s) > 50 { + return time.Time{} + } + + // Pad short timestamp with whitespace at the end. + s = fmt.Sprintf("%-19s", s) + + // Shorten timestamp to max length. + if len(s) > 25 { + s = s[:25] + } + + v := ExifDateTimeMatch + m := ExifDateTimeRegexp.FindStringSubmatch(s) + + // Pattern doesn't match? Return unknown time. + if len(m) == 0 { + return time.Time{} + } + + // Default to UTC. + tz := time.UTC + + // Local time zone currently not supported (undefined). + if timeZone == time.Local.String() { + timeZone = "" + } + + // Set time zone. + loc, err := time.LoadLocation(timeZone) + + // Location found? + if err == nil && timeZone != "" && tz != time.Local { + tz = loc + timeZone = tz.String() + } else { + timeZone = "" + } + + // Does the timestamp contain a time zone offset? + z := m[v["z"]] // Supported values, if not empty: Z, +, - + zh := IntVal(m[v["zh"]], 0, 23, 0) // Hours. + zm := IntVal(m[v["zm"]], 0, 59, 0) // Minutes. + + // Valid time zone offset found? + if offset := (zh*60 + zm) * 60; offset > 0 && offset <= 86400 { + // Offset timezone name example: UTC+03:30 + if z == "+" { + // Positive offset relative to UTC. + tz = time.FixedZone(fmt.Sprintf("UTC+%02d:%02d", zh, zm), offset) + } else if z == "-" { + // Negative offset relative to UTC. + tz = time.FixedZone(fmt.Sprintf("UTC-%02d:%02d", zh, zm), -1*offset) + } + } + + // Create rounded timestamp from parsed input values. + t = time.Date( + IntVal(m[v["year"]], 1, YearMax, time.Now().Year()), + time.Month(IntVal(m[v["month"]], 1, 12, 1)), + IntVal(m[v["day"]], 1, 31, 1), + IntVal(m[v["h"]], 0, 23, 0), + IntVal(m[v["m"]], 0, 59, 0), + IntVal(m[v["s"]], 0, 59, 0), + 0, + tz).Round(time.Second) + + if timeZone != "" && loc != nil && loc != tz { + return t.In(loc) + } + + return t +} + +// DateFromFilePath returns a string as time or the zero time instant in case it can not be converted. +func DateFromFilePath(s string) (result time.Time) { defer func() { if r := recover(); r != nil { result = time.Time{} diff --git a/pkg/txt/time_test.go b/pkg/txt/datetime_test.go similarity index 56% rename from pkg/txt/time_test.go rename to pkg/txt/datetime_test.go index 660707a8a..b4fe48bbe 100644 --- a/pkg/txt/time_test.go +++ b/pkg/txt/datetime_test.go @@ -6,239 +6,334 @@ import ( "github.com/stretchr/testify/assert" ) -func TestTime(t *testing.T) { +func TestDateTime(t *testing.T) { + t.Run("2016: : : : ", func(t *testing.T) { + result := DateTime("2016: : : : ", "") + assert.Equal(t, "2016-01-01 00:00:00 +0000 UTC", result.String()) + }) + t.Run("2016: :__ : : ", func(t *testing.T) { + result := DateTime("2016: :__ : : ", "") + assert.Equal(t, "2016-01-01 00:00:00 +0000 UTC", result.String()) + }) + t.Run("2016:06:28 : :??", func(t *testing.T) { + result := DateTime("2016:06:28 : :??", "") + assert.Equal(t, "2016-06-28 00:00:00 +0000 UTC", result.String()) + }) + t.Run("2016:06:28 09:45:49", func(t *testing.T) { + result := DateTime("2016:06:28 09:45:49", "") + assert.Equal(t, "2016-06-28 09:45:49 +0000 UTC", result.String()) + }) + t.Run("2016:06:28 09:45:49+10:00", func(t *testing.T) { + result := DateTime("2016:06:28 09:45:49+10:00", "") + assert.Equal(t, "2016-06-28 09:45:49 +1000 UTC+10:00", result.String()) + }) + t.Run("2016:06:28 : :", func(t *testing.T) { + result := DateTime("2016:06:28 : :", "") + assert.Equal(t, "2016-06-28 00:00:00 +0000 UTC", result.String()) + }) + t.Run("2016/06/28T09-45:49", func(t *testing.T) { + result := DateTime("2016/06/28T09-45:49", "") + assert.Equal(t, "2016-06-28 09:45:49 +0000 UTC", result.String()) + }) + t.Run("2016:06:28T09:45:49Z", func(t *testing.T) { + result := DateTime("2016:06:28T09:45:49Z", "") + assert.Equal(t, "2016-06-28 09:45:49 +0000 UTC", result.String()) + }) + t.Run("2016:06:28T09:45: Z", func(t *testing.T) { + result := DateTime("2016:06:28T09:45: Z", "") + assert.Equal(t, "2016-06-28 09:45:00 +0000 UTC", result.String()) + }) + t.Run("2016:06:28T09:45: ", func(t *testing.T) { + result := DateTime("2016:06:28T09:45: ", "") + assert.Equal(t, "2016-06-28 09:45:00 +0000 UTC", result.String()) + }) + t.Run("2016:06:28T09:45: ZABC", func(t *testing.T) { + result := DateTime("2016:06:28T09:45: ZABC", "") + assert.Equal(t, "2016-06-28 09:45:00 +0000 UTC", result.String()) + }) + t.Run("2016:06:28T09:45: ABC", func(t *testing.T) { + result := DateTime("2016:06:28T09:45: ABC", "") + assert.Equal(t, "2016-06-28 09:45:00 +0000 UTC", result.String()) + }) + t.Run("2016:06:28 09:45:49+10:00ABC", func(t *testing.T) { + result := DateTime("2016:06:28 09:45:49+10:00ABC", "") + assert.Equal(t, "2016-06-28 09:45:49 +1000 UTC+10:00", result.String()) + }) + t.Run(" 2016:06:28 09:45:49-01:30ABC", func(t *testing.T) { + result := DateTime(" 2016:06:28 09:45:49-01:30ABC", "") + assert.Equal(t, "2016-06-28 09:45:49 -0130 UTC-01:30", result.String()) + }) + t.Run("2016:06:28 09:45:49-0130", func(t *testing.T) { + result := DateTime("2016:06:28 09:45:49-0130", "") + assert.Equal(t, "2016-06-28 09:45:49 -0130 UTC-01:30", result.String()) + }) + t.Run("UTC/016:06:28 09:45:49-0130", func(t *testing.T) { + result := DateTime("2016:06:28 09:45:49-0130", "UTC") + assert.Equal(t, "2016-06-28 11:15:49 +0000 UTC", result.String()) + }) + t.Run("UTC/016:06:28 09:45:49-0130", func(t *testing.T) { + result := DateTime("2016:06:28 09:45:49.0130", "UTC") + assert.Equal(t, "2016-06-28 09:45:49 +0000 UTC", result.String()) + }) + t.Run("2012:08:08 22:07:18", func(t *testing.T) { + result := DateTime("2012:08:08 22:07:18", "") + assert.Equal(t, "2012-08-08 22:07:18 +0000 UTC", result.String()) + }) + t.Run("2020-01-30_09-57-18", func(t *testing.T) { + result := DateTime("2020-01-30_09-57-18", "") + assert.Equal(t, "2020-01-30 09:57:18 +0000 UTC", result.String()) + }) + t.Run("EuropeBerlin/2016:06:28 09:45:49+10:00ABC", func(t *testing.T) { + result := DateTime("2016:06:28 09:45:49+10:00ABC", "Europe/Berlin") + assert.Equal(t, "2016-06-28 01:45:49 +0200 CEST", result.String()) + }) + t.Run("EuropeBerlin/ 2016:06:28 09:45:49-01:30ABC", func(t *testing.T) { + result := DateTime(" 2016:06:28 09:45:49-01:30ABC", "Europe/Berlin") + assert.Equal(t, "2016-06-28 13:15:49 +0200 CEST", result.String()) + }) + t.Run("EuropeBerlin/2012:08:08 22:07:18", func(t *testing.T) { + result := DateTime("2012:08:08 22:07:18", "Europe/Berlin") + assert.Equal(t, "2012-08-08 22:07:18 +0200 CEST", result.String()) + }) + t.Run("EuropeBerlin/2020-01-30_09-57-18", func(t *testing.T) { + result := DateTime("2020-01-30_09-57-18", "Europe/Berlin") + assert.Equal(t, "2020-01-30 09:57:18 +0100 CET", result.String()) + }) +} + +func TestDateFromFilePath(t *testing.T) { t.Run("2016/08/18 iPhone/WRNI2074.jpg", func(t *testing.T) { - result := Time("2016/08/18 iPhone/WRNI2074.jpg") + result := DateFromFilePath("2016/08/18 iPhone/WRNI2074.jpg") assert.False(t, result.IsZero()) assert.Equal(t, "2016-08-18 00:00:00 +0000 UTC", result.String()) }) t.Run("2016/08/18 iPhone/OZBJ8443.jpg", func(t *testing.T) { - result := Time("2016/08/18 iPhone/OZBJ8443.jpg") + result := DateFromFilePath("2016/08/18 iPhone/OZBJ8443.jpg") assert.False(t, result.IsZero()) assert.Equal(t, "2016-08-18 00:00:00 +0000 UTC", result.String()) }) t.Run("2018/04 - April/2018-04-12 19:24:49.gif", func(t *testing.T) { - result := Time("2018/04 - April/2018-04-12 19:24:49.gif") + result := DateFromFilePath("2018/04 - April/2018-04-12 19:24:49.gif") assert.False(t, result.IsZero()) assert.Equal(t, "2018-04-12 19:24:49 +0000 UTC", result.String()) }) t.Run("2018", func(t *testing.T) { - result := Time("2018") + result := DateFromFilePath("2018") assert.True(t, result.IsZero()) }) t.Run("2018-04-12 19/24/49.gif", func(t *testing.T) { - result := Time("2018-04-12 19/24/49.gif") + result := DateFromFilePath("2018-04-12 19/24/49.gif") assert.False(t, result.IsZero()) assert.Equal(t, "2018-04-12 19:24:49 +0000 UTC", result.String()) }) t.Run("/2020/1212/20130518_142022_3D657EBD.jpg", func(t *testing.T) { - result := Time("/2020/1212/20130518_142022_3D657EBD.jpg") + result := DateFromFilePath("/2020/1212/20130518_142022_3D657EBD.jpg") //assert.False(t, result.IsZero()) assert.True(t, result.IsZero()) }) t.Run("20130518_142022_3D657EBD.jpg", func(t *testing.T) { - result := Time("20130518_142022_3D657EBD.jpg") + result := DateFromFilePath("20130518_142022_3D657EBD.jpg") //assert.False(t, result.IsZero()) assert.True(t, result.IsZero()) }) t.Run("telegram_2020_01_30_09_57_18.jpg", func(t *testing.T) { - result := Time("telegram_2020_01_30_09_57_18.jpg") + result := DateFromFilePath("telegram_2020_01_30_09_57_18.jpg") assert.False(t, result.IsZero()) assert.Equal(t, "2020-01-30 09:57:18 +0000 UTC", result.String()) }) t.Run("Screenshot 2019_05_21 at 10.45.52.png", func(t *testing.T) { - result := Time("Screenshot 2019_05_21 at 10.45.52.png") + result := DateFromFilePath("Screenshot 2019_05_21 at 10.45.52.png") assert.False(t, result.IsZero()) assert.Equal(t, "2019-05-21 10:45:52 +0000 UTC", result.String()) }) t.Run("telegram_2020-01-30_09-57-18.jpg", func(t *testing.T) { - result := Time("telegram_2020-01-30_09-57-18.jpg") + result := DateFromFilePath("telegram_2020-01-30_09-57-18.jpg") assert.False(t, result.IsZero()) assert.Equal(t, "2020-01-30 09:57:18 +0000 UTC", result.String()) }) t.Run("Screenshot 2019-05-21 at 10.45.52.png", func(t *testing.T) { - result := Time("Screenshot 2019-05-21 at 10.45.52.png") + result := DateFromFilePath("Screenshot 2019-05-21 at 10.45.52.png") assert.False(t, result.IsZero()) assert.Equal(t, "2019-05-21 10:45:52 +0000 UTC", result.String()) }) t.Run("telegram_2020-01-30_09-18.jpg", func(t *testing.T) { - result := Time("telegram_2020-01-30_09-18.jpg") + result := DateFromFilePath("telegram_2020-01-30_09-18.jpg") assert.False(t, result.IsZero()) assert.Equal(t, "2020-01-30 00:00:00 +0000 UTC", result.String()) }) t.Run("Screenshot 2019-05-21 at 10545.52.png", func(t *testing.T) { - result := Time("Screenshot 2019-05-21 at 10545.52.png") + result := DateFromFilePath("Screenshot 2019-05-21 at 10545.52.png") assert.False(t, result.IsZero()) assert.Equal(t, "2019-05-21 00:00:00 +0000 UTC", result.String()) }) t.Run("/2019-05-21/file2314.JPG", func(t *testing.T) { - result := Time("/2019-05-21/file2314.JPG") + result := DateFromFilePath("/2019-05-21/file2314.JPG") assert.False(t, result.IsZero()) assert.Equal(t, "2019-05-21 00:00:00 +0000 UTC", result.String()) }) t.Run("/2019.05.21", func(t *testing.T) { - result := Time("/2019.05.21") + result := DateFromFilePath("/2019.05.21") assert.True(t, result.IsZero()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) }) t.Run("/05.21.2019", func(t *testing.T) { - result := Time("/05.21.2019") + result := DateFromFilePath("/05.21.2019") assert.True(t, result.IsZero()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) }) t.Run("/21.05.2019", func(t *testing.T) { - result := Time("/21.05.2019") + result := DateFromFilePath("/21.05.2019") assert.True(t, result.IsZero()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) }) t.Run("05/21/2019", func(t *testing.T) { - result := Time("05/21/2019") + result := DateFromFilePath("05/21/2019") assert.True(t, result.IsZero()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) }) t.Run("2019-07-23", func(t *testing.T) { - result := Time("2019-07-23") + result := DateFromFilePath("2019-07-23") assert.False(t, result.IsZero()) assert.Equal(t, "2019-07-23 00:00:00 +0000 UTC", result.String()) }) t.Run("Photos/2015-01-14", func(t *testing.T) { - result := Time("Photos/2015-01-14") + result := DateFromFilePath("Photos/2015-01-14") assert.False(t, result.IsZero()) assert.Equal(t, "2015-01-14 00:00:00 +0000 UTC", result.String()) }) t.Run("21/05/2019", func(t *testing.T) { - result := Time("21/05/2019") + result := DateFromFilePath("21/05/2019") assert.True(t, result.IsZero()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) }) t.Run("2019/05/21", func(t *testing.T) { - result := Time("2019/05/21") + result := DateFromFilePath("2019/05/21") assert.False(t, result.IsZero()) assert.Equal(t, "2019-05-21 00:00:00 +0000 UTC", result.String()) }) t.Run("2019/05/2145", func(t *testing.T) { - result := Time("2019/05/2145") + result := DateFromFilePath("2019/05/2145") assert.True(t, result.IsZero()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) }) t.Run("/05/21/2019", func(t *testing.T) { - result := Time("/05/21/2019") + result := DateFromFilePath("/05/21/2019") assert.True(t, result.IsZero()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) }) t.Run("/21/05/2019", func(t *testing.T) { - result := Time("/21/05/2019") + result := DateFromFilePath("/21/05/2019") assert.True(t, result.IsZero()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) }) t.Run("/2019/05/21.jpeg", func(t *testing.T) { - result := Time("/2019/05/21.jpeg") + result := DateFromFilePath("/2019/05/21.jpeg") assert.False(t, result.IsZero()) assert.Equal(t, "2019-05-21 00:00:00 +0000 UTC", result.String()) }) t.Run("/2019/05/21/foo.txt", func(t *testing.T) { - result := Time("/2019/05/21/foo.txt") + result := DateFromFilePath("/2019/05/21/foo.txt") assert.False(t, result.IsZero()) assert.Equal(t, "2019-05-21 00:00:00 +0000 UTC", result.String()) }) t.Run("2019/21/05", func(t *testing.T) { - result := Time("2019/21/05") + result := DateFromFilePath("2019/21/05") assert.True(t, result.IsZero()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) }) t.Run("/2019/05/21/foo.jpg", func(t *testing.T) { - result := Time("/2019/05/21/foo.jpg") + result := DateFromFilePath("/2019/05/21/foo.jpg") assert.False(t, result.IsZero()) assert.Equal(t, "2019-05-21 00:00:00 +0000 UTC", result.String()) }) t.Run("/2019/21/05/foo.jpg", func(t *testing.T) { - result := Time("/2019/21/05/foo.jpg") + result := DateFromFilePath("/2019/21/05/foo.jpg") assert.True(t, result.IsZero()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) }) t.Run("/2019/5/foo.jpg", func(t *testing.T) { - result := Time("/2019/5/foo.jpg") + result := DateFromFilePath("/2019/5/foo.jpg") assert.False(t, result.IsZero()) assert.Equal(t, "2019-05-01 00:00:00 +0000 UTC", result.String()) }) t.Run("/2019/1/3/foo.jpg", func(t *testing.T) { - result := Time("/2019/1/3/foo.jpg") + result := DateFromFilePath("/2019/1/3/foo.jpg") assert.False(t, result.IsZero()) assert.Equal(t, "2019-01-03 00:00:00 +0000 UTC", result.String()) }) t.Run("/1989/1/3/foo.jpg", func(t *testing.T) { - result := Time("/1989/1/3/foo.jpg") + result := DateFromFilePath("/1989/1/3/foo.jpg") assert.True(t, result.IsZero()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) }) t.Run("545452019/1/3/foo.jpg", func(t *testing.T) { - result := Time("/2019/1/3/foo.jpg") + result := DateFromFilePath("/2019/1/3/foo.jpg") assert.False(t, result.IsZero()) assert.Equal(t, "2019-01-03 00:00:00 +0000 UTC", result.String()) }) t.Run("fo.jpg", func(t *testing.T) { - result := Time("fo.jpg") + result := DateFromFilePath("fo.jpg") assert.True(t, result.IsZero()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) }) t.Run("n >6", func(t *testing.T) { - result := Time("2020-01-30_09-87-18-23.jpg") + result := DateFromFilePath("2020-01-30_09-87-18-23.jpg") assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) }) t.Run("year < yearmin", func(t *testing.T) { - result := Time("1020-01-30_09-57-18.jpg") + result := DateFromFilePath("1020-01-30_09-57-18.jpg") assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) }) t.Run("hour > hourmax", func(t *testing.T) { - result := Time("2020-01-30_25-57-18.jpg") + result := DateFromFilePath("2020-01-30_25-57-18.jpg") assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) }) t.Run("invalid days", func(t *testing.T) { - result := Time("2020-01-00.jpg") + result := DateFromFilePath("2020-01-00.jpg") assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) }) t.Run("IMG-20191120-WA0001.jpg", func(t *testing.T) { - result := Time("IMG-20191120-WA0001.jpg") + result := DateFromFilePath("IMG-20191120-WA0001.jpg") assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) }) t.Run("VID-20191120-WA0001.jpg", func(t *testing.T) { - result := Time("VID-20191120-WA0001.jpg") + result := DateFromFilePath("VID-20191120-WA0001.jpg") assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) }) } diff --git a/pkg/txt/int.go b/pkg/txt/int.go index ffd99ae1f..d6e7a05ee 100644 --- a/pkg/txt/int.go +++ b/pkg/txt/int.go @@ -20,6 +20,31 @@ func Int(s string) int { return int(result) } +// IntVal converts a string to a validated integer or a default if invalid. +func IntVal(s string, min, max, d int) (i int) { + s = strings.TrimSpace(s) + + if s == "" { + return d + } + + result, err := strconv.ParseInt(s, 10, 32) + + if err != nil { + return d + } + + i = int(result) + + if i < min { + return d + } else if max != 0 && i > max { + return d + } + + return i +} + // UInt converts a string to an unsigned integer or 0 if invalid. func UInt(s string) uint { if s == "" { diff --git a/pkg/txt/int_test.go b/pkg/txt/int_test.go index b51343a90..b26f4d3f3 100644 --- a/pkg/txt/int_test.go +++ b/pkg/txt/int_test.go @@ -7,31 +7,79 @@ import ( ) func TestInt(t *testing.T) { - t.Run("empty", func(t *testing.T) { + t.Run("Empty", func(t *testing.T) { result := Int("") assert.Equal(t, 0, result) }) - t.Run("non-numeric", func(t *testing.T) { + t.Run("NonNumeric", func(t *testing.T) { result := Int("Screenshot") assert.Equal(t, 0, result) }) - t.Run("zero", func(t *testing.T) { + t.Run("Zero", func(t *testing.T) { result := Int("0") assert.Equal(t, 0, result) }) - t.Run("int", func(t *testing.T) { + t.Run("LeadingZeros", func(t *testing.T) { + result := Int("000123") + assert.Equal(t, 123, result) + }) + + t.Run("WhitespacePadding", func(t *testing.T) { + result := Int(" 123\t ") + assert.Equal(t, 123, result) + }) + + t.Run("PositiveInt", func(t *testing.T) { result := Int("123") assert.Equal(t, 123, result) }) - t.Run("negative int", func(t *testing.T) { + t.Run("NegativeInt", func(t *testing.T) { result := Int("-123") assert.Equal(t, -123, result) }) } + +func TestIntVal(t *testing.T) { + t.Run("Empty", func(t *testing.T) { + result := IntVal("", 1, 31, 1) + assert.Equal(t, 1, result) + }) + + t.Run("NonNumeric", func(t *testing.T) { + result := IntVal("Screenshot", 1, 31, -1) + assert.Equal(t, -1, result) + }) + + t.Run("Zero", func(t *testing.T) { + result := IntVal("0", -10, 10, -1) + assert.Equal(t, 0, result) + }) + + t.Run("LeadingZeros", func(t *testing.T) { + result := IntVal("000123", 1, 1000, 1) + assert.Equal(t, 123, result) + }) + + t.Run("WhitespacePadding", func(t *testing.T) { + result := IntVal(" 123\t ", 1, 1000, 1) + assert.Equal(t, 123, result) + }) + + t.Run("PositiveInt", func(t *testing.T) { + result := IntVal("123", 1, 1000, 1) + assert.Equal(t, 123, result) + }) + + t.Run("NegativeInt", func(t *testing.T) { + result := IntVal("-123", -1000, 1000, 1) + assert.Equal(t, -123, result) + }) +} + func TestIsUInt(t *testing.T) { assert.False(t, IsUInt("")) assert.False(t, IsUInt("12 3")) @@ -52,27 +100,32 @@ func TestIsPosInt(t *testing.T) { } func TestUInt(t *testing.T) { - t.Run("empty", func(t *testing.T) { + t.Run("Empty", func(t *testing.T) { result := UInt("") assert.Equal(t, uint(0), result) }) - t.Run("non-numeric", func(t *testing.T) { + t.Run("NonNumeric", func(t *testing.T) { result := UInt("Screenshot") assert.Equal(t, uint(0), result) }) - t.Run("zero", func(t *testing.T) { + t.Run("Zero", func(t *testing.T) { result := UInt("0") assert.Equal(t, uint(0), result) }) - t.Run("int", func(t *testing.T) { + t.Run("LeadingZeros", func(t *testing.T) { + result := UInt("000123") + assert.Equal(t, uint(0x7b), result) + }) + + t.Run("PositiveInt", func(t *testing.T) { result := UInt("123") assert.Equal(t, uint(0x7b), result) }) - t.Run("negative int", func(t *testing.T) { + t.Run("NegativeInt", func(t *testing.T) { result := UInt("-123") assert.Equal(t, uint(0), result) })