Gisle Aune
6 years ago
6 changed files with 358 additions and 24 deletions
-
42Gopkg.lock
-
3Gopkg.toml
-
48internal/importers/mirclike/log.go
-
69internal/importers/mirclike/log_test.go
-
92internal/importers/mirclike/post.go
-
126internal/importers/mirclike/post_test.go
@ -0,0 +1,48 @@ |
|||||
|
package mirclike |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"strings" |
||||
|
"time" |
||||
|
|
||||
|
"git.aiterp.net/rpdata/api/models" |
||||
|
) |
||||
|
|
||||
|
// ErrEmptyLog is returned by ParseLog if there are no (valid) posts in the log.
|
||||
|
var ErrEmptyLog = errors.New("No valid posts found in log") |
||||
|
|
||||
|
// ParseLog parses the log and returns the things that can be gleamed from them.
|
||||
|
func ParseLog(data string, date time.Time, strict bool) (models.Log, []models.Post, error) { |
||||
|
lines := strings.Split(data, "\n") |
||||
|
posts := make([]models.Post, 0, len(lines)) |
||||
|
prev := models.Post{} |
||||
|
|
||||
|
for _, line := range lines { |
||||
|
line = strings.Trim(line, "\r\t ") |
||||
|
if len(line) < 1 { |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
post, err := ParsePost(line, date, prev) |
||||
|
if err != nil { |
||||
|
if strict { |
||||
|
return models.Log{}, nil, err |
||||
|
} |
||||
|
|
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
posts = append(posts, post) |
||||
|
prev = post |
||||
|
} |
||||
|
|
||||
|
if len(posts) == 0 { |
||||
|
return models.Log{}, nil, ErrEmptyLog |
||||
|
} |
||||
|
|
||||
|
log := models.Log{ |
||||
|
Date: posts[0].Time, |
||||
|
} |
||||
|
|
||||
|
return log, posts, nil |
||||
|
} |
@ -0,0 +1,69 @@ |
|||||
|
package mirclike_test |
||||
|
|
||||
|
import ( |
||||
|
"strings" |
||||
|
"testing" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/stretchr/testify/assert" |
||||
|
|
||||
|
"git.aiterp.net/rpdata/api/internal/importers/mirclike" |
||||
|
"git.aiterp.net/rpdata/api/models" |
||||
|
) |
||||
|
|
||||
|
var testLog = strings.Join([]string{ |
||||
|
"[11:21] * Va`ynna_Atana returns to the apartment at the end of another long day. She hangs up her jacket and a green scarf next to the door before going straight to the bathroom; she leaves the door open, though. She walked home with Uvena this time, but the two parted ways downstairs.", |
||||
|
"", |
||||
|
"[11:21] <=Scene=> The weather outside is not pleasant in the slightest. The storm is still going on, visibility is reduced and the worst gusts of wind can be felt inside the apartment as a faint shudder. ", |
||||
|
"", |
||||
|
"[11:", |
||||
|
" [11:27] <Test> Stuff and things.", |
||||
|
}, "\r\n") |
||||
|
|
||||
|
var testLogPosts = []models.Post{ |
||||
|
{ |
||||
|
ID: "UNASSIGNED", |
||||
|
LogID: "UNASSIGNED", |
||||
|
Time: parseDate(nil, "2018-05-11 11:21:00"), |
||||
|
Kind: "action", |
||||
|
Nick: "Va`ynna_Atana", |
||||
|
Position: 1, |
||||
|
Text: "returns to the apartment at the end of another long day. She hangs up her jacket and a green scarf next to the door before going straight to the bathroom; she leaves the door open, though. She walked home with Uvena this time, but the two parted ways downstairs.", |
||||
|
}, |
||||
|
{ |
||||
|
ID: "UNASSIGNED", |
||||
|
LogID: "UNASSIGNED", |
||||
|
Time: parseDate(nil, "2018-05-11 11:21:00"), |
||||
|
Kind: "scene", |
||||
|
Nick: "=Scene=", |
||||
|
Position: 2, |
||||
|
Text: "The weather outside is not pleasant in the slightest. The storm is still going on, visibility is reduced and the worst gusts of wind can be felt inside the apartment as a faint shudder.", |
||||
|
}, |
||||
|
{ |
||||
|
ID: "UNASSIGNED", |
||||
|
LogID: "UNASSIGNED", |
||||
|
Time: parseDate(nil, "2018-05-11 11:27:00"), |
||||
|
Kind: "text", |
||||
|
Nick: "Test", |
||||
|
Position: 3, |
||||
|
Text: "Stuff and things.", |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
func TestParseLog(t *testing.T) { |
||||
|
log, posts, err := mirclike.ParseLog(testLog, parseDate(t, "2018-05-11 00:00:00"), false) |
||||
|
if err != nil { |
||||
|
t.Fatal("ParseLog", err) |
||||
|
} |
||||
|
|
||||
|
assert.Equal(t, testLogPosts, posts) |
||||
|
assert.Equal(t, posts[0].Time, log.Date, "Log's date should be the first post's.") |
||||
|
} |
||||
|
|
||||
|
func TestParseLogErrors(t *testing.T) { |
||||
|
_, _, err1 := mirclike.ParseLog("\n\n\n\n\n \n\t\r\n", time.Time{}, false) |
||||
|
_, _, err2 := mirclike.ParseLog("\n\n\n\n\n[14:57]* Stuff \n\t\r\n", time.Time{}, true) |
||||
|
|
||||
|
assert.Equal(t, mirclike.ErrEmptyLog, err1) |
||||
|
assert.Equal(t, mirclike.ErrNotPost, err2) |
||||
|
} |
@ -0,0 +1,92 @@ |
|||||
|
package mirclike |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"strconv" |
||||
|
"strings" |
||||
|
"time" |
||||
|
|
||||
|
"git.aiterp.net/rpdata/api/models" |
||||
|
) |
||||
|
|
||||
|
// ErrNotPost is returned by parsePost if the line is empty or not a post.
|
||||
|
var ErrNotPost = errors.New("not a post") |
||||
|
|
||||
|
// ParsePost parses a post from a mirc-like line. If the previous post is included (it can be empty), it will be used
|
||||
|
// to determine whether midnight has passed.
|
||||
|
func ParsePost(line string, date time.Time, prev models.Post) (models.Post, error) { |
||||
|
// Do basic validation
|
||||
|
line = strings.Trim(line, " \t\n\r") |
||||
|
if len(line) == 0 || !strings.HasPrefix(line, "[") { |
||||
|
return models.Post{}, ErrNotPost |
||||
|
} |
||||
|
|
||||
|
// Parse timestamp
|
||||
|
tsEndIndex := strings.IndexByte(line, ']') |
||||
|
if tsEndIndex == -1 || len(line) < tsEndIndex+5 { |
||||
|
return models.Post{}, ErrNotPost |
||||
|
} |
||||
|
tsStr := line[1:tsEndIndex] |
||||
|
tsSplit := strings.Split(tsStr, ":") |
||||
|
tsUnits := make([]int, len(tsSplit)) |
||||
|
if len(tsSplit) < 2 { |
||||
|
return models.Post{}, ErrNotPost |
||||
|
} |
||||
|
for i := range tsSplit { |
||||
|
n, err := strconv.Atoi(tsSplit[i]) |
||||
|
if err != nil { |
||||
|
return models.Post{}, ErrNotPost |
||||
|
} |
||||
|
|
||||
|
tsUnits[i] = n |
||||
|
} |
||||
|
if len(tsUnits) == 2 { |
||||
|
tsUnits = append(tsUnits, 0) |
||||
|
} |
||||
|
|
||||
|
// Determine timestamp from parsed data and previous post.
|
||||
|
ts := time.Date(date.Year(), date.Month(), date.Day(), tsUnits[0], tsUnits[1], tsUnits[2], 0, date.Location()) |
||||
|
if !prev.Time.IsZero() && prev.Time.Sub(ts) > 30*time.Minute { |
||||
|
ts = time.Date(prev.Time.Year(), prev.Time.Month(), prev.Time.Day()+1, tsUnits[0], tsUnits[1], tsUnits[2], 0, date.Location()) |
||||
|
} |
||||
|
|
||||
|
if line[tsEndIndex+2] == '*' { |
||||
|
split := strings.SplitN(line[tsEndIndex+4:], " ", 2) |
||||
|
|
||||
|
post := models.Post{ |
||||
|
ID: "UNASSIGNED", |
||||
|
LogID: "UNASSIGNED", |
||||
|
Time: ts, |
||||
|
Kind: "action", |
||||
|
Nick: strings.TrimLeft(split[0], "+@!~"), |
||||
|
Text: split[1], |
||||
|
Position: prev.Position + 1, |
||||
|
} |
||||
|
|
||||
|
if post.Nick[0] == '=' { |
||||
|
post.Kind = "scene" |
||||
|
} |
||||
|
|
||||
|
return post, nil |
||||
|
} else if line[tsEndIndex+2] == '<' { |
||||
|
split := strings.SplitN(line[tsEndIndex+2:], " ", 2) |
||||
|
|
||||
|
post := models.Post{ |
||||
|
ID: "UNASSIGNED", |
||||
|
LogID: "UNASSIGNED", |
||||
|
Time: ts, |
||||
|
Kind: "text", |
||||
|
Nick: strings.TrimLeft(split[0][1:len(split[0])-1], "+@!~"), |
||||
|
Text: split[1], |
||||
|
Position: prev.Position + 1, |
||||
|
} |
||||
|
|
||||
|
if post.Nick[0] == '=' { |
||||
|
post.Kind = "scene" |
||||
|
} |
||||
|
|
||||
|
return post, nil |
||||
|
} else { |
||||
|
return models.Post{}, ErrNotPost |
||||
|
} |
||||
|
} |
@ -0,0 +1,126 @@ |
|||||
|
package mirclike_test |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"testing" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/stretchr/testify/assert" |
||||
|
|
||||
|
"git.aiterp.net/rpdata/api/internal/importers/mirclike" |
||||
|
"git.aiterp.net/rpdata/api/models" |
||||
|
) |
||||
|
|
||||
|
func TestParsePost(t *testing.T) { |
||||
|
table := []struct { |
||||
|
Input string |
||||
|
TS string |
||||
|
Kind string |
||||
|
Nick string |
||||
|
Text string |
||||
|
}{ |
||||
|
{ |
||||
|
"[12:34] * Stuff does things.", |
||||
|
"12:34:00", "action", "Stuff", "does things.", |
||||
|
}, |
||||
|
{ |
||||
|
"[12:34] <Stuff> Things said.", |
||||
|
"12:34:00", "text", "Stuff", "Things said.", |
||||
|
}, |
||||
|
{ |
||||
|
"[13:36:59] <Stuff> Things said.", |
||||
|
"13:36:59", "text", "Stuff", "Things said.", |
||||
|
}, |
||||
|
{ |
||||
|
"[23:59] <=Scene=> Scenery and such.", |
||||
|
"23:59:00", "scene", "=Scene=", "Scenery and such.", |
||||
|
}, |
||||
|
{ |
||||
|
"[01:10:11] * =Scene= Scenery and such from the forum or mIRC using my old script.", |
||||
|
"01:10:11", "scene", "=Scene=", "Scenery and such from the forum or mIRC using my old script.", |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
for i, row := range table { |
||||
|
t.Run(fmt.Sprintf("Row_%d", i), func(t *testing.T) { |
||||
|
post, err := mirclike.ParsePost(row.Input, time.Now(), models.Post{}) |
||||
|
if err != nil { |
||||
|
t.Fatal("Could not parse post:", err) |
||||
|
} |
||||
|
|
||||
|
assert.Equal(t, row.TS, post.Time.Format("15:04:05"), "Timestamps should match.") |
||||
|
assert.Equal(t, row.Kind, post.Kind, "Kinds should match.") |
||||
|
assert.Equal(t, row.Nick, post.Nick, "Kinds should match.") |
||||
|
assert.Equal(t, row.Text, post.Text, "Kinds should match.") |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestParsePostErrors(t *testing.T) { |
||||
|
table := []struct { |
||||
|
Input string |
||||
|
Err error |
||||
|
}{ |
||||
|
{"[12:34] <Stuff> Things said.", nil}, |
||||
|
{"[12:34] >Stuff> Things said.", mirclike.ErrNotPost}, |
||||
|
{"12:34] <Stuff> Things said.", mirclike.ErrNotPost}, |
||||
|
{"* Stuff Things said.", mirclike.ErrNotPost}, |
||||
|
{"", mirclike.ErrNotPost}, |
||||
|
{"[12:34 <Stuff> Things said.", mirclike.ErrNotPost}, |
||||
|
{"[TE:XT] <Stuff> Things said.", mirclike.ErrNotPost}, |
||||
|
{"[10] <Stuff> Things said.", mirclike.ErrNotPost}, |
||||
|
{"[12:34:56:789] <Stuff> Things said.", nil}, |
||||
|
{"[12:34:56.789] <Stuff> Things said.", mirclike.ErrNotPost}, |
||||
|
} |
||||
|
|
||||
|
for i, row := range table { |
||||
|
t.Run(fmt.Sprintf("Row_%d", i), func(t *testing.T) { |
||||
|
_, err := mirclike.ParsePost(row.Input, time.Now(), models.Post{}) |
||||
|
|
||||
|
assert.Equal(t, row.Err, err, "Error should match") |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestParseNextDay(t *testing.T) { |
||||
|
table := []struct { |
||||
|
Prev time.Time |
||||
|
TS string |
||||
|
Time time.Time |
||||
|
}{ |
||||
|
{Prev: parseDate(t, "2019-01-12 12:00:00"), TS: "12:01", Time: parseDate(t, "2019-01-12 12:01:00")}, |
||||
|
{Prev: parseDate(t, "2019-01-12 12:00:00"), TS: "11:53:13", Time: parseDate(t, "2019-01-12 11:53:13")}, |
||||
|
{Prev: parseDate(t, "2019-04-08 23:51:59"), TS: "00:09", Time: parseDate(t, "2019-04-09 00:09:00")}, |
||||
|
{Prev: parseDate(t, "2019-01-12 12:00:00"), TS: "11:29:59", Time: parseDate(t, "2019-01-13 11:29:59")}, |
||||
|
} |
||||
|
|
||||
|
for i, row := range table { |
||||
|
t.Run(fmt.Sprintf("Row_%d", i), func(t *testing.T) { |
||||
|
input := fmt.Sprintf("[%s] * Stuff does things.", row.TS) |
||||
|
|
||||
|
post, err := mirclike.ParsePost(input, row.Prev, models.Post{Time: row.Prev}) |
||||
|
if err != nil { |
||||
|
t.Fatal("Could not parse post:", err) |
||||
|
} |
||||
|
|
||||
|
assert.Equal(t, row.Time, post.Time) |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func parseDate(t *testing.T, date string) time.Time { |
||||
|
result, err := time.Parse("2006-01-02 15:04:05", date) |
||||
|
if err != nil { |
||||
|
if t != nil { |
||||
|
t.Fatal("Could not parse date", date, err) |
||||
|
} else { |
||||
|
panic("Could not parse date: " + err.Error()) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result |
||||
|
} |
||||
|
|
||||
|
func formatDate(date time.Time) string { |
||||
|
return date.UTC().Format("2006-01-02 15:04:05") |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue