diff --git a/.golangci.yml b/.golangci.yml index bf4a5a3..6952554 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -34,7 +34,7 @@ linters-settings: check-shadowing: true gocyclo: # minimal code complexity to report, 30 by default - min-complexity: 20 + min-complexity: 30 maligned: # print struct with more effective memory layout or not, false by default suggest-new: true \ No newline at end of file diff --git a/README.md b/README.md index 13f073f..9da3f72 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,6 @@ Those are my commands: - /groupchatinvitegenerate - Generate groupchat invite link - /groupchatlist - Groupchat list - /groupchatmembers - List groupchat members -- /groupchatuserunban - Unban user in groupchat - /groupchatuserban - Ban user in groupchat - /groupchatuserunban - Unban user in groupchat - /groupcreate - Create group @@ -40,6 +39,17 @@ Those are my commands: - /groupundelete - Undelete group - /help - Display this help - /me - Your ID/username +- /meetingroomactivate - Activate meetingroom +- /meetingroomblock - Block meetingroom +- /meetingroombook - Book meetingroom +- /meetingroomcreate - Add meetingroom +- /meetingroomdelete - Delete meetingroom +- /meetingroomlist - Return list of meetingrooms +- /meetingroomrebook - Rebook meetingroom +- /meetingroomrename - Rename meetingrooms +- /meetingroomschedule - Return schedule of meetingroom +- /meetingroomscheduleinfo - Return schedule info +- /meetingroomunbook - Unbook meetingroom - /message - Send message to user - /plugindisable - Disable plugin - /pluginenable - Enable plugin @@ -58,7 +68,6 @@ Those are my commands: - [ ] Поздравлять пользователя с днем рождения и уведомлять админов (заранее) ## Идеи для плагинов -- [ ] Переговорки - [ ] Отпуска - [ ] Онбординг - [ ] Контакты diff --git a/clock/clock.go b/clock/clock.go new file mode 100644 index 0000000..1f63e2b --- /dev/null +++ b/clock/clock.go @@ -0,0 +1,51 @@ +package clock + +import ( + "fmt" + + tgbotapi "gopkg.in/telegram-bot-api.v4" +) + +func GenerateClock(command string, hour, minute int, lang string) tgbotapi.InlineKeyboardMarkup { + keyboard := tgbotapi.InlineKeyboardMarkup{} + if hour < 0 { + keyboard = addHours(command, keyboard, lang) + } else { + keyboard = addMinutes(command, hour, keyboard, lang) + } + return keyboard +} + +func addHours(command string, keyboard tgbotapi.InlineKeyboardMarkup, lang string) tgbotapi.InlineKeyboardMarkup { + var rowHours []tgbotapi.InlineKeyboardButton + pos := 0 + for i := 0; i < 24; i++ { + btn := tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("%02d:...", i), fmt.Sprintf("%s %02d", command, i)) + rowHours = append(rowHours, btn) + if (pos+1)%6 == 0 { + keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, rowHours) + rowHours = []tgbotapi.InlineKeyboardButton{} + } + pos++ + } + + keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, rowHours) + return keyboard +} + +func addMinutes(command string, hour int, keyboard tgbotapi.InlineKeyboardMarkup, lang string) tgbotapi.InlineKeyboardMarkup { + var rowHours []tgbotapi.InlineKeyboardButton + pos := 0 + for i := 0; i < 60; i += 5 { + btn := tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("%02d:%02d", hour, i), fmt.Sprintf("%s %02d:%02d", command, hour, i)) + rowHours = append(rowHours, btn) + if (pos+1)%4 == 0 { + keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, rowHours) + rowHours = []tgbotapi.InlineKeyboardButton{} + } + pos++ + } + + keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, rowHours) + return keyboard +} diff --git a/db/const.go b/db/const.go index 96773c8..dd2e90e 100644 --- a/db/const.go +++ b/db/const.go @@ -13,8 +13,17 @@ const ( GroupChatAlreadyExists = "groupchat already exists" GroupChatNotFound = "groupchat not found" + MeetingroomAlreadyExists = "meetingroom already exists" + MeetingroomNotFound = "meetingroom not found" + MeetingroomDeleted = "meetingroom deleted" + MeetingroomBlocked = "meetingroom blocked" + + MeetingroomBusy = "meetingroom busy" + + Active = "active" Deleted = "deleted" Blocked = "blocked" + Enabled = "enabled" New = "new" Member = "member" Admin = "admin" diff --git a/db/db.go b/db/db.go index c03d5ac..c032fbf 100644 --- a/db/db.go +++ b/db/db.go @@ -208,7 +208,7 @@ func QuerySQLList(db *sql.DB, returnModel interface{}, sql string, args ...inter // StoreTelegramMessage ... func StoreTelegramMessage(db *sql.DB, message *TelegramMessage) error { _, err := db.Exec( - "INSERT INTO telegram_messages (telegram_id, message, created_at, is_incoming) VALUES (?, ?, ?, ?);", + `INSERT INTO telegram_messages (telegram_id, message, created_at, is_incoming) VALUES (?, ?, ?, ?);`, message.TelegramID, message.Message, message.Date, diff --git a/db/groupchat.go b/db/groupchat.go index ddad7db..32314ec 100644 --- a/db/groupchat.go +++ b/db/groupchat.go @@ -8,7 +8,6 @@ import ( dlog "github.com/amoghe/distillog" sql "github.com/lazada/sqle" - _ "github.com/mattn/go-sqlite3" // Register some sql ) @@ -37,7 +36,7 @@ func (gc *Groupchat) String() string { // GetGroupchats ... func GetGroupchats(db *sql.DB, states []string) (groupchats []*Groupchat, err error) { if len(states) == 0 { - states = []string{"active"} + states = []string{Active} } args := make([]interface{}, len(states)) @@ -109,7 +108,7 @@ func AddGroupChatIfNotExist(db *sql.DB, groupchat *Groupchat) (*Groupchat, error } res, err := db.Exec( - "INSERT INTO groupchats (title, telegram_id, invite_link, state) VALUES (?, ?, ?, ?);", + `INSERT INTO groupchats (title, telegram_id, invite_link, state) VALUES (?, ?, ?, ?);`, groupchat.Title, groupchat.TelegramID, groupchat.InviteLink, @@ -126,7 +125,7 @@ func AddGroupChatIfNotExist(db *sql.DB, groupchat *Groupchat) (*Groupchat, error groupchat.CreatedAt = time.Now() - dlog.Debugf("%s (%d) added at %s\n", groupchat.Title, groupchat.ID, groupchat.CreatedAt) + dlog.Debugf(`%s (%d) added at %s\n`, groupchat.Title, groupchat.ID, groupchat.CreatedAt) return groupchat, nil } @@ -134,7 +133,7 @@ func AddGroupChatIfNotExist(db *sql.DB, groupchat *Groupchat) (*Groupchat, error // UpdateGroupChatInviteLink ... func UpdateGroupChatInviteLink(db *sql.DB, groupchat *Groupchat) (int64, error) { result, err := db.Exec( - "UPDATE groupchats SET invite_link = ? WHERE telegram_id = ?;", + `UPDATE groupchats SET invite_link = ? WHERE telegram_id = ?;`, groupchat.InviteLink, groupchat.TelegramID) if err != nil { @@ -152,7 +151,7 @@ func UpdateGroupChatInviteLink(db *sql.DB, groupchat *Groupchat) (int64, error) // UpdateGroupChatTitle ... func UpdateGroupChatTitle(db *sql.DB, groupchat *Groupchat) (int64, error) { result, err := db.Exec( - "UPDATE groupchats SET title = ? WHERE telegram_id = ?;", + `UPDATE groupchats SET title = ? WHERE telegram_id = ?;`, groupchat.Title, groupchat.TelegramID) if err != nil { @@ -186,7 +185,7 @@ func GetGroupChatByTelegramID(db *sql.DB, groupchat *Groupchat) (*Groupchat, err // GroupChatDelete ... func GroupChatDelete(db *sql.DB, groupchat *Groupchat) (bool, error) { _, err := db.Exec( - "DELETE FROM groupchats WHERE telegram_id = ?;", + `DELETE FROM groupchats WHERE telegram_id = ?;`, groupchat.TelegramID, ) if err != nil { diff --git a/db/groups.go b/db/groups.go index a7732e6..1b58a18 100644 --- a/db/groups.go +++ b/db/groups.go @@ -7,7 +7,6 @@ import ( dlog "github.com/amoghe/distillog" sql "github.com/lazada/sqle" - _ "github.com/mattn/go-sqlite3" // Register some sql ) @@ -28,7 +27,7 @@ func AddGroupIfNotExist(db *sql.DB, group *Group) (*Group, error) { var returnModel Group if group.State == "" { - group.State = "active" + group.State = Active } result, err := QuerySQLObject(db, returnModel, `SELECT * FROM groups WHERE name = ?;`, group.Name) @@ -36,7 +35,7 @@ func AddGroupIfNotExist(db *sql.DB, group *Group) (*Group, error) { return nil, err } - if returnModel, ok := result.Interface().(*Group); ok && returnModel.State == "deleted" { + if returnModel, ok := result.Interface().(*Group); ok && returnModel.State == Deleted { return returnModel, fmt.Errorf(GroupDeleted) } @@ -45,7 +44,7 @@ func AddGroupIfNotExist(db *sql.DB, group *Group) (*Group, error) { } res, err := db.Exec( - "INSERT INTO groups (name, state) VALUES (?, ?);", + `INSERT INTO groups (name, state) VALUES (?, ?);`, group.Name, group.State, ) @@ -60,7 +59,7 @@ func AddGroupIfNotExist(db *sql.DB, group *Group) (*Group, error) { group.CreatedAt = time.Now() - dlog.Debugf("%s (%d) added at %s\n", group.Name, group.ID, group.CreatedAt) + dlog.Debugf(`%s (%d) added at %s\n`, group.Name, group.ID, group.CreatedAt) return group, nil } @@ -68,7 +67,7 @@ func AddGroupIfNotExist(db *sql.DB, group *Group) (*Group, error) { // GetGroups ... func GetGroups(db *sql.DB, states []string) (groups []*Group, err error) { if len(states) == 0 { - states = []string{"active"} + states = []string{Active} } args := make([]interface{}, len(states)) @@ -103,7 +102,7 @@ ORDER BY // UpdateGroupState ... func UpdateGroupState(db *sql.DB, group *Group) (int64, error) { result, err := db.Exec( - "UPDATE groups SET state = ? WHERE name = ? AND state != ?;", + `UPDATE groups SET state = ? WHERE name = ? AND state != ?;`, group.State, group.Name, group.State) @@ -122,7 +121,7 @@ func UpdateGroupState(db *sql.DB, group *Group) (int64, error) { // UpdateGroupName ... func UpdateGroupName(db *sql.DB, oldName, newName string) (int64, error) { result, err := db.Exec( - "UPDATE groups SET name = ? WHERE name = ? AND name != ?;", + `UPDATE groups SET name = ? WHERE name = ? AND name != ?;`, newName, oldName, newName) @@ -157,7 +156,7 @@ func GetGroupByName(db *sql.DB, group *Group) (*Group, error) { // AddGroupGroupChatIfNotExist ... func AddGroupGroupChatIfNotExist(db *sql.DB, group *Group, groupchat *Groupchat) (bool, error) { res, err := db.Exec( - "INSERT INTO groups_groupchats (group_id, groupchat_id) VALUES (?, ?);", + `INSERT INTO groups_groupchats (group_id, groupchat_id) VALUES (?, ?);`, group.ID, groupchat.ID, ) @@ -176,7 +175,7 @@ func AddGroupGroupChatIfNotExist(db *sql.DB, group *Group, groupchat *Groupchat) // DeleteGroupGroupChat ... func DeleteGroupGroupChat(db *sql.DB, group *Group, groupchat *Groupchat) (bool, error) { _, err := db.Exec( - "DELETE FROM groups_groupchats WHERE group_id = ? AND groupchat_id = ?;", + `DELETE FROM groups_groupchats WHERE group_id = ? AND groupchat_id = ?;`, group.ID, groupchat.ID, ) @@ -190,7 +189,7 @@ func DeleteGroupGroupChat(db *sql.DB, group *Group, groupchat *Groupchat) (bool, // AddGroupUserIfNotExist ... func AddGroupUserIfNotExist(db *sql.DB, group *Group, user *User) (bool, error) { res, err := db.Exec( - "INSERT INTO groups_Users (group_id, user_id) VALUES (?, ?);", + `INSERT INTO groups_Users (group_id, user_id) VALUES (?, ?);`, group.ID, user.ID, ) @@ -209,7 +208,7 @@ func AddGroupUserIfNotExist(db *sql.DB, group *Group, user *User) (bool, error) // DeleteGroupUser ... func DeleteGroupUser(db *sql.DB, group *Group, user *User) (bool, error) { _, err := db.Exec( - "DELETE FROM groups_Users WHERE group_id = ? AND user_id = ?;", + `DELETE FROM groups_Users WHERE group_id = ? AND user_id = ?;`, group.ID, user.ID, ) diff --git a/db/meetingroom.go b/db/meetingroom.go new file mode 100644 index 0000000..d294d75 --- /dev/null +++ b/db/meetingroom.go @@ -0,0 +1,275 @@ +package db + +import ( + "fmt" + "strings" + "time" + + dlog "github.com/amoghe/distillog" + sql "github.com/lazada/sqle" + _ "github.com/mattn/go-sqlite3" // Register some sql +) + +// Meetingroom ... +type Meetingroom struct { + ID int64 `sql:"id"` + Name string `sql:"name"` + State string `sql:"state"` + CreatedAt time.Time `sql:"created_at"` + UpdateddAt time.Time `sql:"updated_at"` +} + +// MeetingroomSchedule ... +type MeetingroomSchedule struct { + ID int64 `sql:"id"` + MeetingroomID int64 `sql:"meetingroom_id"` + Start time.Time `sql:"start"` + End time.Time `sql:"end"` + Creator int64 `sql:"creator"` + CreatedAt time.Time `sql:"created_at"` + UpdateddAt time.Time `sql:"updated_at"` +} + +func (m *Meetingroom) String() string { + return m.Name +} + +func (s *MeetingroomSchedule) String() string { + return s.Start.Format("15:04") + "..." + s.End.Format("15:04") +} + +// AddMeetingroomIfNotExist ... +func AddMeetingroomIfNotExist(db *sql.DB, m *Meetingroom) (*Meetingroom, error) { + var returnModel Meetingroom + + result, err := QuerySQLObject(db, returnModel, `SELECT * FROM meetingrooms WHERE name = ?;`, m.Name) + if err != nil { + return nil, err + } + + if returnModel, ok := result.Interface().(*Meetingroom); ok && returnModel.State == Deleted { + return returnModel, fmt.Errorf(MeetingroomDeleted) + } + + if returnModel, ok := result.Interface().(*Meetingroom); ok && returnModel.State == Blocked { + return returnModel, fmt.Errorf(MeetingroomBlocked) + } + + if returnModel, ok := result.Interface().(*Meetingroom); ok && returnModel.Name != "" { + return returnModel, fmt.Errorf(MeetingroomAlreadyExists) + } + + res, err := db.Exec( + `INSERT INTO meetingrooms (name, state) VALUES (?, ?);`, + m.Name, + m.State, + ) + if err != nil { + return nil, err + } + + m.ID, err = res.LastInsertId() + if err != nil { + return nil, err + } + + m.CreatedAt = time.Now() + + dlog.Debugf(`%s (%d) added at %s\n`, m.Name, m.ID, m.CreatedAt) + + return m, nil +} + +// GetMeetingroomByName ... +func GetMeetingroomByName(db *sql.DB, m *Meetingroom) (*Meetingroom, error) { + var returnModel Meetingroom + + result, err := QuerySQLObject(db, returnModel, `SELECT * FROM meetingrooms WHERE name = ? AND state=?;`, m.Name, Active) + if err != nil { + return nil, err + } + + if returnModel, ok := result.Interface().(*Meetingroom); ok && returnModel.Name != "" { + return returnModel, nil + } + + return nil, fmt.Errorf(MeetingroomNotFound) +} + +// GetMeetingroomSchedulesByID ... +func GetMeetingroomSchedulesByID(db *sql.DB, m *Meetingroom, date string) (ms []*MeetingroomSchedule, err error) { + var returnModel MeetingroomSchedule + + start, err := time.Parse(`2006.01.02`, date) + if err != nil { + return ms, err + } + + sql := `SELECT * FROM meetingroom_schedule WHERE meetingroom_id = ? AND start >= ? AND end <= ?;` + + args := make([]interface{}, 0) + args = append(args, m.ID) + args = append(args, start) + args = append(args, start.Add(time.Hour*24*time.Duration(1))) // + 1 day + + result, err := QuerySQLList(db, returnModel, sql, args...) + if err != nil { + return ms, err + } + + for _, item := range result { + if returnModel, ok := item.Interface().(*MeetingroomSchedule); ok { + ms = append(ms, returnModel) + } + } + + return ms, err +} + +// GetMeetingroomScheduleByID ... +func GetMeetingroomScheduleByID(db *sql.DB, m *MeetingroomSchedule) (*MeetingroomSchedule, error) { + var returnModel MeetingroomSchedule + + result, err := QuerySQLObject(db, returnModel, `SELECT * FROM meetingroom_schedule WHERE id = ?;`, m.ID) + if err != nil { + return nil, err + } + + if returnModel, ok := result.Interface().(*MeetingroomSchedule); ok && returnModel.ID != 0 { + return returnModel, nil + } + + return nil, fmt.Errorf(MeetingroomNotFound) +} + +// GetMeetingrooms ... +func GetMeetingrooms(db *sql.DB, states []string) (ms []*Meetingroom, err error) { + if len(states) == 0 { + states = []string{Active} + } + + args := make([]interface{}, len(states)) + for i, state := range states { + args[i] = state + } + + var returnModel Meetingroom + sql := `SELECT + * +FROM + meetingrooms +WHERE + state IN (?` + strings.Repeat(`,?`, len(args)-1) + `) +ORDER BY + state, id;` + + result, err := QuerySQLList(db, returnModel, sql, args...) + if err != nil { + return ms, err + } + + for _, item := range result { + if returnModel, ok := item.Interface().(*Meetingroom); ok { + ms = append(ms, returnModel) + } + } + + return ms, err +} + +// UpdateMeetingroomState ... +func UpdateMeetingroomState(db *sql.DB, m *Meetingroom) (int64, error) { + result, err := db.Exec( + `UPDATE meetingrooms SET state = ? WHERE name = ? AND state != ?;`, + m.State, + m.Name, + m.State) + if err != nil { + return -1, err + } + + rows, err := result.RowsAffected() + if err != nil { + return -1, err + } + + return rows, nil +} + +// UpdateMeetingroomName ... +func UpdateMeetingroomName(db *sql.DB, oldName, newName string) (int64, error) { + result, err := db.Exec(`UPDATE meetingrooms SET name = ? WHERE name = ? AND name != ?;`, newName, oldName, newName) + if err != nil { + return -1, err + } + + rows, err := result.RowsAffected() + if err != nil { + return -1, err + } + + return rows, nil +} + +// AddSchedule ... +func AddSchedule(db *sql.DB, ms *MeetingroomSchedule) (*MeetingroomSchedule, error) { + var returnModel MeetingroomSchedule + + result, err := QuerySQLObject( + db, + returnModel, + `SELECT * FROM meetingroom_schedule WHERE meetingroom_id = ? AND start < ? AND END > ?;`, + ms.MeetingroomID, + ms.End, + ms.Start, + ) + + if err != nil { + return nil, err + } + + if returnModel, ok := result.Interface().(*MeetingroomSchedule); ok && returnModel.ID != 0 { + return returnModel, fmt.Errorf(MeetingroomBusy) + } + + res, err := db.Exec( + `INSERT INTO meetingroom_schedule (creator, meetingroom_id, start, end) VALUES (?, ?, ?, ?);`, + ms.Creator, + ms.MeetingroomID, + ms.Start, + ms.End, + ) + if err != nil { + return nil, err + } + + ms.ID, err = res.LastInsertId() + if err != nil { + return nil, err + } + + ms.CreatedAt = time.Now() + + dlog.Debugf(`%s %d (%d) %s-%s added at %s\n`, ms.Creator, ms.MeetingroomID, ms.ID, ms.Start, ms.End, ms.CreatedAt) + + return ms, nil +} + +// RemoveSchedule ... +func RemoveSchedule(db *sql.DB, ms *MeetingroomSchedule) (bool, error) { + var returnModel MeetingroomSchedule + + _, err := QuerySQLObject( + db, + returnModel, + `DELETE FROM meetingroom_schedule WHERE meetingroom_id = ? AND start = ? AND END = ?;`, + ms.MeetingroomID, + ms.End, + ms.Start, + ) + + if err != nil { + return false, err + } + return true, nil +} diff --git a/db/plugins.go b/db/plugins.go index a5b2c9b..e0511f9 100644 --- a/db/plugins.go +++ b/db/plugins.go @@ -6,7 +6,6 @@ import ( dlog "github.com/amoghe/distillog" sql "github.com/lazada/sqle" - _ "github.com/mattn/go-sqlite3" // Register some sql ) @@ -42,7 +41,7 @@ func AddPluginIfNotExist(db *sql.DB, plugin *Plugin) (*Plugin, error) { } res, err := db.Exec( - "INSERT INTO plugins (name, state) VALUES (?, ?);", + `INSERT INTO plugins (name, state) VALUES (?, ?);`, plugin.Name, plugin.State, ) @@ -57,7 +56,7 @@ func AddPluginIfNotExist(db *sql.DB, plugin *Plugin) (*Plugin, error) { plugin.CreatedAt = time.Now() - dlog.Debugf("%s (%s) added at %s\n", plugin.Name, plugin.State, plugin.CreatedAt) + dlog.Debugf(`%s (%s) added at %s\n`, plugin.Name, plugin.State, plugin.CreatedAt) return plugin, nil } @@ -65,7 +64,7 @@ func AddPluginIfNotExist(db *sql.DB, plugin *Plugin) (*Plugin, error) { // UpdatePluginState ... func UpdatePluginState(db *sql.DB, plugin *Plugin) (int64, error) { result, err := db.Exec( - "UPDATE plugins SET state = ? WHERE name = ? AND state != ?;", + `UPDATE plugins SET state = ? WHERE name = ? AND state != ?;`, plugin.State, plugin.Name, plugin.State) @@ -106,5 +105,5 @@ ORDER BY } func (p *Plugin) IsEnabled() bool { - return p.State == "enabled" + return p.State == Enabled } diff --git a/db/users.go b/db/users.go index 11116b8..21c9088 100644 --- a/db/users.go +++ b/db/users.go @@ -45,12 +45,54 @@ func (u *User) String() string { b.WriteRune(']') b.WriteRune(' ') } - b.WriteRune('(') - b.WriteString(u.Role) - b.WriteRune(')') + if u.Role != "" { + b.WriteRune('(') + b.WriteString(u.Role) + b.WriteRune(')') + } + + if b.Len() == 0 { + b.WriteString("ID") + b.WriteString(strconv.FormatInt(u.ID, 10)) + } return b.String() } + +func (u *User) Short() string { + var b strings.Builder + if u.UserName != "" { + b.WriteRune('@') + b.WriteString(u.UserName) + } + if u.FirstName != "" { + if b.Len() != 0 { + b.WriteRune(' ') + } + b.WriteString(u.FirstName) + } + if u.LastName != "" { + if b.Len() != 0 { + b.WriteRune(' ') + } + b.WriteString(u.LastName) + } + if b.Len() == 0 { + if u.TelegramID != 0 { + b.WriteRune('[') + b.WriteString(strconv.FormatInt(u.TelegramID, 10)) + b.WriteRune(']') + } + } + + if b.Len() == 0 { + b.WriteString("ID") + b.WriteString(strconv.FormatInt(u.ID, 10)) + } + + return b.String() +} + func (u *User) Paragraph() string { var b strings.Builder if u.FirstName != "" { @@ -94,20 +136,20 @@ func AddUserIfNotExist(db *sql.DB, user *User) (*User, error) { return nil, err } - if returnModel, ok := result.Interface().(*User); ok && returnModel.Role == "deleted" { + if returnModel, ok := result.Interface().(*User); ok && returnModel.Role == Deleted { return returnModel, fmt.Errorf(UserDeleted) } - if returnModel, ok := result.Interface().(*User); ok && returnModel.Role == "blocked" { + if returnModel, ok := result.Interface().(*User); ok && returnModel.Role == Blocked { return returnModel, fmt.Errorf(UserBlocked) } - if returnModel, ok := result.Interface().(*User); ok && returnModel.UserName != "" { + if returnModel, ok := result.Interface().(*User); ok && returnModel.TelegramID > 0 { return returnModel, fmt.Errorf(UserAlreadyExists) } res, err := db.Exec( - "INSERT INTO users (first_name, last_name, user_name, telegram_id, is_bot, role) VALUES (?, ?, ?, ?, ?, ?);", + `INSERT INTO users (first_name, last_name, user_name, telegram_id, is_bot, role) VALUES (?, ?, ?, ?, ?, ?);`, user.FirstName, user.LastName, user.UserName, @@ -126,7 +168,7 @@ func AddUserIfNotExist(db *sql.DB, user *User) (*User, error) { user.CreatedAt = time.Now() - dlog.Debugf("%s (%d) added at %s\n", user.UserName, user.ID, user.CreatedAt) + dlog.Debugf(`%s (%d) added at %s\n`, user.UserName, user.ID, user.CreatedAt) return user, nil } @@ -134,7 +176,7 @@ func AddUserIfNotExist(db *sql.DB, user *User) (*User, error) { // GetUsers ... func GetUsers(db *sql.DB, roles []string) (users []*User, err error) { if len(roles) == 0 { - roles = []string{"owner", "admin", "member", "new"} + roles = []string{Owner, Admin, Member, New} } args := make([]interface{}, len(roles)) @@ -171,10 +213,12 @@ ORDER BY // UpdateUserRole ... func UpdateUserRole(db *sql.DB, user *User) (int64, error) { result, err := db.Exec( - "UPDATE users SET role = ? WHERE telegram_id = ? AND role != ? AND role != 'owner';", + `UPDATE users SET role = ? WHERE telegram_id = ? AND role != ? AND role != ?;`, user.Role, user.TelegramID, - user.Role) + user.Role, + Owner, + ) if err != nil { return -1, err } @@ -189,7 +233,7 @@ func UpdateUserRole(db *sql.DB, user *User) (int64, error) { // UpdateUserBirthday ... func UpdateUserBirthday(db *sql.DB, user *User) (int64, error) { - result, err := db.Exec("UPDATE users SET birthday = ? WHERE telegram_id = ?;", user.Birthday, user.TelegramID) + result, err := db.Exec(`UPDATE users SET birthday = ? WHERE telegram_id = ?;`, user.Birthday, user.TelegramID) if err != nil { return -1, err } @@ -202,6 +246,22 @@ func UpdateUserBirthday(db *sql.DB, user *User) (int64, error) { return rows, nil } +// GetUserByID ... +func GetUserByID(db *sql.DB, user *User) (*User, error) { + var returnModel User + + result, err := QuerySQLObject(db, returnModel, `SELECT * FROM users WHERE id = ?;`, user.ID) + if err != nil { + return nil, err + } + + if returnModel, ok := result.Interface().(*User); ok && returnModel.Role != "" { + return returnModel, nil + } + + return nil, fmt.Errorf(UserNotFound) +} + // GetUserByTelegramID ... func GetUserByTelegramID(db *sql.DB, user *User) (*User, error) { var returnModel User diff --git a/go.mod b/go.mod index e576b78..afd2cfe 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,10 @@ module github.com/ad/corpobot -go 1.15 +go 1.16 require ( github.com/amoghe/distillog v0.0.0-20180726233512-ae382b35b717 + github.com/araddon/dateparse v0.0.0-20210207001429-0eec95c9db7e github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect github.com/lazada/sqle v0.0.0-20171211164427-f1ca64d42ef4 github.com/mattn/go-sqlite3 v1.14.6 diff --git a/go.sum b/go.sum index af5ef6f..44689c8 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,23 @@ github.com/amoghe/distillog v0.0.0-20180726233512-ae382b35b717 h1:cdJG5d0xtOHgeev36KoJCRZMOct8GzxXCcJgyzUyUd4= github.com/amoghe/distillog v0.0.0-20180726233512-ae382b35b717/go.mod h1:7DXz/QzS6kSSk2b7TqXmf4RGWX3bVsdyxpwWpQ1i0J4= +github.com/araddon/dateparse v0.0.0-20210207001429-0eec95c9db7e h1:OjdSMCht0ZVX7IH0nTdf00xEustvbtUGRgMh3gbdmOg= +github.com/araddon/dateparse v0.0.0-20210207001429-0eec95c9db7e/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU= github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= github.com/lazada/sqle v0.0.0-20171211164427-f1ca64d42ef4 h1:P5RWAfeKQsZ8zYU4/KGqYn/A1HN791/hsFx5qvkcVio= github.com/lazada/sqle v0.0.0-20171211164427-f1ca64d42ef4/go.mod h1:MhEqbqotb5MjvDGF4jAXC9Kiieb3IVe6kw6BUeQPiXY= +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw= @@ -14,5 +26,9 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/telegram-bot-api.v4 v4.6.4 h1:hpHWhzn4jTCsAJZZ2loNKfy2QWyPDRJVl3aTFXeMW8g= gopkg.in/telegram-bot-api.v4 v4.6.4/go.mod h1:5DpGO5dbumb40px+dXcwCpcjmeHNYLpk0bp3XRNvWDM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 7d87c7a..7454131 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( _ "github.com/ad/corpobot/plugins/groupchats" _ "github.com/ad/corpobot/plugins/groups" _ "github.com/ad/corpobot/plugins/me" + _ "github.com/ad/corpobot/plugins/meetingroom" _ "github.com/ad/corpobot/plugins/messages" _ "github.com/ad/corpobot/plugins/starthelp" _ "github.com/ad/corpobot/plugins/users" @@ -22,7 +23,7 @@ import ( tgbotapi "gopkg.in/telegram-bot-api.v4" ) -const version = "0.3.1" +const version = "0.3.2" var ( err error diff --git a/plugins/admin/admin.go b/plugins/admin/admin.go index c69a1bc..3015907 100644 --- a/plugins/admin/admin.go +++ b/plugins/admin/admin.go @@ -4,7 +4,6 @@ import ( database "github.com/ad/corpobot/db" "github.com/ad/corpobot/plugins" "github.com/ad/corpobot/telegram" - dlog "github.com/amoghe/distillog" tgbotapi "gopkg.in/telegram-bot-api.v4" ) @@ -20,9 +19,9 @@ func (m *Plugin) OnStart() { return } - plugins.RegisterCommand("pluginlist", "List of plugins", []string{"owner"}, pluginList) - plugins.RegisterCommand("pluginenable", "Enable plugin", []string{"owner"}, pluginEnable) - plugins.RegisterCommand("plugindisable", "Disable plugin", []string{"owner"}, pluginDisable) + plugins.RegisterCommand("pluginlist", "List of plugins", []string{database.Owner}, pluginList) + plugins.RegisterCommand("pluginenable", "Enable plugin", []string{database.Owner}, pluginEnable) + plugins.RegisterCommand("plugindisable", "Disable plugin", []string{database.Owner}, pluginDisable) } func (m *Plugin) OnStop() { diff --git a/plugins/echo/echo.go b/plugins/echo/echo.go index 5fa71bf..41d0853 100644 --- a/plugins/echo/echo.go +++ b/plugins/echo/echo.go @@ -1,10 +1,9 @@ package echo import ( + database "github.com/ad/corpobot/db" "github.com/ad/corpobot/plugins" "github.com/ad/corpobot/telegram" - - database "github.com/ad/corpobot/db" dlog "github.com/amoghe/distillog" tgbotapi "gopkg.in/telegram-bot-api.v4" ) @@ -20,7 +19,7 @@ func (m *Plugin) OnStart() { return } - plugins.RegisterCommand("echo", "example plugin", []string{"new", "member", "admin", "owner"}, echo) + plugins.RegisterCommand("echo", "example plugin", []string{database.New, database.Member, database.Admin, database.Owner}, echo) } func (m *Plugin) OnStop() { diff --git a/plugins/groupchats/groupchats.go b/plugins/groupchats/groupchats.go index 360cf65..b7b308c 100644 --- a/plugins/groupchats/groupchats.go +++ b/plugins/groupchats/groupchats.go @@ -7,7 +7,6 @@ import ( database "github.com/ad/corpobot/db" "github.com/ad/corpobot/plugins" "github.com/ad/corpobot/telegram" - dlog "github.com/amoghe/distillog" tgbotapi "gopkg.in/telegram-bot-api.v4" ) @@ -23,12 +22,12 @@ func (m *Plugin) OnStart() { return } - plugins.RegisterCommand("groupchatlist", "Groupchat list", []string{"member", "admin", "owner"}, groupChatList) - plugins.RegisterCommand("groupchatinvitegenerate", "Generate groupchat invite link", []string{"admin", "owner"}, groupChatInviteGenerate) - plugins.RegisterCommand("groupchatuserban", "Ban user in groupchat", []string{"admin", "owner"}, groupChatUserBan) - plugins.RegisterCommand("groupchatuserunban", "Unban user in groupchat", []string{"admin", "owner"}, groupChatUserUnban) - plugins.RegisterCommand("groupchatmembers", "List groupchat members", []string{"admin", "owner"}, groupChatMembers) - plugins.RegisterCommand("groupchatdelete", "Delete groupchat", []string{"admin", "owner"}, groupChatDelete) + plugins.RegisterCommand("groupchatlist", "Groupchat list", []string{database.Member, database.Admin, database.Owner}, groupChatList) + plugins.RegisterCommand("groupchatinvitegenerate", "Generate groupchat invite link", []string{database.Admin, database.Owner}, groupChatInviteGenerate) + plugins.RegisterCommand("groupchatuserban", "Ban user in groupchat", []string{database.Admin, database.Owner}, groupChatUserBan) + plugins.RegisterCommand("groupchatuserunban", "Unban user in groupchat", []string{database.Admin, database.Owner}, groupChatUserUnban) + plugins.RegisterCommand("groupchatmembers", "List groupchat members", []string{database.Admin, database.Owner}, groupChatMembers) + plugins.RegisterCommand("groupchatdelete", "Delete groupchat", []string{database.Admin, database.Owner}, groupChatDelete) } func (m *Plugin) OnStop() { diff --git a/plugins/groups/groups.go b/plugins/groups/groups.go index de6f8b9..57b740a 100644 --- a/plugins/groups/groups.go +++ b/plugins/groups/groups.go @@ -23,15 +23,15 @@ func (m *Plugin) OnStart() { return } - plugins.RegisterCommand("grouplist", "Group list", []string{"member", "admin", "owner"}, groupList) - plugins.RegisterCommand("groupcreate", "Create group", []string{"admin", "owner"}, groupCreate) - plugins.RegisterCommand("grouprename", "Rename group", []string{"admin", "owner"}, groupRename) - plugins.RegisterCommand("groupdelete", "Delete group", []string{"admin", "owner"}, groupDeleteUndelete) - plugins.RegisterCommand("groupundelete", "Undelete group", []string{"admin", "owner"}, groupDeleteUndelete) - plugins.RegisterCommand("groupaddgroupchat", "Add groupchat to group", []string{"admin", "owner"}, groupAddGroupChat) - plugins.RegisterCommand("groupdeletegroupchat", "Delete groupchat from group", []string{"admin", "owner"}, groupDeleteGroupChat) - plugins.RegisterCommand("groupadduser", "Add user to group", []string{"admin", "owner"}, groupAddUser) - plugins.RegisterCommand("groupdeleteuser", "Delete user from group", []string{"admin", "owner"}, groupDeleteUser) + plugins.RegisterCommand("grouplist", "Group list", []string{database.Member, database.Admin, database.Owner}, groupList) + plugins.RegisterCommand("groupcreate", "Create group", []string{database.Admin, database.Owner}, groupCreate) + plugins.RegisterCommand("grouprename", "Rename group", []string{database.Admin, database.Owner}, groupRename) + plugins.RegisterCommand("groupdelete", "Delete group", []string{database.Admin, database.Owner}, groupDeleteUndelete) + plugins.RegisterCommand("groupundelete", "Undelete group", []string{database.Admin, database.Owner}, groupDeleteUndelete) + plugins.RegisterCommand("groupaddgroupchat", "Add groupchat to group", []string{database.Admin, database.Owner}, groupAddGroupChat) + plugins.RegisterCommand("groupdeletegroupchat", "Delete groupchat from group", []string{database.Admin, database.Owner}, groupDeleteGroupChat) + plugins.RegisterCommand("groupadduser", "Add user to group", []string{database.Admin, database.Owner}, groupAddUser) + plugins.RegisterCommand("groupdeleteuser", "Delete user from group", []string{database.Admin, database.Owner}, groupDeleteUser) } func (m *Plugin) OnStop() { diff --git a/plugins/me/me.go b/plugins/me/me.go index cec7d6d..dd55f9a 100644 --- a/plugins/me/me.go +++ b/plugins/me/me.go @@ -6,7 +6,6 @@ import ( database "github.com/ad/corpobot/db" "github.com/ad/corpobot/plugins" "github.com/ad/corpobot/telegram" - dlog "github.com/amoghe/distillog" tgbotapi "gopkg.in/telegram-bot-api.v4" ) @@ -22,7 +21,7 @@ func (m *Plugin) OnStart() { return } - plugins.RegisterCommand("me", "Your ID/username", []string{"new", "member", "admin", "owner"}, me) + plugins.RegisterCommand("me", "Your ID/username", []string{database.New, database.Member, database.Admin, database.Owner}, me) } func (m *Plugin) OnStop() { diff --git a/plugins/meetingroom/meetingroom.go b/plugins/meetingroom/meetingroom.go new file mode 100644 index 0000000..4b93885 --- /dev/null +++ b/plugins/meetingroom/meetingroom.go @@ -0,0 +1,587 @@ +package meetingroom + +import ( + "strconv" + "strings" + "time" + + cal "github.com/ad/corpobot/calendar" + "github.com/ad/corpobot/clock" + database "github.com/ad/corpobot/db" + "github.com/ad/corpobot/plugins" + "github.com/ad/corpobot/telegram" + dlog "github.com/amoghe/distillog" + "github.com/araddon/dateparse" + tgbotapi "gopkg.in/telegram-bot-api.v4" +) + +type Plugin struct{} + +func init() { + plugins.RegisterPlugin(&Plugin{}) +} + +func (m *Plugin) OnStart() { + if !plugins.CheckIfPluginDisabled("meetingroom.Plugin", "enabled") { + return + } + + err := database.ExecSQL(plugins.DB, `CREATE TABLE IF NOT EXISTS meetingrooms ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, + "state" VARCHAR(32) NOT NULL, + "created_at" timestamp DEFAULT CURRENT_TIMESTAMP, + "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "meetingroom_name" UNIQUE ("name") ON CONFLICT IGNORE + ); + CREATE TRIGGER IF NOT EXISTS meetingrooms_updated_at_Trigger + AFTER UPDATE On meetingrooms + BEGIN + UPDATE meetingrooms SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') WHERE id = NEW.id; + END; + + CREATE TABLE IF NOT EXISTS meetingroom_schedule ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "meetingroom_id" INTEGER NOT NULL, + "start" timestamp NOT NULL, + "end" timestamp NOT NULL, + "creator" integer NOT NULL, + "created_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "meeting_room_id" FOREIGN KEY ("meetingroom_id") REFERENCES "meetingrooms" ("id") ON DELETE CASCADE, + CONSTRAINT "meeting_room_creator_id" FOREIGN KEY ("creator") REFERENCES "users" ("id") ON DELETE CASCADE, + CONSTRAINT "uniq_schedule_start" UNIQUE ("meetingroom_id", "start") ON CONFLICT IGNORE, + CONSTRAINT "uniq_schedule_end" UNIQUE ("meetingroom_id", "end") ON CONFLICT IGNORE + ); + CREATE TRIGGER IF NOT EXISTS meetingroom_schedule_updated_at_trigger + AFTER UPDATE ON meetingroom_schedule + BEGIN + UPDATE meetingroom_schedule SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') WHERE id = NEW.id; + END; + + CREATE TRIGGER IF NOT EXISTS validate_meetingroom_schedule_trigger + BEFORE INSERT ON meetingroom_schedule + BEGIN + SELECT + CASE + WHEN + NEW.start < CURRENT_TIMESTAMP THEN + RAISE ( ABORT, 'Invalid start, less than now' ) + END; + SELECT + CASE + WHEN + NEW.end <= CURRENT_TIMESTAMP THEN + RAISE ( ABORT, 'Invalid end, less than now' ) + END; + SELECT + CASE + WHEN + NEW.end <= NEW.start THEN + RAISE ( ABORT, 'Invalid end, less than start' ) + END; + END;`) + + if err != nil { + dlog.Errorf("%s", err) + } + + plugins.RegisterCommand("meetingroomcreate", "Add meetingroom", []string{database.Admin, database.Owner}, meetingroomCreate) + plugins.RegisterCommand("meetingroomlist", "Return list of meetingrooms", []string{database.Member, database.Admin, database.Owner}, meetingroomList) + plugins.RegisterCommand("meetingroomrename", "Rename meetingrooms", []string{database.Member, database.Admin, database.Owner}, meetingroomRename) + plugins.RegisterCommand("meetingroomdelete", "Delete meetingroom", []string{database.Admin, database.Owner}, meetingroomDelete) + plugins.RegisterCommand("meetingroomblock", "Block meetingroom", []string{database.Admin, database.Owner}, meetingroomBlock) + plugins.RegisterCommand("meetingroomactivate", "Activate meetingroom", []string{database.Admin, database.Owner}, meetingroomActivate) + + plugins.RegisterCommand("meetingroomschedule", "Return schedule of meetingroom", []string{database.Member, database.Admin, database.Owner}, meetingroomSchedule) + plugins.RegisterCommand("meetingroomscheduleinfo", "Return schedule info", []string{database.Member, database.Admin, database.Owner}, meetingroomScheduleInfo) + plugins.RegisterCommand("meetingroombook", "Book meetingroom", []string{database.Member, database.Admin, database.Owner}, meetingroomBookUnbook) + plugins.RegisterCommand("meetingroomunbook", "Unbook meetingroom", []string{database.Member, database.Admin, database.Owner}, meetingroomBookUnbook) +} + +func (m *Plugin) OnStop() { + dlog.Debugln("[meetingroom.Plugin] Stopped") + + plugins.UnregisterCommand("meetingroomcreate") + plugins.UnregisterCommand("meetingroomlist") + plugins.UnregisterCommand("meetingroomrename") + plugins.UnregisterCommand("meetingroomdelete") + plugins.UnregisterCommand("meetingroomblock") + plugins.UnregisterCommand("meetingroomactivate") + + plugins.UnregisterCommand("meetingroomschedule") + plugins.UnregisterCommand("meetingroomscheduleinfo") + plugins.UnregisterCommand("meetingroombook") + plugins.UnregisterCommand("meetingroomunbook") +} + +var meetingroomList plugins.CommandCallback = func(update *tgbotapi.Update, command, args string, user *database.User) error { + meetingrooms, err := database.GetMeetingrooms(plugins.DB, strings.Fields(args)) + if err != nil { + return err + } + + if len(meetingrooms) == 0 { + return telegram.Send(user.TelegramID, "meetingroom list is empty") + } + + buttons := make([][]tgbotapi.InlineKeyboardButton, 0) + for _, m := range meetingrooms { + buttons = append(buttons, tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("schedule "+m.String(), "/meetingroomschedule "+m.String()))) + buttons = append(buttons, tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("book "+m.String(), "/meetingroombook "+m.String()))) + buttons = append(buttons, tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("unbook "+m.String(), "/meetingroomunbook "+m.String()))) + } + + replyKeyboard := tgbotapi.NewInlineKeyboardMarkup(buttons...) + return telegram.SendCustom(user.TelegramID, 0, "Meetingrooms:", false, &replyKeyboard) +} + +var meetingroomCreate plugins.CommandCallback = func(update *tgbotapi.Update, command, args string, user *database.User) error { + if args == "" { + return telegram.Send(user.TelegramID, "failed: empty meetingroom name") + } + + meetingroom := &database.Meetingroom{ + Name: args, + State: "active", + } + + _, err := database.AddMeetingroomIfNotExist(plugins.DB, meetingroom) + if err != nil { + return telegram.Send(user.TelegramID, "failed: "+err.Error()) + } + + return telegram.Send(user.TelegramID, "meetingroom created") +} + +var meetingroomRename plugins.CommandCallback = func(update *tgbotapi.Update, command, args string, user *database.User) error { + names := strings.Split(args, "\n") + + if len(names) != 2 { + return telegram.Send(user.TelegramID, "failed: you must provide the names of the two meetingrooms with a new line between them") + } + + oldName, newName := strings.TrimSpace(names[0]), strings.TrimSpace(names[1]) + + if oldName == "" || newName == "" { + return telegram.Send(user.TelegramID, "failed: you must provide the names of the two meetingrooms with a new line between them") + } + + rows, err := database.UpdateMeetingroomName(plugins.DB, oldName, newName) + if err != nil { + return err + } + + if rows != 1 { + return telegram.Send(user.TelegramID, "failed") + } + + return telegram.Send(user.TelegramID, "success") +} + +var meetingroomDelete plugins.CommandCallback = func(update *tgbotapi.Update, command, args string, user *database.User) error { + meetingroom := &database.Meetingroom{ + Name: args, + State: "deleted", + } + + rows, err := database.UpdateMeetingroomState(plugins.DB, meetingroom) + if err != nil { + return err + } + + if rows != 1 { + return telegram.Send(user.TelegramID, "failed") + } + + return telegram.Send(user.TelegramID, "success") +} + +var meetingroomBlock plugins.CommandCallback = func(update *tgbotapi.Update, command, args string, user *database.User) error { + meetingroom := &database.Meetingroom{ + Name: args, + State: "blocked", + } + + rows, err := database.UpdateMeetingroomState(plugins.DB, meetingroom) + if err != nil { + return err + } + + if rows != 1 { + return telegram.Send(user.TelegramID, "failed") + } + + return telegram.Send(user.TelegramID, "success") +} +var meetingroomActivate plugins.CommandCallback = func(update *tgbotapi.Update, command, args string, user *database.User) error { + meetingroom := &database.Meetingroom{ + Name: args, + State: "active", + } + + rows, err := database.UpdateMeetingroomState(plugins.DB, meetingroom) + if err != nil { + return err + } + + if rows != 1 { + return telegram.Send(user.TelegramID, "failed") + } + + return telegram.Send(user.TelegramID, "success") +} + +var meetingroomSchedule plugins.CommandCallback = func(update *tgbotapi.Update, command, args string, user *database.User) error { + if args == "" { + meetingrooms, err := database.GetMeetingrooms(plugins.DB, strings.Fields(args)) + if err != nil { + return err + } + + if len(meetingrooms) == 0 { + return telegram.Send(user.TelegramID, "meetingroom list is empty") + } + + buttons := make([][]tgbotapi.InlineKeyboardButton, 0) + + for _, m := range meetingrooms { + buttons = append(buttons, tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData(m.String(), "/meetingroomschedule "+m.String()))) + } + + replyKeyboard := tgbotapi.NewInlineKeyboardMarkup(buttons...) + + return telegram.SendCustom(user.TelegramID, 0, "Choose meetingroom and date to show schedule", false, &replyKeyboard) + } + + var m *database.Meetingroom + params := strings.Split(args, "\n") + if len(params) <= 2 { + var err2 error + m, err2 = database.GetMeetingroomByName(plugins.DB, &database.Meetingroom{Name: params[0]}) + if err2 != nil { + return telegram.Send(user.TelegramID, "Meetingroom not found, try another name or check list /meetingroomlist") + } + } + + dateValue := "" + if len(params) == 2 { + dateValue = params[1] + } + + if m.ID != 0 && dateValue != "" { + schedules, err := database.GetMeetingroomSchedulesByID(plugins.DB, m, dateValue) + if err != nil { + return err + } + + if len(schedules) == 0 { + return telegram.Send(user.TelegramID, "schedule not found") + } + + buttons := make([][]tgbotapi.InlineKeyboardButton, 0) + + for _, s := range schedules { + creator, err := database.GetUserByID(plugins.DB, &database.User{ID: s.Creator}) + if err != nil { + creator = &database.User{ID: s.Creator} + } + buttons = append(buttons, tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData(creator.Short()+": "+s.String(), "/meetingroomschedule "+strconv.FormatInt(s.ID, 10)))) + } + + replyKeyboard := tgbotapi.NewInlineKeyboardMarkup(buttons...) + + return telegram.SendCustom(user.TelegramID, 0, "Schedule for "+params[0]+" "+dateValue, false, &replyKeyboard) + } + + lang := telegram.GetLanguage(update) + + replyKeyboard := tgbotapi.InlineKeyboardMarkup{} + + switch { + case strings.HasPrefix(dateValue, "<"): + date := strings.TrimLeft(dateValue, "<") + year, month, _, err := cal.ParseDate(date) + if err == nil { + replyKeyboard, _, _ = cal.HandlerPrevMonth("/meetingroomschedule "+params[0]+"\n", year, time.Month(month), lang) + } + case strings.HasPrefix(dateValue, ">"): + date := strings.TrimLeft(dateValue, ">") + year, month, _, err := cal.ParseDate(date) + if err == nil { + replyKeyboard, _, _ = cal.HandlerNextMonth("/meetingroomschedule "+params[0]+"\n", year, time.Month(month), lang) + } + case strings.HasPrefix(dateValue, "«"): + date := strings.TrimLeft(dateValue, "«") + year, month, _, err := cal.ParseDate(date) + if err == nil { + replyKeyboard, _, _ = cal.HandlerPrevYear("/meetingroomschedule "+params[0]+"\n", year, time.Month(month), lang) + } + case strings.HasPrefix(dateValue, "»"): + date := strings.TrimLeft(dateValue, "»") + year, month, _, err := cal.ParseDate(date) + if err == nil { + replyKeyboard, _, _ = cal.HandlerNextYear("/meetingroomschedule "+params[0]+"\n", year, time.Month(month), lang) + } + case strings.HasPrefix(dateValue, "m"): + currentTime := time.Now() + year := currentTime.Year() + month := currentTime.Month() + + date := strings.TrimLeft(dateValue, "m") + year2, month2, _, err := cal.ParseDate(date) + if err == nil { + year = year2 + month = time.Month(month2) + } + replyKeyboard = cal.GenerateMonths("/meetingroomschedule "+params[0]+"\n", year, month, lang) + case strings.HasPrefix(dateValue, "y"): + currentTime := time.Now() + year := currentTime.Year() + month := currentTime.Month() + + date := strings.TrimLeft(dateValue, "y") + year2, month2, _, err := cal.ParseDate(date) + if err == nil { + year = year2 + month = time.Month(month2) + } + replyKeyboard = cal.GenerateYears("/meetingroomschedule "+params[0]+"\n", year, month, lang) + default: + currentTime := time.Now() + year := currentTime.Year() + month := currentTime.Month() + + year2, month2, _, err := cal.ParseDate(dateValue) + if err == nil { + year = year2 + month = time.Month(month2) + } + replyKeyboard = cal.GenerateCalendar("/meetingroomschedule "+params[0]+"\n", year, month, lang) + } + + if update.CallbackQuery != nil { + _, err := plugins.Bot.AnswerCallbackQuery(tgbotapi.NewCallback(update.CallbackQuery.ID, "")) + if err != nil { + dlog.Errorln(err.Error()) + } + + edit := tgbotapi.EditMessageReplyMarkupConfig{ + BaseEdit: tgbotapi.BaseEdit{ + ChatID: update.CallbackQuery.Message.Chat.ID, + MessageID: update.CallbackQuery.Message.MessageID, + ReplyMarkup: &replyKeyboard, + }, + } + + _, err = plugins.Bot.Send(edit) + return err + } + + return telegram.SendCustom(user.TelegramID, 0, "Choose date to show schedule for "+params[0], false, &replyKeyboard) +} + +var meetingroomScheduleInfo plugins.CommandCallback = func(update *tgbotapi.Update, command, args string, user *database.User) error { + if args == "" { + meetingrooms, err := database.GetMeetingrooms(plugins.DB, []string{}) + if err != nil { + return err + } + + if len(meetingrooms) == 0 { + return telegram.Send(user.TelegramID, "meetingroom list is empty, /meetingroomcreate") + } + + buttons := make([][]tgbotapi.InlineKeyboardButton, 0) + + for _, m := range meetingrooms { + buttons = append(buttons, tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData(m.String(), "/meetingroomscheduleinfo "+m.String()))) + } + + replyKeyboard := tgbotapi.NewInlineKeyboardMarkup(buttons...) + + return telegram.SendCustom(user.TelegramID, 0, "Choose meetingroom and date to show schedule", false, &replyKeyboard) + } + + scheduleID, err := strconv.ParseInt(args, 10, 64) + if err != nil { + return telegram.Send(user.TelegramID, err.Error()) + } + + s, err := database.GetMeetingroomScheduleByID(plugins.DB, &database.MeetingroomSchedule{ID: scheduleID}) + if err != nil { + return telegram.Send(user.TelegramID, "Schedule not found, try another id "+err.Error()) + } + + buttons := make([][]tgbotapi.InlineKeyboardButton, 0) + + creator, err := database.GetUserByID(plugins.DB, &database.User{ID: s.Creator}) + if err != nil { + creator = &database.User{ID: s.Creator} + } + buttons = append(buttons, tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData(creator.Short()+": "+s.String(), "/meetingroomscheduleinfo "+strconv.FormatInt(s.ID, 10)))) + buttons = append(buttons, tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("unbook", "/meetingroomunbook "+strconv.FormatInt(s.ID, 10)))) + + replyKeyboard := tgbotapi.NewInlineKeyboardMarkup(buttons...) + + return telegram.SendCustom(user.TelegramID, 0, "Schedule info", false, &replyKeyboard) +} + +var meetingroomBookUnbook plugins.CommandCallback = func(update *tgbotapi.Update, command, args string, user *database.User) error { + isBook := true + if command == "meetingroomunbook" { + isBook = false + } + + if args == "" { + meetingrooms, err := database.GetMeetingrooms(plugins.DB, strings.Fields(args)) + if err != nil { + return err + } + + if len(meetingrooms) == 0 { + return telegram.Send(user.TelegramID, "meetingroom list is empty, /meetingroomcreate") + } + + buttons := make([][]tgbotapi.InlineKeyboardButton, 0) + + for _, m := range meetingrooms { + buttons = append(buttons, tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData(m.String(), "/"+command+" "+m.String()))) + } + + replyKeyboard := tgbotapi.NewInlineKeyboardMarkup(buttons...) + + return telegram.SendCustom(user.TelegramID, 0, "Choose meetingroom, date, start and end time to add schedule", false, &replyKeyboard) + } + + var m *database.Meetingroom + params := strings.SplitN(args, "\n", 3) + var err2 error + m, err2 = database.GetMeetingroomByName(plugins.DB, &database.Meetingroom{Name: params[0]}) + if err2 != nil { + return telegram.Send(user.TelegramID, "Meetingroom not found, try another name or check list /meetingroomlist") + } + + // показать выбор первого значения + // ... + + var startValue time.Time + if len(params) > 1 { + start, err := dateparse.ParseAny(params[1]) + if err == nil { + startValue = start.Truncate(1 * time.Minute) + } + } + + // показать выбор второго значения + // ... + + var endValue time.Time + if len(params) == 3 { + end, err := dateparse.ParseAny(params[2]) + if err == nil { + endValue = end.Truncate(1 * time.Minute) + } + } + + if m.ID != 0 && !startValue.IsZero() && !endValue.IsZero() { + if startValue.After(endValue) || startValue.Equal(endValue) { + return telegram.Send(user.TelegramID, "start must come before the end") + } + + creator, err := database.GetUserByTelegramID(plugins.DB, &database.User{TelegramID: user.TelegramID}) + if err != nil { + return telegram.Send(user.TelegramID, "failed: "+err.Error()) + } + + meetingroomSchedule := &database.MeetingroomSchedule{ + MeetingroomID: m.ID, + Creator: creator.ID, + Start: startValue, + End: endValue, + } + + // book + if isBook { + ms, err3 := database.AddSchedule(plugins.DB, meetingroomSchedule) + if err3 != nil { + if err3.Error() == database.MeetingroomBusy { + buttons := make([][]tgbotapi.InlineKeyboardButton, 0) + + creator, err := database.GetUserByID(plugins.DB, &database.User{ID: ms.Creator}) + if err != nil { + creator = &database.User{ID: ms.Creator} + } + buttons = append(buttons, tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData(creator.Short()+": "+ms.String(), "/meetingroomscheduleinfo "+strconv.FormatInt(ms.ID, 10)))) + buttons = append(buttons, tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("unbook", "/meetingroomunbook "+strconv.FormatInt(ms.ID, 10)))) + + replyKeyboard := tgbotapi.NewInlineKeyboardMarkup(buttons...) + + return telegram.SendCustom(user.TelegramID, 0, "Meetingroom is busy", false, &replyKeyboard) + } + + return telegram.Send(user.TelegramID, "failed: "+err3.Error()+"\n"+ms.String()) + } + buttons := make([][]tgbotapi.InlineKeyboardButton, 0) + buttons = append(buttons, tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("unbook", "/meetingroomunbook "+strconv.FormatInt(ms.ID, 10)))) + replyKeyboard := tgbotapi.NewInlineKeyboardMarkup(buttons...) + + return telegram.SendCustom(user.TelegramID, 0, "Meetingroom booked from "+startValue.Format("2006.01.02 15:04")+" to "+endValue.Format("2006.01.02 15:04"), false, &replyKeyboard) + } + + // unbook + _, err4 := database.RemoveSchedule(plugins.DB, meetingroomSchedule) + if err4 != nil { + if err4.Error() == database.MeetingroomBusy { + buttons := make([][]tgbotapi.InlineKeyboardButton, 0) + + buttons = append(buttons, tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("book", "/meetingroombook "+params[0]))) + + replyKeyboard := tgbotapi.NewInlineKeyboardMarkup(buttons...) + + return telegram.SendCustom(user.TelegramID, 0, "Meetingroom is busy", false, &replyKeyboard) + } + + return telegram.Send(user.TelegramID, "failed: "+err4.Error()) + } + buttons := make([][]tgbotapi.InlineKeyboardButton, 0) + buttons = append(buttons, tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("book", "/meetingroombook "+params[0]))) + replyKeyboard := tgbotapi.NewInlineKeyboardMarkup(buttons...) + + return telegram.SendCustom(user.TelegramID, 0, "Meetingroom unbooked from "+startValue.Format("2006.01.02 15:04")+" to "+endValue.Format("2006.01.02 15:04"), false, &replyKeyboard) + } + + lang := telegram.GetLanguage(update) + + // show calendar for start + if startValue.IsZero() { + replyKeyboard := clock.GenerateClock("/meetingroombook "+params[0]+"\n", -1, -1, lang) + + if update.CallbackQuery != nil { + _, err := plugins.Bot.AnswerCallbackQuery(tgbotapi.NewCallback(update.CallbackQuery.ID, "")) + if err != nil { + dlog.Errorln(err.Error()) + } + + edit := tgbotapi.EditMessageReplyMarkupConfig{ + BaseEdit: tgbotapi.BaseEdit{ + ChatID: update.CallbackQuery.Message.Chat.ID, + MessageID: update.CallbackQuery.Message.MessageID, + ReplyMarkup: &replyKeyboard, + }, + } + + _, err = plugins.Bot.Send(edit) + return err + } + + return telegram.SendCustom(user.TelegramID, 0, "Choose", false, &replyKeyboard) + } + + // show calendar for end + if endValue.IsZero() { + return telegram.Send(user.TelegramID, "end") + } + + return telegram.Send(user.TelegramID, "not implemented") +} diff --git a/plugins/messages/messages.go b/plugins/messages/messages.go index b7fdf6c..decbeff 100644 --- a/plugins/messages/messages.go +++ b/plugins/messages/messages.go @@ -25,8 +25,8 @@ func (m *Plugin) OnStart() { return } - plugins.RegisterCommand("broadcast", "Send message to all users", []string{"admin", "owner"}, broadcast) - plugins.RegisterCommand("message", "Send message to user", []string{"admin", "owner"}, message) + plugins.RegisterCommand("broadcast", "Send message to all users", []string{database.Admin, database.Owner}, broadcast) + plugins.RegisterCommand("message", "Send message to user", []string{database.Admin, database.Owner}, message) } func (m *Plugin) OnStop() { diff --git a/plugins/starthelp/starthelp.go b/plugins/starthelp/starthelp.go index 25d1160..f46e9ab 100644 --- a/plugins/starthelp/starthelp.go +++ b/plugins/starthelp/starthelp.go @@ -22,8 +22,8 @@ func (m *Plugin) OnStart() { return } - plugins.RegisterCommand("start", "Bot /start command", []string{"new", "member", "admin", "owner"}, start) - plugins.RegisterCommand("help", "Display this help", []string{"new", "member", "admin", "owner"}, help) + plugins.RegisterCommand("start", "Bot /start command", []string{database.New, database.Member, database.Admin, database.Owner}, start) + plugins.RegisterCommand("help", "Display this help", []string{database.New, database.Member, database.Admin, database.Owner}, help) } func (m *Plugin) OnStop() { diff --git a/telegram/telegram.go b/telegram/telegram.go index f27c73f..58eaeb2 100644 --- a/telegram/telegram.go +++ b/telegram/telegram.go @@ -93,7 +93,7 @@ func ProcessTelegramMessages(db *sql.DB, bot *tgbotapi.BotAPI, updates tgbotapi. user, errAddUser := database.AddUserIfNotExist(db, user) if errAddUser == nil { - users, errGetUsers := database.GetUsers(db, []string{"admin", "owner"}) + users, errGetUsers := database.GetUsers(db, []string{database.Admin, database.Owner}) if errGetUsers == nil { newUserMessage := "New user registered: " + user.String() replyKeyboard := tgbotapi.NewInlineKeyboardMarkup(tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("user info", "/user "+strconv.FormatInt(user.TelegramID, 10)))) diff --git a/vendor/github.com/araddon/dateparse/.travis.yml b/vendor/github.com/araddon/dateparse/.travis.yml new file mode 100644 index 0000000..3b4b177 --- /dev/null +++ b/vendor/github.com/araddon/dateparse/.travis.yml @@ -0,0 +1,13 @@ +language: go + +go: + - 1.13.x + +before_install: + - go get -t -v ./... + +script: + - go test -race -coverprofile=coverage.txt -covermode=atomic + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/vendor/github.com/araddon/dateparse/LICENSE b/vendor/github.com/araddon/dateparse/LICENSE new file mode 100644 index 0000000..f675ed3 --- /dev/null +++ b/vendor/github.com/araddon/dateparse/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-2017 Aaron Raddon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/araddon/dateparse/README.md b/vendor/github.com/araddon/dateparse/README.md new file mode 100644 index 0000000..fe682dd --- /dev/null +++ b/vendor/github.com/araddon/dateparse/README.md @@ -0,0 +1,323 @@ +Go Date Parser +--------------------------- + +Parse many date strings without knowing format in advance. Uses a scanner to read bytes and use a state machine to find format. Much faster than shotgun based parse methods. See [bench_test.go](https://github.com/araddon/dateparse/blob/master/bench_test.go) for performance comparison. + + +[![Code Coverage](https://codecov.io/gh/araddon/dateparse/branch/master/graph/badge.svg)](https://codecov.io/gh/araddon/dateparse) +[![GoDoc](https://godoc.org/github.com/araddon/dateparse?status.svg)](http://godoc.org/github.com/araddon/dateparse) +[![Build Status](https://travis-ci.org/araddon/dateparse.svg?branch=master)](https://travis-ci.org/araddon/dateparse) +[![Go ReportCard](https://goreportcard.com/badge/araddon/dateparse)](https://goreportcard.com/report/araddon/dateparse) + +**MM/DD/YYYY VS DD/MM/YYYY** Right now this uses mm/dd/yyyy WHEN ambiguous if this is not desired behavior, use `ParseStrict` which will fail on ambiguous date strings. + +**Timezones** The location your server is configured affects the results! See example or https://play.golang.org/p/IDHRalIyXh and last paragraph here https://golang.org/pkg/time/#Parse. + + +```go + +// Normal parse. Equivalent Timezone rules as time.Parse() +t, err := dateparse.ParseAny("3/1/2014") + +// Parse Strict, error on ambigous mm/dd vs dd/mm dates +t, err := dateparse.ParseStrict("3/1/2014") +> returns error + +// Return a string that represents the layout to parse the given date-time. +layout, err := dateparse.ParseFormat("May 8, 2009 5:57:51 PM") +> "Jan 2, 2006 3:04:05 PM" + +``` + +cli tool for testing dateformats +---------------------------------- + +[Date Parse CLI](https://github.com/araddon/dateparse/blob/master/dateparse) + + +Extended example +------------------- + +https://github.com/araddon/dateparse/blob/master/example/main.go + +```go +package main + +import ( + "flag" + "fmt" + "time" + + "github.com/scylladb/termtables" + "github.com/araddon/dateparse" +) + +var examples = []string{ + "May 8, 2009 5:57:51 PM", + "oct 7, 1970", + "oct 7, '70", + "oct. 7, 1970", + "oct. 7, 70", + "Mon Jan 2 15:04:05 2006", + "Mon Jan 2 15:04:05 MST 2006", + "Mon Jan 02 15:04:05 -0700 2006", + "Monday, 02-Jan-06 15:04:05 MST", + "Mon, 02 Jan 2006 15:04:05 MST", + "Tue, 11 Jul 2017 16:28:13 +0200 (CEST)", + "Mon, 02 Jan 2006 15:04:05 -0700", + "Mon 30 Sep 2018 09:09:09 PM UTC", + "Mon Aug 10 15:44:11 UTC+0100 2015", + "Thu, 4 Jan 2018 17:53:36 +0000", + "Fri Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time)", + "Sun, 3 Jan 2021 00:12:23 +0800 (GMT+08:00)", + "September 17, 2012 10:09am", + "September 17, 2012 at 10:09am PST-08", + "September 17, 2012, 10:10:09", + "October 7, 1970", + "October 7th, 1970", + "12 Feb 2006, 19:17", + "12 Feb 2006 19:17", + "14 May 2019 19:11:40.164", + "7 oct 70", + "7 oct 1970", + "03 February 2013", + "1 July 2013", + "2013-Feb-03", + // dd/Mon/yyy alpha Months + "06/Jan/2008:15:04:05 -0700", + "06/Jan/2008 15:04:05 -0700", + // mm/dd/yy + "3/31/2014", + "03/31/2014", + "08/21/71", + "8/1/71", + "4/8/2014 22:05", + "04/08/2014 22:05", + "4/8/14 22:05", + "04/2/2014 03:00:51", + "8/8/1965 12:00:00 AM", + "8/8/1965 01:00:01 PM", + "8/8/1965 01:00 PM", + "8/8/1965 1:00 PM", + "8/8/1965 12:00 AM", + "4/02/2014 03:00:51", + "03/19/2012 10:11:59", + "03/19/2012 10:11:59.3186369", + // yyyy/mm/dd + "2014/3/31", + "2014/03/31", + "2014/4/8 22:05", + "2014/04/08 22:05", + "2014/04/2 03:00:51", + "2014/4/02 03:00:51", + "2012/03/19 10:11:59", + "2012/03/19 10:11:59.3186369", + // yyyy:mm:dd + "2014:3:31", + "2014:03:31", + "2014:4:8 22:05", + "2014:04:08 22:05", + "2014:04:2 03:00:51", + "2014:4:02 03:00:51", + "2012:03:19 10:11:59", + "2012:03:19 10:11:59.3186369", + // Chinese + "2014年04月08日", + // yyyy-mm-ddThh + "2006-01-02T15:04:05+0000", + "2009-08-12T22:15:09-07:00", + "2009-08-12T22:15:09", + "2009-08-12T22:15:09.988", + "2009-08-12T22:15:09Z", + "2017-07-19T03:21:51:897+0100", + "2019-05-29T08:41-04", // no seconds, 2 digit TZ offset + // yyyy-mm-dd hh:mm:ss + "2014-04-26 17:24:37.3186369", + "2012-08-03 18:31:59.257000000", + "2014-04-26 17:24:37.123", + "2013-04-01 22:43", + "2013-04-01 22:43:22", + "2014-12-16 06:20:00 UTC", + "2014-12-16 06:20:00 GMT", + "2014-04-26 05:24:37 PM", + "2014-04-26 13:13:43 +0800", + "2014-04-26 13:13:43 +0800 +08", + "2014-04-26 13:13:44 +09:00", + "2012-08-03 18:31:59.257000000 +0000 UTC", + "2015-09-30 18:48:56.35272715 +0000 UTC", + "2015-02-18 00:12:00 +0000 GMT", + "2015-02-18 00:12:00 +0000 UTC", + "2015-02-08 03:02:00 +0300 MSK m=+0.000000001", + "2015-02-08 03:02:00.001 +0300 MSK m=+0.000000001", + "2017-07-19 03:21:51+00:00", + "2014-04-26", + "2014-04", + "2014", + "2014-05-11 08:20:13,787", + // yyyy-mm-dd-07:00 + "2020-07-20+08:00", + // mm.dd.yy + "3.31.2014", + "03.31.2014", + "08.21.71", + "2014.03", + "2014.03.30", + // yyyymmdd and similar + "20140601", + "20140722105203", + // yymmdd hh:mm:yy mysql log + // 080313 05:21:55 mysqld started + "171113 14:14:20", + // unix seconds, ms, micro, nano + "1332151919", + "1384216367189", + "1384216367111222", + "1384216367111222333", +} + +var ( + timezone = "" +) + +func main() { + flag.StringVar(&timezone, "timezone", "UTC", "Timezone aka `America/Los_Angeles` formatted time-zone") + flag.Parse() + + if timezone != "" { + // NOTE: This is very, very important to understand + // time-parsing in go + loc, err := time.LoadLocation(timezone) + if err != nil { + panic(err.Error()) + } + time.Local = loc + } + + table := termtables.CreateTable() + + table.AddHeaders("Input", "Parsed, and Output as %v") + for _, dateExample := range examples { + t, err := dateparse.ParseLocal(dateExample) + if err != nil { + panic(err.Error()) + } + table.AddRow(dateExample, fmt.Sprintf("%v", t)) + } + fmt.Println(table.Render()) +} + +/* ++-------------------------------------------------------+-----------------------------------------+ +| Input | Parsed, and Output as %v | ++-------------------------------------------------------+-----------------------------------------+ +| May 8, 2009 5:57:51 PM | 2009-05-08 17:57:51 +0000 UTC | +| oct 7, 1970 | 1970-10-07 00:00:00 +0000 UTC | +| oct 7, '70 | 1970-10-07 00:00:00 +0000 UTC | +| oct. 7, 1970 | 1970-10-07 00:00:00 +0000 UTC | +| oct. 7, 70 | 1970-10-07 00:00:00 +0000 UTC | +| Mon Jan 2 15:04:05 2006 | 2006-01-02 15:04:05 +0000 UTC | +| Mon Jan 2 15:04:05 MST 2006 | 2006-01-02 15:04:05 +0000 MST | +| Mon Jan 02 15:04:05 -0700 2006 | 2006-01-02 15:04:05 -0700 -0700 | +| Monday, 02-Jan-06 15:04:05 MST | 2006-01-02 15:04:05 +0000 MST | +| Mon, 02 Jan 2006 15:04:05 MST | 2006-01-02 15:04:05 +0000 MST | +| Tue, 11 Jul 2017 16:28:13 +0200 (CEST) | 2017-07-11 16:28:13 +0200 +0200 | +| Mon, 02 Jan 2006 15:04:05 -0700 | 2006-01-02 15:04:05 -0700 -0700 | +| Mon 30 Sep 2018 09:09:09 PM UTC | 2018-09-30 21:09:09 +0000 UTC | +| Mon Aug 10 15:44:11 UTC+0100 2015 | 2015-08-10 15:44:11 +0000 UTC | +| Thu, 4 Jan 2018 17:53:36 +0000 | 2018-01-04 17:53:36 +0000 UTC | +| Fri Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time) | 2015-07-03 18:04:07 +0100 GMT | +| Sun, 3 Jan 2021 00:12:23 +0800 (GMT+08:00) | 2021-01-03 00:12:23 +0800 +0800 | +| September 17, 2012 10:09am | 2012-09-17 10:09:00 +0000 UTC | +| September 17, 2012 at 10:09am PST-08 | 2012-09-17 10:09:00 -0800 PST | +| September 17, 2012, 10:10:09 | 2012-09-17 10:10:09 +0000 UTC | +| October 7, 1970 | 1970-10-07 00:00:00 +0000 UTC | +| October 7th, 1970 | 1970-10-07 00:00:00 +0000 UTC | +| 12 Feb 2006, 19:17 | 2006-02-12 19:17:00 +0000 UTC | +| 12 Feb 2006 19:17 | 2006-02-12 19:17:00 +0000 UTC | +| 14 May 2019 19:11:40.164 | 2019-05-14 19:11:40.164 +0000 UTC | +| 7 oct 70 | 1970-10-07 00:00:00 +0000 UTC | +| 7 oct 1970 | 1970-10-07 00:00:00 +0000 UTC | +| 03 February 2013 | 2013-02-03 00:00:00 +0000 UTC | +| 1 July 2013 | 2013-07-01 00:00:00 +0000 UTC | +| 2013-Feb-03 | 2013-02-03 00:00:00 +0000 UTC | +| 06/Jan/2008:15:04:05 -0700 | 2008-01-06 15:04:05 -0700 -0700 | +| 06/Jan/2008 15:04:05 -0700 | 2008-01-06 15:04:05 -0700 -0700 | +| 3/31/2014 | 2014-03-31 00:00:00 +0000 UTC | +| 03/31/2014 | 2014-03-31 00:00:00 +0000 UTC | +| 08/21/71 | 1971-08-21 00:00:00 +0000 UTC | +| 8/1/71 | 1971-08-01 00:00:00 +0000 UTC | +| 4/8/2014 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 04/08/2014 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 4/8/14 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 04/2/2014 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | +| 8/8/1965 12:00:00 AM | 1965-08-08 00:00:00 +0000 UTC | +| 8/8/1965 01:00:01 PM | 1965-08-08 13:00:01 +0000 UTC | +| 8/8/1965 01:00 PM | 1965-08-08 13:00:00 +0000 UTC | +| 8/8/1965 1:00 PM | 1965-08-08 13:00:00 +0000 UTC | +| 8/8/1965 12:00 AM | 1965-08-08 00:00:00 +0000 UTC | +| 4/02/2014 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | +| 03/19/2012 10:11:59 | 2012-03-19 10:11:59 +0000 UTC | +| 03/19/2012 10:11:59.3186369 | 2012-03-19 10:11:59.3186369 +0000 UTC | +| 2014/3/31 | 2014-03-31 00:00:00 +0000 UTC | +| 2014/03/31 | 2014-03-31 00:00:00 +0000 UTC | +| 2014/4/8 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 2014/04/08 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 2014/04/2 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | +| 2014/4/02 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | +| 2012/03/19 10:11:59 | 2012-03-19 10:11:59 +0000 UTC | +| 2012/03/19 10:11:59.3186369 | 2012-03-19 10:11:59.3186369 +0000 UTC | +| 2014:3:31 | 2014-03-31 00:00:00 +0000 UTC | +| 2014:03:31 | 2014-03-31 00:00:00 +0000 UTC | +| 2014:4:8 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 2014:04:08 22:05 | 2014-04-08 22:05:00 +0000 UTC | +| 2014:04:2 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | +| 2014:4:02 03:00:51 | 2014-04-02 03:00:51 +0000 UTC | +| 2012:03:19 10:11:59 | 2012-03-19 10:11:59 +0000 UTC | +| 2012:03:19 10:11:59.3186369 | 2012-03-19 10:11:59.3186369 +0000 UTC | +| 2014年04月08日 | 2014-04-08 00:00:00 +0000 UTC | +| 2006-01-02T15:04:05+0000 | 2006-01-02 15:04:05 +0000 UTC | +| 2009-08-12T22:15:09-07:00 | 2009-08-12 22:15:09 -0700 -0700 | +| 2009-08-12T22:15:09 | 2009-08-12 22:15:09 +0000 UTC | +| 2009-08-12T22:15:09.988 | 2009-08-12 22:15:09.988 +0000 UTC | +| 2009-08-12T22:15:09Z | 2009-08-12 22:15:09 +0000 UTC | +| 2017-07-19T03:21:51:897+0100 | 2017-07-19 03:21:51.897 +0100 +0100 | +| 2019-05-29T08:41-04 | 2019-05-29 08:41:00 -0400 -0400 | +| 2014-04-26 17:24:37.3186369 | 2014-04-26 17:24:37.3186369 +0000 UTC | +| 2012-08-03 18:31:59.257000000 | 2012-08-03 18:31:59.257 +0000 UTC | +| 2014-04-26 17:24:37.123 | 2014-04-26 17:24:37.123 +0000 UTC | +| 2013-04-01 22:43 | 2013-04-01 22:43:00 +0000 UTC | +| 2013-04-01 22:43:22 | 2013-04-01 22:43:22 +0000 UTC | +| 2014-12-16 06:20:00 UTC | 2014-12-16 06:20:00 +0000 UTC | +| 2014-12-16 06:20:00 GMT | 2014-12-16 06:20:00 +0000 UTC | +| 2014-04-26 05:24:37 PM | 2014-04-26 17:24:37 +0000 UTC | +| 2014-04-26 13:13:43 +0800 | 2014-04-26 13:13:43 +0800 +0800 | +| 2014-04-26 13:13:43 +0800 +08 | 2014-04-26 13:13:43 +0800 +0800 | +| 2014-04-26 13:13:44 +09:00 | 2014-04-26 13:13:44 +0900 +0900 | +| 2012-08-03 18:31:59.257000000 +0000 UTC | 2012-08-03 18:31:59.257 +0000 UTC | +| 2015-09-30 18:48:56.35272715 +0000 UTC | 2015-09-30 18:48:56.35272715 +0000 UTC | +| 2015-02-18 00:12:00 +0000 GMT | 2015-02-18 00:12:00 +0000 UTC | +| 2015-02-18 00:12:00 +0000 UTC | 2015-02-18 00:12:00 +0000 UTC | +| 2015-02-08 03:02:00 +0300 MSK m=+0.000000001 | 2015-02-08 03:02:00 +0300 +0300 | +| 2015-02-08 03:02:00.001 +0300 MSK m=+0.000000001 | 2015-02-08 03:02:00.001 +0300 +0300 | +| 2017-07-19 03:21:51+00:00 | 2017-07-19 03:21:51 +0000 UTC | +| 2014-04-26 | 2014-04-26 00:00:00 +0000 UTC | +| 2014-04 | 2014-04-01 00:00:00 +0000 UTC | +| 2014 | 2014-01-01 00:00:00 +0000 UTC | +| 2014-05-11 08:20:13,787 | 2014-05-11 08:20:13.787 +0000 UTC | +| 2020-07-20+08:00 | 2020-07-20 00:00:00 +0800 +0800 | +| 3.31.2014 | 2014-03-31 00:00:00 +0000 UTC | +| 03.31.2014 | 2014-03-31 00:00:00 +0000 UTC | +| 08.21.71 | 1971-08-21 00:00:00 +0000 UTC | +| 2014.03 | 2014-03-01 00:00:00 +0000 UTC | +| 2014.03.30 | 2014-03-30 00:00:00 +0000 UTC | +| 20140601 | 2014-06-01 00:00:00 +0000 UTC | +| 20140722105203 | 2014-07-22 10:52:03 +0000 UTC | +| 171113 14:14:20 | 2017-11-13 14:14:20 +0000 UTC | +| 1332151919 | 2012-03-19 10:11:59 +0000 UTC | +| 1384216367189 | 2013-11-12 00:32:47.189 +0000 UTC | +| 1384216367111222 | 2013-11-12 00:32:47.111222 +0000 UTC | +| 1384216367111222333 | 2013-11-12 00:32:47.111222333 +0000 UTC | ++-------------------------------------------------------+-----------------------------------------+ +*/ + +``` diff --git a/vendor/github.com/araddon/dateparse/go.mod b/vendor/github.com/araddon/dateparse/go.mod new file mode 100644 index 0000000..071cd5e --- /dev/null +++ b/vendor/github.com/araddon/dateparse/go.mod @@ -0,0 +1,9 @@ +module github.com/araddon/dateparse + +go 1.12 + +require ( + github.com/mattn/go-runewidth v0.0.10 // indirect + github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4 + github.com/stretchr/testify v1.7.0 +) diff --git a/vendor/github.com/araddon/dateparse/go.sum b/vendor/github.com/araddon/dateparse/go.sum new file mode 100644 index 0000000..40bf744 --- /dev/null +++ b/vendor/github.com/araddon/dateparse/go.sum @@ -0,0 +1,18 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4 h1:8qmTC5ByIXO3GP/IzBkxcZ/99VITvnIETDhdFz/om7A= +github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/vendor/github.com/araddon/dateparse/parseany.go b/vendor/github.com/araddon/dateparse/parseany.go new file mode 100644 index 0000000..a5c4053 --- /dev/null +++ b/vendor/github.com/araddon/dateparse/parseany.go @@ -0,0 +1,2189 @@ +// Package dateparse parses date-strings without knowing the format +// in advance, using a fast lex based approach to eliminate shotgun +// attempts. It leans towards US style dates when there is a conflict. +package dateparse + +import ( + "fmt" + "strconv" + "strings" + "time" + "unicode" + "unicode/utf8" +) + +// func init() { +// gou.SetupLogging("debug") +// gou.SetColorOutput() +// } + +var days = []string{ + "mon", + "tue", + "wed", + "thu", + "fri", + "sat", + "sun", + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", +} + +var months = []string{ + "january", + "february", + "march", + "april", + "may", + "june", + "july", + "august", + "september", + "october", + "november", + "december", +} + +type dateState uint8 +type timeState uint8 + +const ( + dateStart dateState = iota // 0 + dateDigit + dateDigitSt + dateYearDash + dateYearDashAlphaDash + dateYearDashDash + dateYearDashDashWs // 5 + dateYearDashDashT + dateYearDashDashOffset + dateDigitDash + dateDigitDashAlpha + dateDigitDashAlphaDash // 10 + dateDigitDot + dateDigitDotDot + dateDigitSlash + dateDigitYearSlash + dateDigitSlashAlpha // 15 + dateDigitColon + dateDigitChineseYear + dateDigitChineseYearWs + dateDigitWs + dateDigitWsMoYear // 20 + dateDigitWsMolong + dateAlpha + dateAlphaWs + dateAlphaWsDigit + dateAlphaWsDigitMore // 25 + dateAlphaWsDigitMoreWs + dateAlphaWsDigitMoreWsYear + dateAlphaWsMonth + dateAlphaWsDigitYearmaybe + dateAlphaWsMonthMore + dateAlphaWsMonthSuffix + dateAlphaWsMore + dateAlphaWsAtTime + dateAlphaWsAlpha + dateAlphaWsAlphaYearmaybe // 35 + dateAlphaPeriodWsDigit + dateWeekdayComma + dateWeekdayAbbrevComma +) +const ( + // Time state + timeIgnore timeState = iota // 0 + timeStart + timeWs + timeWsAlpha + timeWsAlphaWs + timeWsAlphaZoneOffset // 5 + timeWsAlphaZoneOffsetWs + timeWsAlphaZoneOffsetWsYear + timeWsAlphaZoneOffsetWsExtra + timeWsAMPMMaybe + timeWsAMPM // 10 + timeWsOffset + timeWsOffsetWs // 12 + timeWsOffsetColonAlpha + timeWsOffsetColon + timeWsYear // 15 + timeOffset + timeOffsetColon + timeAlpha + timePeriod + timePeriodOffset // 20 + timePeriodOffsetColon + timePeriodOffsetColonWs + timePeriodWs + timePeriodWsAlpha + timePeriodWsOffset // 25 + timePeriodWsOffsetWs + timePeriodWsOffsetWsAlpha + timePeriodWsOffsetColon + timePeriodWsOffsetColonAlpha + timeZ + timeZDigit +) + +var ( + // ErrAmbiguousMMDD for date formats such as 04/02/2014 the mm/dd vs dd/mm are + // ambiguous, so it is an error for strict parse rules. + ErrAmbiguousMMDD = fmt.Errorf("This date has ambiguous mm/dd vs dd/mm type format") +) + +func unknownErr(datestr string) error { + return fmt.Errorf("Could not find format for %q", datestr) +} + +// ParseAny parse an unknown date format, detect the layout. +// Normal parse. Equivalent Timezone rules as time.Parse(). +// NOTE: please see readme on mmdd vs ddmm ambiguous dates. +func ParseAny(datestr string, opts ...ParserOption) (time.Time, error) { + p, err := parseTime(datestr, nil, opts...) + if err != nil { + return time.Time{}, err + } + return p.parse() +} + +// ParseIn with Location, equivalent to time.ParseInLocation() timezone/offset +// rules. Using location arg, if timezone/offset info exists in the +// datestring, it uses the given location rules for any zone interpretation. +// That is, MST means one thing when using America/Denver and something else +// in other locations. +func ParseIn(datestr string, loc *time.Location, opts ...ParserOption) (time.Time, error) { + p, err := parseTime(datestr, loc, opts...) + if err != nil { + return time.Time{}, err + } + return p.parse() +} + +// ParseLocal Given an unknown date format, detect the layout, +// using time.Local, parse. +// +// Set Location to time.Local. Same as ParseIn Location but lazily uses +// the global time.Local variable for Location argument. +// +// denverLoc, _ := time.LoadLocation("America/Denver") +// time.Local = denverLoc +// +// t, err := dateparse.ParseLocal("3/1/2014") +// +// Equivalent to: +// +// t, err := dateparse.ParseIn("3/1/2014", denverLoc) +// +func ParseLocal(datestr string, opts ...ParserOption) (time.Time, error) { + p, err := parseTime(datestr, time.Local, opts...) + if err != nil { + return time.Time{}, err + } + return p.parse() +} + +// MustParse parse a date, and panic if it can't be parsed. Used for testing. +// Not recommended for most use-cases. +func MustParse(datestr string, opts ...ParserOption) time.Time { + p, err := parseTime(datestr, nil, opts...) + if err != nil { + panic(err.Error()) + } + t, err := p.parse() + if err != nil { + panic(err.Error()) + } + return t +} + +// ParseFormat parse's an unknown date-time string and returns a layout +// string that can parse this (and exact same format) other date-time strings. +// +// layout, err := dateparse.ParseFormat("2013-02-01 00:00:00") +// // layout = "2006-01-02 15:04:05" +// +func ParseFormat(datestr string, opts ...ParserOption) (string, error) { + p, err := parseTime(datestr, nil, opts...) + if err != nil { + return "", err + } + _, err = p.parse() + if err != nil { + return "", err + } + return string(p.format), nil +} + +// ParseStrict parse an unknown date format. IF the date is ambigous +// mm/dd vs dd/mm then return an error. These return errors: 3.3.2014 , 8/8/71 etc +func ParseStrict(datestr string, opts ...ParserOption) (time.Time, error) { + p, err := parseTime(datestr, nil, opts...) + if err != nil { + return time.Time{}, err + } + if p.ambiguousMD { + return time.Time{}, ErrAmbiguousMMDD + } + return p.parse() +} + +func parseTime(datestr string, loc *time.Location, opts ...ParserOption) (p *parser, err error) { + + p = newParser(datestr, loc, opts...) + if p.retryAmbiguousDateWithSwap { + // month out of range signifies that a day/month swap is the correct solution to an ambiguous date + // this is because it means that a day is being interpreted as a month and overflowing the valid value for that + // by retrying in this case, we can fix a common situation with no assumptions + defer func() { + if p.ambiguousMD { + // if it errors out with the following error, swap before we + // get out of this function to reduce scope it needs to be applied on + _, err := p.parse() + if err != nil && strings.Contains(err.Error(), "month out of range") { + // create the option to reverse the preference + preferMonthFirst := PreferMonthFirst(!p.preferMonthFirst) + // turn off the retry to avoid endless recursion + retryAmbiguousDateWithSwap := RetryAmbiguousDateWithSwap(false) + modifiedOpts := append(opts, preferMonthFirst, retryAmbiguousDateWithSwap) + p, err = parseTime(datestr, time.Local, modifiedOpts...) + } + } + + }() + } + + i := 0 + + // General strategy is to read rune by rune through the date looking for + // certain hints of what type of date we are dealing with. + // Hopefully we only need to read about 5 or 6 bytes before + // we figure it out and then attempt a parse +iterRunes: + for ; i < len(datestr); i++ { + //r := rune(datestr[i]) + r, bytesConsumed := utf8.DecodeRuneInString(datestr[i:]) + if bytesConsumed > 1 { + i += (bytesConsumed - 1) + } + + // gou.Debugf("i=%d r=%s state=%d %s", i, string(r), p.stateDate, datestr) + switch p.stateDate { + case dateStart: + if unicode.IsDigit(r) { + p.stateDate = dateDigit + } else if unicode.IsLetter(r) { + p.stateDate = dateAlpha + } else { + return nil, unknownErr(datestr) + } + case dateDigit: + + switch r { + case '-', '\u2212': + // 2006-01-02 + // 2013-Feb-03 + // 13-Feb-03 + // 29-Jun-2016 + if i == 4 { + p.stateDate = dateYearDash + p.yeari = 0 + p.yearlen = i + p.moi = i + 1 + p.set(0, "2006") + } else { + p.stateDate = dateDigitDash + } + case '/': + // 08/May/2005 + // 03/31/2005 + // 2014/02/24 + p.stateDate = dateDigitSlash + if i == 4 { + // 2014/02/24 - Year first / + p.yearlen = i // since it was start of datestr, i=len + p.moi = i + 1 + p.setYear() + p.stateDate = dateDigitYearSlash + } else { + // Either Ambiguous dd/mm vs mm/dd OR dd/month/yy + // 08/May/2005 + // 03/31/2005 + // 31/03/2005 + if i+2 < len(p.datestr) && unicode.IsLetter(rune(datestr[i+1])) { + // 08/May/2005 + p.stateDate = dateDigitSlashAlpha + p.moi = i + 1 + p.daylen = 2 + p.dayi = 0 + p.setDay() + continue + } + // Ambiguous dd/mm vs mm/dd the bane of date-parsing + // 03/31/2005 + // 31/03/2005 + p.ambiguousMD = true + if p.preferMonthFirst { + if p.molen == 0 { + // 03/31/2005 + p.molen = i + p.setMonth() + p.dayi = i + 1 + } + } else { + if p.daylen == 0 { + p.daylen = i + p.setDay() + p.moi = i + 1 + } + } + + } + + case ':': + // 03/31/2005 + // 2014/02/24 + p.stateDate = dateDigitColon + if i == 4 { + p.yearlen = i + p.moi = i + 1 + p.setYear() + } else { + p.ambiguousMD = true + if p.preferMonthFirst { + if p.molen == 0 { + p.molen = i + p.setMonth() + p.dayi = i + 1 + } + } + } + + case '.': + // 3.31.2014 + // 08.21.71 + // 2014.05 + p.stateDate = dateDigitDot + if i == 4 { + p.yearlen = i + p.moi = i + 1 + p.setYear() + } else { + p.ambiguousMD = true + p.moi = 0 + p.molen = i + p.setMonth() + p.dayi = i + 1 + } + + case ' ': + // 18 January 2018 + // 8 January 2018 + // 8 jan 2018 + // 02 Jan 2018 23:59 + // 02 Jan 2018 23:59:34 + // 12 Feb 2006, 19:17 + // 12 Feb 2006, 19:17:22 + if i == 6 { + p.stateDate = dateDigitSt + } else { + p.stateDate = dateDigitWs + p.dayi = 0 + p.daylen = i + } + case '年': + // Chinese Year + p.stateDate = dateDigitChineseYear + case ',': + return nil, unknownErr(datestr) + default: + continue + } + p.part1Len = i + + case dateDigitSt: + p.set(0, "060102") + i = i - 1 + p.stateTime = timeStart + break iterRunes + case dateYearDash: + // dateYearDashDashT + // 2006-01-02T15:04:05Z07:00 + // 2020-08-17T17:00:00:000+0100 + // dateYearDashDashWs + // 2013-04-01 22:43:22 + // dateYearDashAlphaDash + // 2013-Feb-03 + switch r { + case '-': + p.molen = i - p.moi + p.dayi = i + 1 + p.stateDate = dateYearDashDash + p.setMonth() + default: + if unicode.IsLetter(r) { + p.stateDate = dateYearDashAlphaDash + } + } + + case dateYearDashDash: + // dateYearDashDashT + // 2006-01-02T15:04:05Z07:00 + // dateYearDashDashWs + // 2013-04-01 22:43:22 + // dateYearDashDashOffset + // 2020-07-20+00:00 + switch r { + case '+', '-': + p.offseti = i + p.daylen = i - p.dayi + p.stateDate = dateYearDashDashOffset + p.setDay() + case ' ': + p.daylen = i - p.dayi + p.stateDate = dateYearDashDashWs + p.stateTime = timeStart + p.setDay() + break iterRunes + case 'T': + p.daylen = i - p.dayi + p.stateDate = dateYearDashDashT + p.stateTime = timeStart + p.setDay() + break iterRunes + } + + case dateYearDashDashT: + // dateYearDashDashT + // 2006-01-02T15:04:05Z07:00 + // 2020-08-17T17:00:00:000+0100 + + case dateYearDashDashOffset: + // 2020-07-20+00:00 + switch r { + case ':': + p.set(p.offseti, "-07:00") + // case ' ': + // return nil, unknownErr(datestr) + } + + case dateYearDashAlphaDash: + // 2013-Feb-03 + switch r { + case '-': + p.molen = i - p.moi + p.set(p.moi, "Jan") + p.dayi = i + 1 + } + case dateDigitDash: + // 13-Feb-03 + // 29-Jun-2016 + if unicode.IsLetter(r) { + p.stateDate = dateDigitDashAlpha + p.moi = i + } else { + return nil, unknownErr(datestr) + } + case dateDigitDashAlpha: + // 13-Feb-03 + // 28-Feb-03 + // 29-Jun-2016 + switch r { + case '-': + p.molen = i - p.moi + p.set(p.moi, "Jan") + p.yeari = i + 1 + p.stateDate = dateDigitDashAlphaDash + } + + case dateDigitDashAlphaDash: + // 13-Feb-03 ambiguous + // 28-Feb-03 ambiguous + // 29-Jun-2016 dd-month(alpha)-yyyy + switch r { + case ' ': + // we need to find if this was 4 digits, aka year + // or 2 digits which makes it ambiguous year/day + length := i - (p.moi + p.molen + 1) + if length == 4 { + p.yearlen = 4 + p.set(p.yeari, "2006") + // We now also know that part1 was the day + p.dayi = 0 + p.daylen = p.part1Len + p.setDay() + } else if length == 2 { + // We have no idea if this is + // yy-mon-dd OR dd-mon-yy + // + // We are going to ASSUME (bad, bad) that it is dd-mon-yy which is a horible assumption + p.ambiguousMD = true + p.yearlen = 2 + p.set(p.yeari, "06") + // We now also know that part1 was the day + p.dayi = 0 + p.daylen = p.part1Len + p.setDay() + } + p.stateTime = timeStart + break iterRunes + } + + case dateDigitYearSlash: + // 2014/07/10 06:55:38.156283 + // I honestly don't know if this format ever shows up as yyyy/ + + switch r { + case ' ', ':': + p.stateTime = timeStart + if p.daylen == 0 { + p.daylen = i - p.dayi + p.setDay() + } + break iterRunes + case '/': + if p.molen == 0 { + p.molen = i - p.moi + p.setMonth() + p.dayi = i + 1 + } + } + + case dateDigitSlashAlpha: + // 06/May/2008 + + switch r { + case '/': + // | + // 06/May/2008 + if p.molen == 0 { + p.set(p.moi, "Jan") + p.yeari = i + 1 + } + // We aren't breaking because we are going to re-use this case + // to find where the date starts, and possible time begins + case ' ', ':': + p.stateTime = timeStart + if p.yearlen == 0 { + p.yearlen = i - p.yeari + p.setYear() + } + break iterRunes + } + + case dateDigitSlash: + // 03/19/2012 10:11:59 + // 04/2/2014 03:00:37 + // 3/1/2012 10:11:59 + // 4/8/2014 22:05 + // 3/1/2014 + // 10/13/2014 + // 01/02/2006 + // 1/2/06 + + switch r { + case '/': + // This is the 2nd / so now we should know start pts of all of the dd, mm, yy + if p.preferMonthFirst { + if p.daylen == 0 { + p.daylen = i - p.dayi + p.setDay() + p.yeari = i + 1 + } + } else { + if p.molen == 0 { + p.molen = i - p.moi + p.setMonth() + p.yeari = i + 1 + } + } + // Note no break, we are going to pass by and re-enter this dateDigitSlash + // and look for ending (space) or not (just date) + case ' ': + p.stateTime = timeStart + if p.yearlen == 0 { + p.yearlen = i - p.yeari + p.setYear() + } + break iterRunes + } + + case dateDigitColon: + // 2014:07:10 06:55:38.156283 + // 03:19:2012 10:11:59 + // 04:2:2014 03:00:37 + // 3:1:2012 10:11:59 + // 4:8:2014 22:05 + // 3:1:2014 + // 10:13:2014 + // 01:02:2006 + // 1:2:06 + + switch r { + case ' ': + p.stateTime = timeStart + if p.yearlen == 0 { + p.yearlen = i - p.yeari + p.setYear() + } else if p.daylen == 0 { + p.daylen = i - p.dayi + p.setDay() + } + break iterRunes + case ':': + if p.yearlen > 0 { + // 2014:07:10 06:55:38.156283 + if p.molen == 0 { + p.molen = i - p.moi + p.setMonth() + p.dayi = i + 1 + } + } else if p.preferMonthFirst { + if p.daylen == 0 { + p.daylen = i - p.dayi + p.setDay() + p.yeari = i + 1 + } + } + } + + case dateDigitWs: + // 18 January 2018 + // 8 January 2018 + // 8 jan 2018 + // 1 jan 18 + // 02 Jan 2018 23:59 + // 02 Jan 2018 23:59:34 + // 12 Feb 2006, 19:17 + // 12 Feb 2006, 19:17:22 + switch r { + case ' ': + p.yeari = i + 1 + //p.yearlen = 4 + p.dayi = 0 + p.daylen = p.part1Len + p.setDay() + p.stateTime = timeStart + if i > p.daylen+len(" Sep") { // November etc + // If len greather than space + 3 it must be full month + p.stateDate = dateDigitWsMolong + } else { + // If len=3, the might be Feb or May? Ie ambigous abbreviated but + // we can parse may with either. BUT, that means the + // format may not be correct? + // mo := strings.ToLower(datestr[p.daylen+1 : i]) + p.moi = p.daylen + 1 + p.molen = i - p.moi + p.set(p.moi, "Jan") + p.stateDate = dateDigitWsMoYear + } + } + + case dateDigitWsMoYear: + // 8 jan 2018 + // 02 Jan 2018 23:59 + // 02 Jan 2018 23:59:34 + // 12 Feb 2006, 19:17 + // 12 Feb 2006, 19:17:22 + switch r { + case ',': + p.yearlen = i - p.yeari + p.setYear() + i++ + break iterRunes + case ' ': + p.yearlen = i - p.yeari + p.setYear() + break iterRunes + } + case dateDigitWsMolong: + // 18 January 2018 + // 8 January 2018 + + case dateDigitChineseYear: + // dateDigitChineseYear + // 2014年04月08日 + // weekday %Y年%m月%e日 %A %I:%M %p + // 2013年07月18日 星期四 10:27 上午 + if r == ' ' { + p.stateDate = dateDigitChineseYearWs + break + } + case dateDigitDot: + // This is the 2nd period + // 3.31.2014 + // 08.21.71 + // 2014.05 + // 2018.09.30 + if r == '.' { + if p.moi == 0 { + // 3.31.2014 + p.daylen = i - p.dayi + p.yeari = i + 1 + p.setDay() + p.stateDate = dateDigitDotDot + } else { + // 2018.09.30 + //p.molen = 2 + p.molen = i - p.moi + p.dayi = i + 1 + p.setMonth() + p.stateDate = dateDigitDotDot + } + } + case dateDigitDotDot: + // iterate all the way through + case dateAlpha: + // dateAlphaWS + // Mon Jan _2 15:04:05 2006 + // Mon Jan _2 15:04:05 MST 2006 + // Mon Jan 02 15:04:05 -0700 2006 + // Mon Aug 10 15:44:11 UTC+0100 2015 + // Fri Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time) + // dateAlphaWSDigit + // May 8, 2009 5:57:51 PM + // oct 1, 1970 + // dateAlphaWsMonth + // April 8, 2009 + // dateAlphaWsMore + // dateAlphaWsAtTime + // January 02, 2006 at 3:04pm MST-07 + // + // dateAlphaPeriodWsDigit + // oct. 1, 1970 + // dateWeekdayComma + // Monday, 02 Jan 2006 15:04:05 MST + // Monday, 02-Jan-06 15:04:05 MST + // Monday, 02 Jan 2006 15:04:05 -0700 + // Monday, 02 Jan 2006 15:04:05 +0100 + // dateWeekdayAbbrevComma + // Mon, 02 Jan 2006 15:04:05 MST + // Mon, 02 Jan 2006 15:04:05 -0700 + // Thu, 13 Jul 2017 08:58:40 +0100 + // Tue, 11 Jul 2017 16:28:13 +0200 (CEST) + // Mon, 02-Jan-06 15:04:05 MST + switch { + case r == ' ': + // X + // April 8, 2009 + if i > 3 { + // Check to see if the alpha is name of month? or Day? + month := strings.ToLower(datestr[0:i]) + if isMonthFull(month) { + p.fullMonth = month + // len(" 31, 2018") = 9 + if len(datestr[i:]) < 10 { + // April 8, 2009 + p.stateDate = dateAlphaWsMonth + } else { + p.stateDate = dateAlphaWsMore + } + p.dayi = i + 1 + break + } + + } else { + // This is possibly ambiguous? May will parse as either though. + // So, it could return in-correct format. + // dateAlphaWs + // May 05, 2005, 05:05:05 + // May 05 2005, 05:05:05 + // Jul 05, 2005, 05:05:05 + // May 8 17:57:51 2009 + // May 8 17:57:51 2009 + // skip & return to dateStart + // Tue 05 May 2020, 05:05:05 + // Mon Jan 2 15:04:05 2006 + + maybeDay := strings.ToLower(datestr[0:i]) + if isDay(maybeDay) { + // using skip throws off indices used by other code; saner to restart + return parseTime(datestr[i+1:], loc) + } + p.stateDate = dateAlphaWs + } + + case r == ',': + // Mon, 02 Jan 2006 + + if i == 3 { + p.stateDate = dateWeekdayAbbrevComma + p.set(0, "Mon") + } else { + p.stateDate = dateWeekdayComma + p.skip = i + 2 + i++ + // TODO: lets just make this "skip" as we don't need + // the mon, monday, they are all superfelous and not needed + // just lay down the skip, no need to fill and then skip + } + case r == '.': + // sept. 28, 2017 + // jan. 28, 2017 + p.stateDate = dateAlphaPeriodWsDigit + if i == 3 { + p.molen = i + p.set(0, "Jan") + } else if i == 4 { + // gross + datestr = datestr[0:i-1] + datestr[i:] + return parseTime(datestr, loc, opts...) + } else { + return nil, unknownErr(datestr) + } + } + + case dateAlphaWs: + // dateAlphaWsAlpha + // Mon Jan _2 15:04:05 2006 + // Mon Jan _2 15:04:05 MST 2006 + // Mon Jan 02 15:04:05 -0700 2006 + // Fri Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time) + // Mon Aug 10 15:44:11 UTC+0100 2015 + // dateAlphaWsDigit + // May 8, 2009 5:57:51 PM + // May 8 2009 5:57:51 PM + // May 8 17:57:51 2009 + // May 8 17:57:51 2009 + // May 08 17:57:51 2009 + // oct 1, 1970 + // oct 7, '70 + switch { + case unicode.IsLetter(r): + p.set(0, "Mon") + p.stateDate = dateAlphaWsAlpha + p.set(i, "Jan") + case unicode.IsDigit(r): + p.set(0, "Jan") + p.stateDate = dateAlphaWsDigit + p.dayi = i + } + + case dateAlphaWsDigit: + // May 8, 2009 5:57:51 PM + // May 8 2009 5:57:51 PM + // oct 1, 1970 + // oct 7, '70 + // oct. 7, 1970 + // May 8 17:57:51 2009 + // May 8 17:57:51 2009 + // May 08 17:57:51 2009 + if r == ',' { + p.daylen = i - p.dayi + p.setDay() + p.stateDate = dateAlphaWsDigitMore + } else if r == ' ' { + p.daylen = i - p.dayi + p.setDay() + p.yeari = i + 1 + p.stateDate = dateAlphaWsDigitYearmaybe + p.stateTime = timeStart + } else if unicode.IsLetter(r) { + p.stateDate = dateAlphaWsMonthSuffix + i-- + } + case dateAlphaWsDigitYearmaybe: + // x + // May 8 2009 5:57:51 PM + // May 8 17:57:51 2009 + // May 8 17:57:51 2009 + // May 08 17:57:51 2009 + // Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time) + if r == ':' { + // Guessed wrong; was not a year + i = i - 3 + p.stateDate = dateAlphaWsDigit + p.yeari = 0 + break iterRunes + } else if r == ' ' { + // must be year format, not 15:04 + p.yearlen = i - p.yeari + p.setYear() + break iterRunes + } + case dateAlphaWsDigitMore: + // x + // May 8, 2009 5:57:51 PM + // May 05, 2005, 05:05:05 + // May 05 2005, 05:05:05 + // oct 1, 1970 + // oct 7, '70 + if r == ' ' { + p.yeari = i + 1 + p.stateDate = dateAlphaWsDigitMoreWs + } + case dateAlphaWsDigitMoreWs: + // x + // May 8, 2009 5:57:51 PM + // May 05, 2005, 05:05:05 + // oct 1, 1970 + // oct 7, '70 + switch r { + case '\'': + p.yeari = i + 1 + case ' ', ',': + // x + // May 8, 2009 5:57:51 PM + // x + // May 8, 2009, 5:57:51 PM + p.stateDate = dateAlphaWsDigitMoreWsYear + p.yearlen = i - p.yeari + p.setYear() + p.stateTime = timeStart + break iterRunes + } + + case dateAlphaWsMonth: + // April 8, 2009 + // April 8 2009 + switch r { + case ' ', ',': + // x + // June 8, 2009 + // x + // June 8 2009 + if p.daylen == 0 { + p.daylen = i - p.dayi + p.setDay() + } + case 's', 'S', 'r', 'R', 't', 'T', 'n', 'N': + // st, rd, nd, st + i-- + p.stateDate = dateAlphaWsMonthSuffix + default: + if p.daylen > 0 && p.yeari == 0 { + p.yeari = i + } + } + case dateAlphaWsMonthMore: + // X + // January 02, 2006, 15:04:05 + // January 02 2006, 15:04:05 + // January 02, 2006 15:04:05 + // January 02 2006 15:04:05 + switch r { + case ',': + p.yearlen = i - p.yeari + p.setYear() + p.stateTime = timeStart + i++ + break iterRunes + case ' ': + p.yearlen = i - p.yeari + p.setYear() + p.stateTime = timeStart + break iterRunes + } + case dateAlphaWsMonthSuffix: + // x + // April 8th, 2009 + // April 8th 2009 + switch r { + case 't', 'T': + if p.nextIs(i, 'h') || p.nextIs(i, 'H') { + if len(datestr) > i+2 { + return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc, opts...) + } + } + case 'n', 'N': + if p.nextIs(i, 'd') || p.nextIs(i, 'D') { + if len(datestr) > i+2 { + return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc, opts...) + } + } + case 's', 'S': + if p.nextIs(i, 't') || p.nextIs(i, 'T') { + if len(datestr) > i+2 { + return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc, opts...) + } + } + case 'r', 'R': + if p.nextIs(i, 'd') || p.nextIs(i, 'D') { + if len(datestr) > i+2 { + return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc, opts...) + } + } + } + case dateAlphaWsMore: + // January 02, 2006, 15:04:05 + // January 02 2006, 15:04:05 + // January 2nd, 2006, 15:04:05 + // January 2nd 2006, 15:04:05 + // September 17, 2012 at 5:00pm UTC-05 + switch { + case r == ',': + // x + // January 02, 2006, 15:04:05 + if p.nextIs(i, ' ') { + p.daylen = i - p.dayi + p.setDay() + p.yeari = i + 2 + p.stateDate = dateAlphaWsMonthMore + i++ + } + + case r == ' ': + // x + // January 02 2006, 15:04:05 + p.daylen = i - p.dayi + p.setDay() + p.yeari = i + 1 + p.stateDate = dateAlphaWsMonthMore + case unicode.IsDigit(r): + // XX + // January 02, 2006, 15:04:05 + continue + case unicode.IsLetter(r): + // X + // January 2nd, 2006, 15:04:05 + p.daylen = i - p.dayi + p.setDay() + p.stateDate = dateAlphaWsMonthSuffix + i-- + } + + case dateAlphaPeriodWsDigit: + // oct. 7, '70 + switch { + case r == ' ': + // continue + case unicode.IsDigit(r): + p.stateDate = dateAlphaWsDigit + p.dayi = i + default: + return p, unknownErr(datestr) + } + case dateWeekdayComma: + // Monday, 02 Jan 2006 15:04:05 MST + // Monday, 02 Jan 2006 15:04:05 -0700 + // Monday, 02 Jan 2006 15:04:05 +0100 + // Monday, 02-Jan-06 15:04:05 MST + if p.dayi == 0 { + p.dayi = i + } + switch r { + case ' ', '-': + if p.moi == 0 { + p.moi = i + 1 + p.daylen = i - p.dayi + p.setDay() + } else if p.yeari == 0 { + p.yeari = i + 1 + p.molen = i - p.moi + p.set(p.moi, "Jan") + } else { + p.stateTime = timeStart + break iterRunes + } + } + case dateWeekdayAbbrevComma: + // Mon, 02 Jan 2006 15:04:05 MST + // Mon, 02 Jan 2006 15:04:05 -0700 + // Thu, 13 Jul 2017 08:58:40 +0100 + // Thu, 4 Jan 2018 17:53:36 +0000 + // Tue, 11 Jul 2017 16:28:13 +0200 (CEST) + // Mon, 02-Jan-06 15:04:05 MST + switch r { + case ' ', '-': + if p.dayi == 0 { + p.dayi = i + 1 + } else if p.moi == 0 { + p.daylen = i - p.dayi + p.setDay() + p.moi = i + 1 + } else if p.yeari == 0 { + p.molen = i - p.moi + p.set(p.moi, "Jan") + p.yeari = i + 1 + } else { + p.yearlen = i - p.yeari + p.setYear() + p.stateTime = timeStart + break iterRunes + } + } + + default: + break iterRunes + } + } + p.coalesceDate(i) + if p.stateTime == timeStart { + // increment first one, since the i++ occurs at end of loop + if i < len(p.datestr) { + i++ + } + // ensure we skip any whitespace prefix + for ; i < len(datestr); i++ { + r := rune(datestr[i]) + if r != ' ' { + break + } + } + + iterTimeRunes: + for ; i < len(datestr); i++ { + r := rune(datestr[i]) + + // gou.Debugf("i=%d r=%s state=%d iterTimeRunes %s %s", i, string(r), p.stateTime, p.ds(), p.ts()) + + switch p.stateTime { + case timeStart: + // 22:43:22 + // 22:43 + // timeComma + // 08:20:13,787 + // timeWs + // 05:24:37 PM + // 06:20:00 UTC + // 06:20:00 UTC-05 + // 00:12:00 +0000 UTC + // 22:18:00 +0000 UTC m=+0.000000001 + // 15:04:05 -0700 + // 15:04:05 -07:00 + // 15:04:05 2008 + // timeOffset + // 03:21:51+00:00 + // 19:55:00+0100 + // timePeriod + // 17:24:37.3186369 + // 00:07:31.945167 + // 18:31:59.257000000 + // 00:00:00.000 + // timePeriodOffset + // 19:55:00.799+0100 + // timePeriodOffsetColon + // 15:04:05.999-07:00 + // timePeriodWs + // timePeriodWsOffset + // 00:07:31.945167 +0000 + // 00:00:00.000 +0000 + // timePeriodWsOffsetAlpha + // 00:07:31.945167 +0000 UTC + // 22:18:00.001 +0000 UTC m=+0.000000001 + // 00:00:00.000 +0000 UTC + // timePeriodWsAlpha + // 06:20:00.000 UTC + if p.houri == 0 { + p.houri = i + } + switch r { + case ',': + // hm, lets just swap out comma for period. for some reason go + // won't parse it. + // 2014-05-11 08:20:13,787 + ds := []byte(p.datestr) + ds[i] = '.' + return parseTime(string(ds), loc, opts...) + case '-', '+': + // 03:21:51+00:00 + p.stateTime = timeOffset + if p.seci == 0 { + // 22:18+0530 + p.minlen = i - p.mini + } else { + if p.seclen == 0 { + p.seclen = i - p.seci + } + if p.msi > 0 && p.mslen == 0 { + p.mslen = i - p.msi + } + } + p.offseti = i + case '.': + p.stateTime = timePeriod + p.seclen = i - p.seci + p.msi = i + 1 + case 'Z': + p.stateTime = timeZ + if p.seci == 0 { + p.minlen = i - p.mini + } else { + p.seclen = i - p.seci + } + // (Z)ulu time + p.loc = time.UTC + case 'a', 'A': + if p.nextIs(i, 't') || p.nextIs(i, 'T') { + // x + // September 17, 2012 at 5:00pm UTC-05 + i++ // skip t + if p.nextIs(i, ' ') { + // x + // September 17, 2012 at 5:00pm UTC-05 + i++ // skip ' + p.houri = 0 // reset hour + } + } else { + switch { + case r == 'a' && p.nextIs(i, 'm'): + p.coalesceTime(i) + p.set(i, "am") + case r == 'A' && p.nextIs(i, 'M'): + p.coalesceTime(i) + p.set(i, "PM") + } + } + + case 'p', 'P': + // Could be AM/PM + switch { + case r == 'p' && p.nextIs(i, 'm'): + p.coalesceTime(i) + p.set(i, "pm") + case r == 'P' && p.nextIs(i, 'M'): + p.coalesceTime(i) + p.set(i, "PM") + } + case ' ': + p.coalesceTime(i) + p.stateTime = timeWs + case ':': + if p.mini == 0 { + p.mini = i + 1 + p.hourlen = i - p.houri + } else if p.seci == 0 { + p.seci = i + 1 + p.minlen = i - p.mini + } else if p.seci > 0 { + // 18:31:59:257 ms uses colon, wtf + p.seclen = i - p.seci + p.set(p.seci, "05") + p.msi = i + 1 + + // gross, gross, gross. manipulating the datestr is horrible. + // https://github.com/araddon/dateparse/issues/117 + // Could not get the parsing to work using golang time.Parse() without + // replacing that colon with period. + p.set(i, ".") + datestr = datestr[0:i] + "." + datestr[i+1:] + p.datestr = datestr + } + } + case timeOffset: + // 19:55:00+0100 + // timeOffsetColon + // 15:04:05+07:00 + // 15:04:05-07:00 + if r == ':' { + p.stateTime = timeOffsetColon + } + case timeWs: + // timeWsAlpha + // 06:20:00 UTC + // 06:20:00 UTC-05 + // 15:44:11 UTC+0100 2015 + // 18:04:07 GMT+0100 (GMT Daylight Time) + // 17:57:51 MST 2009 + // timeWsAMPMMaybe + // 05:24:37 PM + // timeWsOffset + // 15:04:05 -0700 + // 00:12:00 +0000 UTC + // timeWsOffsetColon + // 15:04:05 -07:00 + // 17:57:51 -0700 2009 + // timeWsOffsetColonAlpha + // 00:12:00 +00:00 UTC + // timeWsYear + // 00:12:00 2008 + // timeZ + // 15:04:05.99Z + switch r { + case 'A', 'P': + // Could be AM/PM or could be PST or similar + p.tzi = i + p.stateTime = timeWsAMPMMaybe + case '+', '-': + p.offseti = i + p.stateTime = timeWsOffset + default: + if unicode.IsLetter(r) { + // 06:20:00 UTC + // 06:20:00 UTC-05 + // 15:44:11 UTC+0100 2015 + // 17:57:51 MST 2009 + p.tzi = i + p.stateTime = timeWsAlpha + } else if unicode.IsDigit(r) { + // 00:12:00 2008 + p.stateTime = timeWsYear + p.yeari = i + } + } + case timeWsAlpha: + // 06:20:00 UTC + // 06:20:00 UTC-05 + // timeWsAlphaWs + // 17:57:51 MST 2009 + // timeWsAlphaZoneOffset + // timeWsAlphaZoneOffsetWs + // timeWsAlphaZoneOffsetWsExtra + // 18:04:07 GMT+0100 (GMT Daylight Time) + // timeWsAlphaZoneOffsetWsYear + // 15:44:11 UTC+0100 2015 + switch r { + case '+', '-': + p.tzlen = i - p.tzi + if p.tzlen == 4 { + p.set(p.tzi, " MST") + } else if p.tzlen == 3 { + p.set(p.tzi, "MST") + } + p.stateTime = timeWsAlphaZoneOffset + p.offseti = i + case ' ': + // 17:57:51 MST 2009 + // 17:57:51 MST + p.tzlen = i - p.tzi + if p.tzlen == 4 { + p.set(p.tzi, " MST") + } else if p.tzlen == 3 { + p.set(p.tzi, "MST") + } + p.stateTime = timeWsAlphaWs + p.yeari = i + 1 + } + case timeWsAlphaWs: + // 17:57:51 MST 2009 + + case timeWsAlphaZoneOffset: + // 06:20:00 UTC-05 + // timeWsAlphaZoneOffset + // timeWsAlphaZoneOffsetWs + // timeWsAlphaZoneOffsetWsExtra + // 18:04:07 GMT+0100 (GMT Daylight Time) + // timeWsAlphaZoneOffsetWsYear + // 15:44:11 UTC+0100 2015 + switch r { + case ' ': + p.set(p.offseti, "-0700") + if p.yeari == 0 { + p.yeari = i + 1 + } + p.stateTime = timeWsAlphaZoneOffsetWs + } + case timeWsAlphaZoneOffsetWs: + // timeWsAlphaZoneOffsetWs + // timeWsAlphaZoneOffsetWsExtra + // 18:04:07 GMT+0100 (GMT Daylight Time) + // timeWsAlphaZoneOffsetWsYear + // 15:44:11 UTC+0100 2015 + if unicode.IsDigit(r) { + p.stateTime = timeWsAlphaZoneOffsetWsYear + } else { + p.extra = i - 1 + p.stateTime = timeWsAlphaZoneOffsetWsExtra + } + case timeWsAlphaZoneOffsetWsYear: + // 15:44:11 UTC+0100 2015 + if unicode.IsDigit(r) { + p.yearlen = i - p.yeari + 1 + if p.yearlen == 4 { + p.setYear() + } + } + case timeWsAMPMMaybe: + // timeWsAMPMMaybe + // timeWsAMPM + // 05:24:37 PM + // timeWsAlpha + // 00:12:00 PST + // 15:44:11 UTC+0100 2015 + if r == 'M' { + //return parse("2006-01-02 03:04:05 PM", datestr, loc) + p.stateTime = timeWsAMPM + p.set(i-1, "PM") + if p.hourlen == 2 { + p.set(p.houri, "03") + } else if p.hourlen == 1 { + p.set(p.houri, "3") + } + } else { + p.stateTime = timeWsAlpha + } + + case timeWsOffset: + // timeWsOffset + // 15:04:05 -0700 + // timeWsOffsetWsOffset + // 17:57:51 -0700 -07 + // timeWsOffsetWs + // 17:57:51 -0700 2009 + // 00:12:00 +0000 UTC + // timeWsOffsetColon + // 15:04:05 -07:00 + // timeWsOffsetColonAlpha + // 00:12:00 +00:00 UTC + switch r { + case ':': + p.stateTime = timeWsOffsetColon + case ' ': + p.set(p.offseti, "-0700") + p.yeari = i + 1 + p.stateTime = timeWsOffsetWs + } + case timeWsOffsetWs: + // 17:57:51 -0700 2009 + // 00:12:00 +0000 UTC + // 22:18:00.001 +0000 UTC m=+0.000000001 + // w Extra + // 17:57:51 -0700 -07 + switch r { + case '=': + // eff you golang + if datestr[i-1] == 'm' { + p.extra = i - 2 + p.trimExtra() + break + } + case '+', '-', '(': + // This really doesn't seem valid, but for some reason when round-tripping a go date + // their is an extra +03 printed out. seems like go bug to me, but, parsing anyway. + // 00:00:00 +0300 +03 + // 00:00:00 +0300 +0300 + p.extra = i - 1 + p.stateTime = timeWsOffset + p.trimExtra() + break + default: + switch { + case unicode.IsDigit(r): + p.yearlen = i - p.yeari + 1 + if p.yearlen == 4 { + p.setYear() + } + case unicode.IsLetter(r): + // 15:04:05 -0700 MST + if p.tzi == 0 { + p.tzi = i + } + } + } + + case timeWsOffsetColon: + // timeWsOffsetColon + // 15:04:05 -07:00 + // timeWsOffsetColonAlpha + // 2015-02-18 00:12:00 +00:00 UTC + if unicode.IsLetter(r) { + // 2015-02-18 00:12:00 +00:00 UTC + p.stateTime = timeWsOffsetColonAlpha + break iterTimeRunes + } + case timePeriod: + // 15:04:05.999999999+07:00 + // 15:04:05.999999999-07:00 + // 15:04:05.999999+07:00 + // 15:04:05.999999-07:00 + // 15:04:05.999+07:00 + // 15:04:05.999-07:00 + // timePeriod + // 17:24:37.3186369 + // 00:07:31.945167 + // 18:31:59.257000000 + // 00:00:00.000 + // timePeriodOffset + // 19:55:00.799+0100 + // timePeriodOffsetColon + // 15:04:05.999-07:00 + // timePeriodWs + // timePeriodWsOffset + // 00:07:31.945167 +0000 + // 00:00:00.000 +0000 + // With Extra + // 00:00:00.000 +0300 +03 + // timePeriodWsOffsetAlpha + // 00:07:31.945167 +0000 UTC + // 00:00:00.000 +0000 UTC + // 22:18:00.001 +0000 UTC m=+0.000000001 + // timePeriodWsAlpha + // 06:20:00.000 UTC + switch r { + case ' ': + p.mslen = i - p.msi + p.stateTime = timePeriodWs + case '+', '-': + // This really shouldn't happen + p.mslen = i - p.msi + p.offseti = i + p.stateTime = timePeriodOffset + default: + if unicode.IsLetter(r) { + // 06:20:00.000 UTC + p.mslen = i - p.msi + p.stateTime = timePeriodWsAlpha + } + } + case timePeriodOffset: + // timePeriodOffset + // 19:55:00.799+0100 + // timePeriodOffsetColon + // 15:04:05.999-07:00 + // 13:31:51.999-07:00 MST + if r == ':' { + p.stateTime = timePeriodOffsetColon + } + case timePeriodOffsetColon: + // timePeriodOffset + // timePeriodOffsetColon + // 15:04:05.999-07:00 + // 13:31:51.999 -07:00 MST + switch r { + case ' ': + p.set(p.offseti, "-07:00") + p.stateTime = timePeriodOffsetColonWs + p.tzi = i + 1 + } + case timePeriodOffsetColonWs: + // continue + case timePeriodWs: + // timePeriodWs + // timePeriodWsOffset + // 00:07:31.945167 +0000 + // 00:00:00.000 +0000 + // timePeriodWsOffsetAlpha + // 00:07:31.945167 +0000 UTC + // 00:00:00.000 +0000 UTC + // timePeriodWsOffsetColon + // 13:31:51.999 -07:00 MST + // timePeriodWsAlpha + // 06:20:00.000 UTC + if p.offseti == 0 { + p.offseti = i + } + switch r { + case '+', '-': + p.mslen = i - p.msi - 1 + p.stateTime = timePeriodWsOffset + default: + if unicode.IsLetter(r) { + // 00:07:31.945167 +0000 UTC + // 00:00:00.000 +0000 UTC + p.stateTime = timePeriodWsOffsetWsAlpha + break iterTimeRunes + } + } + + case timePeriodWsOffset: + // timePeriodWs + // timePeriodWsOffset + // 00:07:31.945167 +0000 + // 00:00:00.000 +0000 + // With Extra + // 00:00:00.000 +0300 +03 + // timePeriodWsOffsetAlpha + // 00:07:31.945167 +0000 UTC + // 00:00:00.000 +0000 UTC + // 03:02:00.001 +0300 MSK m=+0.000000001 + // timePeriodWsOffsetColon + // 13:31:51.999 -07:00 MST + // timePeriodWsAlpha + // 06:20:00.000 UTC + switch r { + case ':': + p.stateTime = timePeriodWsOffsetColon + case ' ': + p.set(p.offseti, "-0700") + case '+', '-': + // This really doesn't seem valid, but for some reason when round-tripping a go date + // their is an extra +03 printed out. seems like go bug to me, but, parsing anyway. + // 00:00:00.000 +0300 +03 + // 00:00:00.000 +0300 +0300 + p.extra = i - 1 + p.trimExtra() + break + default: + if unicode.IsLetter(r) { + // 00:07:31.945167 +0000 UTC + // 00:00:00.000 +0000 UTC + // 03:02:00.001 +0300 MSK m=+0.000000001 + p.stateTime = timePeriodWsOffsetWsAlpha + } + } + case timePeriodWsOffsetWsAlpha: + // 03:02:00.001 +0300 MSK m=+0.000000001 + // eff you golang + if r == '=' && datestr[i-1] == 'm' { + p.extra = i - 2 + p.trimExtra() + break + } + + case timePeriodWsOffsetColon: + // 13:31:51.999 -07:00 MST + switch r { + case ' ': + p.set(p.offseti, "-07:00") + default: + if unicode.IsLetter(r) { + // 13:31:51.999 -07:00 MST + p.tzi = i + p.stateTime = timePeriodWsOffsetColonAlpha + } + } + case timePeriodWsOffsetColonAlpha: + // continue + case timeZ: + // timeZ + // 15:04:05.99Z + // With a time-zone at end after Z + // 2006-01-02T15:04:05.999999999Z07:00 + // 2006-01-02T15:04:05Z07:00 + // RFC3339 = "2006-01-02T15:04:05Z07:00" + // RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00" + if unicode.IsDigit(r) { + p.stateTime = timeZDigit + } + + } + } + + switch p.stateTime { + case timeWsAlpha: + switch len(p.datestr) - p.tzi { + case 3: + // 13:31:51.999 +01:00 CET + p.set(p.tzi, "MST") + case 4: + p.set(p.tzi, "MST") + p.extra = len(p.datestr) - 1 + p.trimExtra() + } + + case timeWsAlphaWs: + p.yearlen = i - p.yeari + p.setYear() + case timeWsYear: + p.yearlen = i - p.yeari + p.setYear() + case timeWsAlphaZoneOffsetWsExtra: + p.trimExtra() + case timeWsAlphaZoneOffset: + // 06:20:00 UTC-05 + if i-p.offseti < 4 { + p.set(p.offseti, "-07") + } else { + p.set(p.offseti, "-0700") + } + + case timePeriod: + p.mslen = i - p.msi + case timeOffset: + + switch len(p.datestr) - p.offseti { + case 0, 1, 2, 4: + return p, fmt.Errorf("TZ offset not recognized %q near %q (must be 2 or 4 digits optional colon)", datestr, string(datestr[p.offseti:])) + case 3: + // 19:55:00+01 + p.set(p.offseti, "-07") + case 5: + // 19:55:00+0100 + p.set(p.offseti, "-0700") + } + + case timeWsOffset: + p.set(p.offseti, "-0700") + case timeWsOffsetWs: + // 17:57:51 -0700 2009 + // 00:12:00 +0000 UTC + if p.tzi > 0 { + switch len(p.datestr) - p.tzi { + case 3: + // 13:31:51.999 +01:00 CET + p.set(p.tzi, "MST") + case 4: + // 13:31:51.999 +01:00 CEST + p.set(p.tzi, "MST ") + } + + } + case timeWsOffsetColon: + // 17:57:51 -07:00 + p.set(p.offseti, "-07:00") + case timeOffsetColon: + // 15:04:05+07:00 + p.set(p.offseti, "-07:00") + case timePeriodOffset: + // 19:55:00.799+0100 + p.set(p.offseti, "-0700") + case timePeriodOffsetColon: + p.set(p.offseti, "-07:00") + case timePeriodWsOffsetColonAlpha: + p.tzlen = i - p.tzi + switch p.tzlen { + case 3: + p.set(p.tzi, "MST") + case 4: + p.set(p.tzi, "MST ") + } + case timePeriodWsOffset: + p.set(p.offseti, "-0700") + } + p.coalesceTime(i) + } + + switch p.stateDate { + case dateDigit: + // unixy timestamps ish + // example ct type + // 1499979655583057426 19 nanoseconds + // 1499979795437000 16 micro-seconds + // 20180722105203 14 yyyyMMddhhmmss + // 1499979795437 13 milliseconds + // 1332151919 10 seconds + // 20140601 8 yyyymmdd + // 2014 4 yyyy + t := time.Time{} + if len(datestr) == len("1499979655583057426") { // 19 + // nano-seconds + if nanoSecs, err := strconv.ParseInt(datestr, 10, 64); err == nil { + t = time.Unix(0, nanoSecs) + } + } else if len(datestr) == len("1499979795437000") { // 16 + // micro-seconds + if microSecs, err := strconv.ParseInt(datestr, 10, 64); err == nil { + t = time.Unix(0, microSecs*1000) + } + } else if len(datestr) == len("yyyyMMddhhmmss") { // 14 + // yyyyMMddhhmmss + p.format = []byte("20060102150405") + return p, nil + } else if len(datestr) == len("1332151919000") { // 13 + if miliSecs, err := strconv.ParseInt(datestr, 10, 64); err == nil { + t = time.Unix(0, miliSecs*1000*1000) + } + } else if len(datestr) == len("1332151919") { //10 + if secs, err := strconv.ParseInt(datestr, 10, 64); err == nil { + t = time.Unix(secs, 0) + } + } else if len(datestr) == len("20140601") { + p.format = []byte("20060102") + return p, nil + } else if len(datestr) == len("2014") { + p.format = []byte("2006") + return p, nil + } else if len(datestr) < 4 { + return nil, fmt.Errorf("unrecognized format, too short %v", datestr) + } + if !t.IsZero() { + if loc == nil { + p.t = &t + return p, nil + } + t = t.In(loc) + p.t = &t + return p, nil + } + case dateDigitSt: + // 171113 14:14:20 + return p, nil + + case dateYearDash: + // 2006-01 + return p, nil + + case dateYearDashDash: + // 2006-01-02 + // 2006-1-02 + // 2006-1-2 + // 2006-01-2 + return p, nil + + case dateYearDashDashOffset: + /// 2020-07-20+00:00 + switch len(p.datestr) - p.offseti { + case 5: + p.set(p.offseti, "-0700") + case 6: + p.set(p.offseti, "-07:00") + } + return p, nil + + case dateYearDashAlphaDash: + // 2013-Feb-03 + // 2013-Feb-3 + p.daylen = i - p.dayi + p.setDay() + return p, nil + + case dateYearDashDashWs: + // 2013-04-01 + return p, nil + + case dateYearDashDashT: + return p, nil + + case dateDigitDashAlphaDash: + // 13-Feb-03 ambiguous + // 28-Feb-03 ambiguous + // 29-Jun-2016 + length := len(datestr) - (p.moi + p.molen + 1) + if length == 4 { + p.yearlen = 4 + p.set(p.yeari, "2006") + // We now also know that part1 was the day + p.dayi = 0 + p.daylen = p.part1Len + p.setDay() + } else if length == 2 { + // We have no idea if this is + // yy-mon-dd OR dd-mon-yy + // + // We are going to ASSUME (bad, bad) that it is dd-mon-yy which is a horible assumption + p.ambiguousMD = true + p.yearlen = 2 + p.set(p.yeari, "06") + // We now also know that part1 was the day + p.dayi = 0 + p.daylen = p.part1Len + p.setDay() + } + + return p, nil + + case dateDigitDot: + // 2014.05 + p.molen = i - p.moi + p.setMonth() + return p, nil + + case dateDigitDotDot: + // 03.31.1981 + // 3.31.2014 + // 3.2.1981 + // 3.2.81 + // 08.21.71 + // 2018.09.30 + return p, nil + + case dateDigitWsMoYear: + // 2 Jan 2018 + // 2 Jan 18 + // 2 Jan 2018 23:59 + // 02 Jan 2018 23:59 + // 12 Feb 2006, 19:17 + return p, nil + + case dateDigitWsMolong: + // 18 January 2018 + // 8 January 2018 + if p.daylen == 2 { + p.format = []byte("02 January 2006") + return p, nil + } + p.format = []byte("2 January 2006") + return p, nil // parse("2 January 2006", datestr, loc) + + case dateAlphaWsMonth: + p.yearlen = i - p.yeari + p.setYear() + return p, nil + + case dateAlphaWsMonthMore: + return p, nil + + case dateAlphaWsDigitMoreWs: + // oct 1, 1970 + p.yearlen = i - p.yeari + p.setYear() + return p, nil + + case dateAlphaWsDigitMoreWsYear: + // May 8, 2009 5:57:51 PM + // Jun 7, 2005, 05:57:51 + return p, nil + + case dateAlphaWsAlpha: + return p, nil + + case dateAlphaWsDigit: + return p, nil + + case dateAlphaWsDigitYearmaybe: + return p, nil + + case dateDigitSlash: + // 3/1/2014 + // 10/13/2014 + // 01/02/2006 + return p, nil + + case dateDigitSlashAlpha: + // 03/Jun/2014 + return p, nil + + case dateDigitYearSlash: + // 2014/10/13 + return p, nil + + case dateDigitColon: + // 3:1:2014 + // 10:13:2014 + // 01:02:2006 + // 2014:10:13 + return p, nil + + case dateDigitChineseYear: + // dateDigitChineseYear + // 2014年04月08日 + p.format = []byte("2006年01月02日") + return p, nil + + case dateDigitChineseYearWs: + p.format = []byte("2006年01月02日 15:04:05") + return p, nil + + case dateWeekdayComma: + // Monday, 02 Jan 2006 15:04:05 -0700 + // Monday, 02 Jan 2006 15:04:05 +0100 + // Monday, 02-Jan-06 15:04:05 MST + return p, nil + + case dateWeekdayAbbrevComma: + // Mon, 02-Jan-06 15:04:05 MST + // Mon, 02 Jan 2006 15:04:05 MST + return p, nil + + } + + return nil, unknownErr(datestr) +} + +type parser struct { + loc *time.Location + preferMonthFirst bool + retryAmbiguousDateWithSwap bool + ambiguousMD bool + stateDate dateState + stateTime timeState + format []byte + datestr string + fullMonth string + skip int + extra int + part1Len int + yeari int + yearlen int + moi int + molen int + dayi int + daylen int + houri int + hourlen int + mini int + minlen int + seci int + seclen int + msi int + mslen int + offseti int + offsetlen int + tzi int + tzlen int + t *time.Time +} + +// ParserOption defines a function signature implemented by options +// Options defined like this accept the parser and operate on the data within +type ParserOption func(*parser) error + +// PreferMonthFirst is an option that allows preferMonthFirst to be changed from its default +func PreferMonthFirst(preferMonthFirst bool) ParserOption { + return func(p *parser) error { + p.preferMonthFirst = preferMonthFirst + return nil + } +} + +// RetryAmbiguousDateWithSwap is an option that allows retryAmbiguousDateWithSwap to be changed from its default +func RetryAmbiguousDateWithSwap(retryAmbiguousDateWithSwap bool) ParserOption { + return func(p *parser) error { + p.retryAmbiguousDateWithSwap = retryAmbiguousDateWithSwap + return nil + } +} + +func newParser(dateStr string, loc *time.Location, opts ...ParserOption) *parser { + p := &parser{ + stateDate: dateStart, + stateTime: timeIgnore, + datestr: dateStr, + loc: loc, + preferMonthFirst: true, + retryAmbiguousDateWithSwap: false, + } + p.format = []byte(dateStr) + + // allow the options to mutate the parser fields from their defaults + for _, option := range opts { + option(p) + } + return p +} + +func (p *parser) nextIs(i int, b byte) bool { + if len(p.datestr) > i+1 && p.datestr[i+1] == b { + return true + } + return false +} + +func (p *parser) set(start int, val string) { + if start < 0 { + return + } + if len(p.format) < start+len(val) { + return + } + for i, r := range val { + p.format[start+i] = byte(r) + } +} +func (p *parser) setMonth() { + if p.molen == 2 { + p.set(p.moi, "01") + } else if p.molen == 1 { + p.set(p.moi, "1") + } +} + +func (p *parser) setDay() { + if p.daylen == 2 { + p.set(p.dayi, "02") + } else if p.daylen == 1 { + p.set(p.dayi, "2") + } +} +func (p *parser) setYear() { + if p.yearlen == 2 { + p.set(p.yeari, "06") + } else if p.yearlen == 4 { + p.set(p.yeari, "2006") + } +} +func (p *parser) coalesceDate(end int) { + if p.yeari > 0 { + if p.yearlen == 0 { + p.yearlen = end - p.yeari + } + p.setYear() + } + if p.moi > 0 && p.molen == 0 { + p.molen = end - p.moi + p.setMonth() + } + if p.dayi > 0 && p.daylen == 0 { + p.daylen = end - p.dayi + p.setDay() + } +} +func (p *parser) ts() string { + return fmt.Sprintf("h:(%d:%d) m:(%d:%d) s:(%d:%d)", p.houri, p.hourlen, p.mini, p.minlen, p.seci, p.seclen) +} +func (p *parser) ds() string { + return fmt.Sprintf("%s d:(%d:%d) m:(%d:%d) y:(%d:%d)", p.datestr, p.dayi, p.daylen, p.moi, p.molen, p.yeari, p.yearlen) +} +func (p *parser) coalesceTime(end int) { + // 03:04:05 + // 15:04:05 + // 3:04:05 + // 3:4:5 + // 15:04:05.00 + if p.houri > 0 { + if p.hourlen == 2 { + p.set(p.houri, "15") + } else if p.hourlen == 1 { + p.set(p.houri, "3") + } + } + if p.mini > 0 { + if p.minlen == 0 { + p.minlen = end - p.mini + } + if p.minlen == 2 { + p.set(p.mini, "04") + } else { + p.set(p.mini, "4") + } + } + if p.seci > 0 { + if p.seclen == 0 { + p.seclen = end - p.seci + } + if p.seclen == 2 { + p.set(p.seci, "05") + } else { + p.set(p.seci, "5") + } + } + + if p.msi > 0 { + for i := 0; i < p.mslen; i++ { + p.format[p.msi+i] = '0' + } + } +} +func (p *parser) setFullMonth(month string) { + if p.moi == 0 { + p.format = []byte(fmt.Sprintf("%s%s", "January", p.format[len(month):])) + } +} + +func (p *parser) trimExtra() { + if p.extra > 0 && len(p.format) > p.extra { + p.format = p.format[0:p.extra] + p.datestr = p.datestr[0:p.extra] + } +} + +// func (p *parser) remove(i, length int) { +// if len(p.format) > i+length { +// //append(a[:i], a[j:]...) +// p.format = append(p.format[0:i], p.format[i+length:]...) +// } +// if len(p.datestr) > i+length { +// //append(a[:i], a[j:]...) +// p.datestr = fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+length:]) +// } +// } + +func (p *parser) parse() (time.Time, error) { + if p.t != nil { + return *p.t, nil + } + if len(p.fullMonth) > 0 { + p.setFullMonth(p.fullMonth) + } + if p.skip > 0 && len(p.format) > p.skip { + p.format = p.format[p.skip:] + p.datestr = p.datestr[p.skip:] + } + + if p.loc == nil { + // gou.Debugf("parse layout=%q input=%q \ntx, err := time.Parse(%q, %q)", string(p.format), p.datestr, string(p.format), p.datestr) + return time.Parse(string(p.format), p.datestr) + } + //gou.Debugf("parse layout=%q input=%q \ntx, err := time.ParseInLocation(%q, %q, %v)", string(p.format), p.datestr, string(p.format), p.datestr, p.loc) + return time.ParseInLocation(string(p.format), p.datestr, p.loc) +} +func isDay(alpha string) bool { + for _, day := range days { + if alpha == day { + return true + } + } + return false +} +func isMonthFull(alpha string) bool { + for _, month := range months { + if alpha == month { + return true + } + } + return false +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 96d87a5..a1d61b1 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,6 +1,9 @@ # github.com/amoghe/distillog v0.0.0-20180726233512-ae382b35b717 ## explicit github.com/amoghe/distillog +# github.com/araddon/dateparse v0.0.0-20210207001429-0eec95c9db7e +## explicit +github.com/araddon/dateparse # github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible ## explicit # github.com/lazada/sqle v0.0.0-20171211164427-f1ca64d42ef4