Metadata: Improve parsing of (incomplete) timestamp strings #625 #2133

This commit is contained in:
Michael Mayer 2022-03-25 16:31:09 +01:00
parent eaecedf4bb
commit 680686185f
9 changed files with 355 additions and 97 deletions

View file

@ -114,7 +114,7 @@ func (m *Folder) SetValuesFromPath() {
} }
if len(m.Path) >= 6 { 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 txt.IsUInt(s) || txt.IsTime(s) {
if date.Day() > 1 { if date.Day() > 1 {
m.FolderTitle = date.Format("January 2, 2006") m.FolderTitle = date.Format("January 2, 2006")

View file

@ -11,18 +11,20 @@ import (
"time" "time"
"github.com/dsoprea/go-exif/v3" "github.com/dsoprea/go-exif/v3"
"gopkg.in/photoprism/go-tz.v2/tz"
exifcommon "github.com/dsoprea/go-exif/v3/common" exifcommon "github.com/dsoprea/go-exif/v3/common"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/sanitize" "github.com/photoprism/photoprism/pkg/sanitize"
"gopkg.in/photoprism/go-tz.v2/tz" "github.com/photoprism/photoprism/pkg/txt"
) )
var exifIfdMapping *exifcommon.IfdMapping var exifIfdMapping *exifcommon.IfdMapping
var exifTagIndex = exif.NewTagIndex() var exifTagIndex = exif.NewTagIndex()
var exifMutex = sync.Mutex{} var exifMutex = sync.Mutex{}
var exifDateFields = []string{"DateTimeOriginal", "DateTimeDigitized", "CreateDate", "DateTime"}
const DateTimeZero = "0000:00:00 00:00:00"
func init() { func init() {
exifIfdMapping = exifcommon.NewIfdMapping() 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. // 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) { func Exif(fileName string, fileType fs.FileFormat) (data Data, err error) {
err = data.Exif(fileName, fileType) 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) { for _, name := range exifDateFields {
takenAt = value if dateTime := txt.DateTime(tags[name], data.TimeZone); !dateTime.IsZero() {
} else if value, ok := tags["DateTimeDigitized"]; ok && ValidDateTime(value) { takenAt = dateTime
takenAt = value break
} else if value, ok := tags["CreateDate"]; ok && ValidDateTime(value) { }
takenAt = value
} else if value, ok := tags["DateTime"]; ok && ValidDateTime(value) {
takenAt = value
} }
if ValidDateTime(takenAt) { // Valid time found in Exif metadata?
takenAt = strings.ReplaceAll(takenAt, "/", ":") if !takenAt.IsZero() {
takenAt = strings.ReplaceAll(takenAt, "-", ":") if takenAtLocal, err := time.ParseInLocation("2006-01-02T15:04:05", takenAt.Format("2006-01-02T15:04:05"), time.UTC); err == nil {
data.TakenAtLocal = takenAtLocal
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
}
} else { } 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 { if value, ok := tags["Flash"]; ok {

View file

@ -79,13 +79,8 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) {
continue continue
} }
s := strings.TrimSpace(jsonValue.String()) if dateTime := txt.DateTime(jsonValue.String(), ""); !dateTime.IsZero() {
s = strings.ReplaceAll(s, "/", ":") fieldValue.Set(reflect.ValueOf(dateTime))
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)))
} }
case time.Duration: case time.Duration:
if !fieldValue.IsZero() { if !fieldValue.IsZero() {

View file

@ -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. // 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] { if m.IsMedia() && entity.SrcPriority[photo.TakenSrc] <= entity.SrcPriority[entity.SrcName] {
// Try to extract time from original file name first. // 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) photo.SetTakenAt(taken, taken, "", entity.SrcName)
} else if taken, takenSrc := m.TakenAt(); takenSrc == entity.SrcName { } else if taken, takenSrc := m.TakenAt(); takenSrc == entity.SrcName {
photo.SetTakenAt(taken, taken, "", entity.SrcName) photo.SetTakenAt(taken, taken, "", entity.SrcName)

View file

@ -128,7 +128,7 @@ func (m *MediaFile) TakenAt() (time.Time, string) {
return m.takenAt, m.takenAtSrc 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.takenAt = nameTime
m.takenAtSrc = entity.SrcName m.takenAtSrc = entity.SrcName

View file

@ -1,11 +1,14 @@
package txt package txt
import ( import (
"fmt"
"regexp" "regexp"
"strings" "strings"
"time" "time"
) )
// Go regular expression tester: https://regoio.herokuapp.com/
var DateRegexp = regexp.MustCompile("\\D\\d{4}[\\-_]\\d{2}[\\-_]\\d{2,}") var DateRegexp = regexp.MustCompile("\\D\\d{4}[\\-_]\\d{2}[\\-_]\\d{2,}")
var DatePathRegexp = regexp.MustCompile("\\D\\d{4}/\\d{1,2}/?\\d*") 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,}") 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 YearRegexp = regexp.MustCompile("\\d{4,5}")
var IsDateRegexp = regexp.MustCompile("\\d{4}[\\-_]?\\d{2}[\\-_]?\\d{2}") 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 IsDateTimeRegexp = regexp.MustCompile("\\d{4}[\\-_]?\\d{2}[\\-_]?\\d{2}.{1,4}\\d{2}\\D?\\d{2}\\D?\\d{2}")
var ExifDateTimeRegexp = regexp.MustCompile("((?P<year>\\d{4})|\\D{4})\\D((?P<month>\\d{2})|\\D{2})\\D((?P<day>\\d{2})|\\D{2})\\D((?P<h>\\d{2})|\\D{2})\\D((?P<m>\\d{2})|\\D{2})\\D((?P<s>\\d{2})|\\D{2})(?P<z>\\D)?(?P<zh>\\d{2})?\\D?(?P<zm>\\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 ( var (
YearMin = 1990 YearMin = 1990
@ -32,8 +46,99 @@ const (
SecMax = 59 SecMax = 59
) )
// Time returns a string as time or the zero time instant in case it can not be converted. // DateTime parses a string and returns a valid Exif timestamp if possible.
func Time(s string) (result time.Time) { 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() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
result = time.Time{} result = time.Time{}

View file

@ -6,239 +6,334 @@ import (
"github.com/stretchr/testify/assert" "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) { 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.False(t, result.IsZero())
assert.Equal(t, "2016-08-18 00:00:00 +0000 UTC", result.String()) 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) { 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.False(t, result.IsZero())
assert.Equal(t, "2016-08-18 00:00:00 +0000 UTC", result.String()) 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) { 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.False(t, result.IsZero())
assert.Equal(t, "2018-04-12 19:24:49 +0000 UTC", result.String()) assert.Equal(t, "2018-04-12 19:24:49 +0000 UTC", result.String())
}) })
t.Run("2018", func(t *testing.T) { t.Run("2018", func(t *testing.T) {
result := Time("2018") result := DateFromFilePath("2018")
assert.True(t, result.IsZero()) assert.True(t, result.IsZero())
}) })
t.Run("2018-04-12 19/24/49.gif", func(t *testing.T) { 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.False(t, result.IsZero())
assert.Equal(t, "2018-04-12 19:24:49 +0000 UTC", result.String()) 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) { 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.False(t, result.IsZero())
assert.True(t, result.IsZero()) assert.True(t, result.IsZero())
}) })
t.Run("20130518_142022_3D657EBD.jpg", func(t *testing.T) { 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.False(t, result.IsZero())
assert.True(t, result.IsZero()) assert.True(t, result.IsZero())
}) })
t.Run("telegram_2020_01_30_09_57_18.jpg", func(t *testing.T) { 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.False(t, result.IsZero())
assert.Equal(t, "2020-01-30 09:57:18 +0000 UTC", result.String()) 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) { 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.False(t, result.IsZero())
assert.Equal(t, "2019-05-21 10:45:52 +0000 UTC", result.String()) 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) { 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.False(t, result.IsZero())
assert.Equal(t, "2020-01-30 09:57:18 +0000 UTC", result.String()) 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) { 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.False(t, result.IsZero())
assert.Equal(t, "2019-05-21 10:45:52 +0000 UTC", result.String()) 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) { 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.False(t, result.IsZero())
assert.Equal(t, "2020-01-30 00:00:00 +0000 UTC", result.String()) 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) { 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.False(t, result.IsZero())
assert.Equal(t, "2019-05-21 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "2019-05-21 00:00:00 +0000 UTC", result.String())
}) })
t.Run("/2019-05-21/file2314.JPG", func(t *testing.T) { 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.False(t, result.IsZero())
assert.Equal(t, "2019-05-21 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "2019-05-21 00:00:00 +0000 UTC", result.String())
}) })
t.Run("/2019.05.21", func(t *testing.T) { 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.True(t, result.IsZero())
assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String())
}) })
t.Run("/05.21.2019", func(t *testing.T) { 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.True(t, result.IsZero())
assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String())
}) })
t.Run("/21.05.2019", func(t *testing.T) { 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.True(t, result.IsZero())
assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String())
}) })
t.Run("05/21/2019", func(t *testing.T) { 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.True(t, result.IsZero())
assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String())
}) })
t.Run("2019-07-23", func(t *testing.T) { 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.False(t, result.IsZero())
assert.Equal(t, "2019-07-23 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "2019-07-23 00:00:00 +0000 UTC", result.String())
}) })
t.Run("Photos/2015-01-14", func(t *testing.T) { 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.False(t, result.IsZero())
assert.Equal(t, "2015-01-14 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "2015-01-14 00:00:00 +0000 UTC", result.String())
}) })
t.Run("21/05/2019", func(t *testing.T) { 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.True(t, result.IsZero())
assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String())
}) })
t.Run("2019/05/21", func(t *testing.T) { 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.False(t, result.IsZero())
assert.Equal(t, "2019-05-21 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "2019-05-21 00:00:00 +0000 UTC", result.String())
}) })
t.Run("2019/05/2145", func(t *testing.T) { 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.True(t, result.IsZero())
assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String())
}) })
t.Run("/05/21/2019", func(t *testing.T) { 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.True(t, result.IsZero())
assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String())
}) })
t.Run("/21/05/2019", func(t *testing.T) { 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.True(t, result.IsZero())
assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String())
}) })
t.Run("/2019/05/21.jpeg", func(t *testing.T) { 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.False(t, result.IsZero())
assert.Equal(t, "2019-05-21 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "2019-05-21 00:00:00 +0000 UTC", result.String())
}) })
t.Run("/2019/05/21/foo.txt", func(t *testing.T) { 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.False(t, result.IsZero())
assert.Equal(t, "2019-05-21 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "2019-05-21 00:00:00 +0000 UTC", result.String())
}) })
t.Run("2019/21/05", func(t *testing.T) { 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.True(t, result.IsZero())
assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String())
}) })
t.Run("/2019/05/21/foo.jpg", func(t *testing.T) { 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.False(t, result.IsZero())
assert.Equal(t, "2019-05-21 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "2019-05-21 00:00:00 +0000 UTC", result.String())
}) })
t.Run("/2019/21/05/foo.jpg", func(t *testing.T) { 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.True(t, result.IsZero())
assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String())
}) })
t.Run("/2019/5/foo.jpg", func(t *testing.T) { 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.False(t, result.IsZero())
assert.Equal(t, "2019-05-01 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "2019-05-01 00:00:00 +0000 UTC", result.String())
}) })
t.Run("/2019/1/3/foo.jpg", func(t *testing.T) { 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.False(t, result.IsZero())
assert.Equal(t, "2019-01-03 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "2019-01-03 00:00:00 +0000 UTC", result.String())
}) })
t.Run("/1989/1/3/foo.jpg", func(t *testing.T) { 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.True(t, result.IsZero())
assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String())
}) })
t.Run("545452019/1/3/foo.jpg", func(t *testing.T) { 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.False(t, result.IsZero())
assert.Equal(t, "2019-01-03 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "2019-01-03 00:00:00 +0000 UTC", result.String())
}) })
t.Run("fo.jpg", func(t *testing.T) { t.Run("fo.jpg", func(t *testing.T) {
result := Time("fo.jpg") result := DateFromFilePath("fo.jpg")
assert.True(t, result.IsZero()) assert.True(t, result.IsZero())
assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String())
}) })
t.Run("n >6", func(t *testing.T) { 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()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String())
}) })
t.Run("year < yearmin", func(t *testing.T) { 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()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String())
}) })
t.Run("hour > hourmax", func(t *testing.T) { 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()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String())
}) })
t.Run("invalid days", func(t *testing.T) { 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()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String())
}) })
t.Run("IMG-20191120-WA0001.jpg", func(t *testing.T) { 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()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String())
}) })
t.Run("VID-20191120-WA0001.jpg", func(t *testing.T) { 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()) assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String())
}) })
} }

