git.sophuwu.com > byterate
forgot to commit for the whole day.
made mostly everything.
sophuwu sophie@skisiel.com
Tue, 07 Jan 2025 02:04:38 +0100
commit

b8fab1cbb3bb757bc64b871b39cf1cf1fd2970f6

5 files changed, 362 insertions(+), 0 deletions(-)

jump to
A .gitignore

@@ -0,0 +1,3 @@

+.idea +build/ +byterate
A README.md

@@ -0,0 +1,89 @@

+# byterate + +Library and command line tool to calculate the time it takes to transfer a file at a given rate. + +## Library Usage + +```go +package main + +import ( + "fmt" + "sophuwu.site/byterate" +) + +func main() { + size, err := byterate.ParseSize("1gb") + if err != nil { + panic(err) + } + rate, err := byterate.ParseSize("1mbit") + if err != nil { + panic(err) + } + endTime, duration, err := byterate.Time(size, rate) + if err != nil { + panic(err) + } + fmt.Printf("End Time: %s\n", endTime) + fmt.Printf("Duration: %s\n", duration) + } +} +``` + +## Command Line Usage + +```sh +byterate [options] <size> <rate> +``` + +### Options: ++ `-h` `--help` Show the help message. ++ `-d` `--duration` Print the duration of the transfer. ++ `-t` `--time` Print the time the transfer will end. + +if no options are given, duration will be printed. + +### Arguments: ++ `<size>` is the size of the file to transfer. ++ `<rate>` is the transfer rate as a size, always per second. + +### Arguments Format: +`<size>` and `<rate>` are numbers with optional SI prefixes and units. + +#### Supported units: ++ `b` for bytes ++ `bit` for bits + +If no unit is given, bytes are assumed. + +#### SI Prefixes: +The prefixes are case-insensitive. The prefixes are in base 10 by default. To use base 2, add an `i` to the prefix. + +Base 10 prefixes: `k` `m` `g` `t` `p` `e` `z` `y` + +Base 2 prefixes: `ki` `mi` `gi` `ti` `pi` `ei` `zi` `yi` + +### Examples: + +Duration of 10 GiB at 120 mbps:\ +`byterate 10gib 120mbit` + +The completion time of 16 MiB at 1.2 MiB/s:\ +`byterate -t 16mib 1.2mib` + +The duration and completion time of 15 GB at 1.5 MB/s:\ +`byterate -dt 15g 1.5m` + +## Installation + +```sh +git clone sophuwu.site/byterate +cd byterate +go build -trimpath -ldflags="-s -w" -o byterate +sudo install byterate /usr/local/bin/byterate +``` + +## License + +MIT
A bytesize.go

@@ -0,0 +1,105 @@

+package byterate + +import ( + "errors" + "strings" + "time" +) + +type Size uint64 + +const Prefix = "bkmgtpezy" + +// parseFloat parses a float from a string and return the ending string +func parseFloat(s string) (float64, string, error) { + if len(s) < 1 { + return 0, s, errors.New("empty string") + } + var ( + f float64 = 0 + sign bool = false + c rune + i int + ) + if s[0] == '-' { + sign = true + s = s[1:] + } + for i, c = range s { + if c >= '0' && c <= '9' { + f = f*10 + float64(c-'0') + } else { + break + } + } + if s[i] == '.' { + s = s[i+1:] + var m float64 = 10 + for i, c = range s { + if c >= '0' && c <= '9' { + f += float64(c-'0') / m + m *= 10 + } else { + break + } + } + } + if sign { + f = -f + } + return f, s[i:], nil +} + +// ParseSize parses a string into a Size +func ParseSize(s string) (Size, error) { + if len(s) < 1 { + return 0, errors.New("empty string") + } + var ( + num float64 + err error + ) + num, s, err = parseFloat(s) + if err != nil { + return 0, err + } + s = strings.ToLower(s) + if strings.HasSuffix(s, "bit") { + s = strings.TrimPrefix(s, "bit") + } else { + num *= 8 + } + if len(s) > 0 { + i := strings.Index(Prefix, s[0:1]) + if i > 0 { + if strings.Contains(s, "i") { + i = 1 << (10 * i) + num *= float64(i) + } else { + for i > 0 { + num *= 1e3 + i-- + } + } + } + } + return Size(num), nil +} + +/* +Time returns the expected end time and duration of a transfer given the size and rate. +rate is size per second +*/ +func Time(size, rate Size) (endTime time.Time, duration time.Duration, err error) { + if rate == 0 { + err = errors.New("rate must be greater than 0") + return + } + if rate > size { + err = errors.New("rate must be less than Size") + return + } + duration = time.Duration(size/rate) * time.Second + endTime = time.Now().Add(duration) + return +}
A cmd/cli.go