View file

@ -20,6 +20,31 @@ func Int(s string) int {
return int(result) 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. // UInt converts a string to an unsigned integer or 0 if invalid.
func UInt(s string) uint { func UInt(s string) uint {
if s == "" { if s == "" {

View file

@ -7,31 +7,79 @@ import (
) )
func TestInt(t *testing.T) { func TestInt(t *testing.T) {
t.Run("empty", func(t *testing.T) { t.Run("Empty", func(t *testing.T) {
result := Int("") result := Int("")
assert.Equal(t, 0, result) assert.Equal(t, 0, result)
}) })
t.Run("non-numeric", func(t *testing.T) { t.Run("NonNumeric", func(t *testing.T) {
result := Int("Screenshot") result := Int("Screenshot")
assert.Equal(t, 0, result) assert.Equal(t, 0, result)
}) })
t.Run("zero", func(t *testing.T) { t.Run("Zero", func(t *testing.T) {
result := Int("0") result := Int("0")
assert.Equal(t, 0, result) 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") result := Int("123")
assert.Equal(t, 123, result) assert.Equal(t, 123, result)
}) })
t.Run("negative int", func(t *testing.T) { t.Run("NegativeInt", func(t *testing.T) {
result := Int("-123") result := Int("-123")
assert.Equal(t, -123, result) 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) { func TestIsUInt(t *testing.T) {
assert.False(t, IsUInt("")) assert.False(t, IsUInt(""))
assert.False(t, IsUInt("12 3")) assert.False(t, IsUInt("12 3"))
@ -52,27 +100,32 @@ func TestIsPosInt(t *testing.T) {
} }
func TestUInt(t *testing.T) { func TestUInt(t *testing.T) {
t.Run("empty", func(t *testing.T) { t.Run("Empty", func(t *testing.T) {
result := UInt("") result := UInt("")
assert.Equal(t, uint(0), result) assert.Equal(t, uint(0), result)
}) })
t.Run("non-numeric", func(t *testing.T) { t.Run("NonNumeric", func(t *testing.T) {
result := UInt("Screenshot") result := UInt("Screenshot")
assert.Equal(t, uint(0), result) assert.Equal(t, uint(0), result)
}) })
t.Run("zero", func(t *testing.T) { t.Run("Zero", func(t *testing.T) {
result := UInt("0") result := UInt("0")
assert.Equal(t, uint(0), result) 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") result := UInt("123")
assert.Equal(t, uint(0x7b), result) assert.Equal(t, uint(0x7b), result)
}) })
t.Run("negative int", func(t *testing.T) { t.Run("NegativeInt", func(t *testing.T) {
result := UInt("-123") result := UInt("-123")
assert.Equal(t, uint(0), result) assert.Equal(t, uint(0), result)
}) })