@@ -0,0 +1,162 @@

+package main + +import ( + "fmt" + "os" + "sophuwu.site/byterate" + "strings" + "time" +) + +type inputs struct { + nums []string + help bool + dur bool + time bool +} + +func parseArgs(args []string) (i inputs, err error) { + opts := "" + for _, arg := range args { + if strings.HasPrefix(arg, "--") { + opts += arg[2:3] + } else if strings.HasPrefix(arg, "-") { + opts += arg[1:] + } else { + i.nums = append(i.nums, arg) + } + } + for _, opt := range opts { + switch opt { + case 'h': + i.help = true + case 'd': + i.dur = true + case 't': + i.time = true + } + } + if !i.dur && !i.time { + i.dur = true + } + if len(i.nums) != 2 { + err = fmt.Errorf("expected 2 arguments, got %d", len(i.nums)) + } + return +} + +func tailS(n int) string { + if n == 1 { + return "" + } + return "s" +} + +func main() { + var ( + s, r byterate.Size + t time.Time + d time.Duration + i inputs + err error + prnt string + ) + + i, err = parseArgs(os.Args[1:]) + + if i.help { + fmt.Println(strings.ReplaceAll(helpText, "{{ name }}", os.Args[0])) + return + } + + if err != nil { + goto bad + } + + s, err = byterate.ParseSize(i.nums[0]) + if err != nil { + goto bad + } + + r, err = byterate.ParseSize(i.nums[1]) + if err != nil { + goto bad + } + + t, d, err = byterate.Time(s, r) + if err != nil { + goto bad + } + + if i.dur { + if d.Hours() > 23 { + days := int(d.Hours() / 24) + if days > 365 { + years := days / 365 + prnt = fmt.Sprintf("%d year%s, ", years, tailS(years)) + days %= 365 + } + hrs := int(d.Hours()) % 24 + prnt += fmt.Sprintf("%d day%s, %02d hour%s, ", days, tailS(days), hrs, tailS(hrs)) + } else if d.Hours() >= 1 { + prnt = fmt.Sprintf("%02d hour%s, ", int(d.Hours()), tailS(int(d.Hours()))) + } + mins := int(d.Minutes()) % 60 + if prnt != "" || mins > 0 { + prnt += fmt.Sprintf("%02d minute%s and ", mins, tailS(mins)) + } + secs := int(d.Seconds()) % 60 + prnt += fmt.Sprintf("%02d second%s", secs, tailS(secs)) + fmt.Println(prnt) + } + if i.time { + prnt = "Today " + ndate := time.Now().Truncate(time.Hour * 24) + tdate := t.Truncate(time.Hour * 24) + if !tdate.Equal(ndate) { + if tdate.Equal(ndate.AddDate(0, 0, 1)) { + prnt = "Tomorrow " + } else if t.Before(ndate.AddDate(0, 0, 6)) { + prnt = t.Weekday().String() + " at " + prnt + } else if t.Year() == ndate.Year() { + prnt = "02 Jan " + } else { + prnt = "02 Jan 2006 " + } + } + fmt.Println(t.Format(prnt + "15:04")) + } + return + +bad: + fmt.Printf("Usage: %s <size> <rate>\n", os.Args[0]) + fmt.Println("\t--help for more information") +} + +const helpText = `{{ name }}: + Print the time it takes to transfer a file at a given rate. + + Usage: {{ name }} [options] <size> <rate> + + Options: + -h --help + Show this help message. + -d --duration + Print the duration the transfer will take. + -t --time + Print the time the transfer will finish if started now. + + if no options are given, duration will be printed. + + <size> is the size of the file to transfer. + <rate> is the transfer rate as a size, always per second. + + SI prefixes are supported (e.g. 1k = 1000, 1ki = 1024) + Supported units are: b for bytes, bit for bits. + If no unit is given, bytes are assumed. + + Examples: + 10 GiB at 120 mbps : {{ name }} 10gib 120mbit + 16 MiB at 1.2 MiB/s: {{ name }} 16mib 1.2mib + 15 GB at 1.5 MB/s : {{ name }} 1.5g 1.5m +`
A go.mod

@@ -0,0 +1,3 @@

+module sophuwu.site/byterate + +go 1.23.4