Date formatting.

This commit is contained in:
Nuno Cruces 2022-03-30 15:36:50 +01:00
parent f21574baa6
commit 0086cdcbf0
11 changed files with 337 additions and 228 deletions

View file

@ -18,7 +18,7 @@ go run github.com/randall77/makefat zenity zenity_macos_x64 zenity_macos_arm &&
zip -9 zenity_macos.zip zenity
zip -9 zenity_brew.zip zenity zenity.exe
rm zenity zenity_macos_* zenity.exe
GOOS=linux go build -tags dev
go build -tags dev

View file

@ -19,6 +19,7 @@ import (
"time"
"github.com/ncruces/zenity"
"github.com/ncruces/zenity/internal/strftime"
"github.com/ncruces/zenity/internal/zenutil"
)
@ -522,7 +523,7 @@ func lstResult(l []string, err error) {
func calResult(d time.Time, err error) {
errResult(err)
os.Stdout.WriteString(zenutil.Strftime(zenutil.DateFormat, d))
os.Stdout.WriteString(strftime.Format(zenutil.DateFormat, d))
os.Stdout.WriteString(zenutil.LineBreak)
}

View file

@ -3,16 +3,21 @@ package zenity
import (
"time"
"github.com/ncruces/zenity/internal/strftime"
"github.com/ncruces/zenity/internal/zenutil"
)
func calendar(text string, opts options) (time.Time, error) {
func calendar(text string, opts options) (t time.Time, err error) {
var date zenutil.Date
date.OK, date.Cancel, date.Extra = getAlertButtons(opts)
date.Format = zenutil.StrftimeUTS35(zenutil.DateFormat)
date.Format, err = strftime.UTS35(zenutil.DateFormat)
if err != nil {
return
}
if opts.time != nil {
date.Date = opts.time.Unix()
unix := opts.time.Unix()
date.Date = &unix
}
if opts.title != nil {
@ -25,8 +30,7 @@ func calendar(text string, opts options) (time.Time, error) {
out, err := zenutil.Run(opts.ctx, "date", date)
str, err := strResult(opts, out, err)
if err != nil {
return time.Time{}, err
return
}
layout := zenutil.StrftimeLayout(zenutil.DateFormat)
return time.Parse(layout, str)
return strftime.Parse(zenutil.DateFormat, str)
}

View file

@ -6,6 +6,7 @@ import (
"strconv"
"time"
"github.com/ncruces/zenity/internal/strftime"
"github.com/ncruces/zenity/internal/zenutil"
)
@ -27,6 +28,5 @@ func calendar(text string, opts options) (time.Time, error) {
if err != nil {
return time.Time{}, err
}
layout := zenutil.StrftimeLayout(zenutil.DateFormat)
return time.Parse(layout, str)
return strftime.Parse(zenutil.DateFormat, str)
}

View file

@ -0,0 +1,65 @@
package strftime
type parser struct {
fmt string
specs map[byte]string
unpadded map[byte]string
writeLit func(byte) error
writeFmt func(string) error
fallback func(spec byte, pad bool) error
}
func (p *parser) parse() error {
const (
initial = iota
specifier
nopadding
)
var err error
state := initial
for _, b := range []byte(p.fmt) {
switch state {
default:
if b == '%' {
state = specifier
} else {
err = p.writeLit(b)
}
case specifier:
if b == '-' {
state = nopadding
continue
}
if s, ok := p.specs[b]; ok {
err = p.writeFmt(s)
} else {
err = p.fallback(b, true)
}
state = initial
case nopadding:
if s, ok := p.unpadded[b]; ok {
err = p.writeFmt(s)
} else if s, ok := p.specs[b]; ok {
err = p.writeFmt(s)
} else {
err = p.fallback(b, false)
}
state = initial
}
if err != nil {
return err
}
}
switch state {
case specifier:
return p.writeLit('%')
case nopadding:
return p.writeLit('-')
}
return nil
}

View file

@ -0,0 +1,102 @@
package strftime
func (p *parser) goSpecifiers() {
// https://strftime.org/
p.specs = map[byte]string{
'B': "January",
'b': "Jan",
'h': "Jan",
'm': "01",
'A': "Monday",
'a': "Mon",
'e': "_2",
'd': "02",
'j': "002",
'H': "15",
'I': "03",
'M': "04",
'S': "05",
'Y': "2006",
'y': "06",
'p': "PM",
'Z': "MST",
'z': "-0700",
'L': "000",
'f': "000000",
'N': "000000000",
'+': "Mon Jan _2 15:04:05 MST 2006",
'c': "Mon Jan _2 15:04:05 2006",
'F': "2006-01-02",
'D': "01/02/06",
'x': "01/02/06",
'r': "03:04:05 PM",
'T': "15:04:05",
'X': "15:04:05",
'R': "15:04",
'%': "%",
't': "\t",
'n': "\n",
}
p.unpadded = map[byte]string{
'm': "1",
'd': "2",
'I': "3",
'M': "4",
'S': "5",
}
}
func (p *parser) uts35Specifiers() { // https://nsdateformatter.com/
p.specs = map[byte]string{
'B': "MMMM",
'b': "MMM",
'h': "MMM",
'm': "MM",
'A': "EEEE",
'a': "E",
'd': "dd",
'j': "DDD",
'H': "HH",
'I': "hh",
'M': "mm",
'S': "ss",
'Y': "yyyy",
'y': "yy",
'G': "YYYY",
'g': "YY",
'V': "ww",
'p': "a",
'Z': "zzz",
'z': "Z",
'L': "SSS",
'f': "SSSSSS",
'N': "SSSSSSSSS",
'+': "E MMM d HH:mm:ss zzz yyyy",
'c': "E MMM d HH:mm:ss yyyy",
'F': "yyyy-MM-dd",
'D': "MM/dd/yy",
'x': "MM/dd/yy",
'r': "hh:mm:ss a",
'T': "HH:mm:ss",
'X': "HH:mm:ss",
'R': "HH:mm",
'%': "%",
't': "\t",
'n': "\n",
}
p.unpadded = map[byte]string{
'm': "M",
'd': "d",
'j': "D",
'H': "H",
'I': "h",
'M': "m",
'S': "s",
}
}

View file

@ -0,0 +1,148 @@
package strftime
import (
"bytes"
"errors"
"strconv"
"strings"
"time"
)
func Format(fmt string, t time.Time) string {
var res strings.Builder
var parser parser
parser.fmt = fmt
parser.goSpecifiers()
parser.writeLit = res.WriteByte
parser.writeFmt = func(fmt string) error {
switch fmt {
default:
res.WriteString(t.Format(fmt))
case "000", "000000", "000000000":
res.WriteString(t.Format("." + fmt)[1:])
}
return nil
}
parser.fallback = func(spec byte, pad bool) error {
switch spec {
default:
return errors.New("strftime: unsupported specifier: %" + string(spec))
case 'C':
s := t.Format("2006")
res.WriteString(s[:len(s)-2])
case 'g':
y, _ := t.ISOWeek()
res.WriteString(time.Date(y, 1, 1, 0, 0, 0, 0, time.UTC).Format("06"))
case 'G':
y, _ := t.ISOWeek()
res.WriteString(time.Date(y, 1, 1, 0, 0, 0, 0, time.UTC).Format("2006"))
case 'V':
_, w := t.ISOWeek()
if w < 10 && pad {
res.WriteByte('0')
}
res.WriteString(strconv.Itoa(w))
case 'w':
w := int(t.Weekday())
res.WriteString(strconv.Itoa(w))
case 'u':
if w := int(t.Weekday()); w == 0 {
res.WriteByte('7')
} else {
res.WriteString(strconv.Itoa(w))
}
}
return nil
}
parser.parse()
return res.String()
}
func Parse(fmt, value string) (time.Time, error) {
layout, err := Layout(fmt)
if err != nil {
return time.Time{}, err
}
return time.Parse(layout, value)
}
func Layout(fmt string) (string, error) {
var res strings.Builder
var parser parser
parser.fmt = fmt
parser.goSpecifiers()
parser.writeLit = func(b byte) error {
if bytes.IndexByte([]byte("MonJan_0123456789"), b) >= 0 {
return errors.New("strftime: unsupported literal: " + string(b))
}
res.WriteByte(b)
return nil
}
parser.writeFmt = func(s string) error {
res.WriteString(s)
return nil
}
parser.fallback = func(spec byte, pad bool) error {
return errors.New("strftime: unsupported specifier: %" + string(spec))
}
if err := parser.parse(); err != nil {
return "", err
}
parser.writeFmt("")
return res.String(), nil
}
func UTS35(fmt string) (string, error) {
var parser parser
parser.fmt = fmt
parser.uts35Specifiers()
const quote = '\''
var literal bool
var res strings.Builder
parser.writeLit = func(b byte) error {
if b == quote {
res.WriteByte(quote)
res.WriteByte(quote)
return nil
}
if !literal && ('a' <= b && b <= 'z' || 'A' <= b && b <= 'Z') {
literal = true
res.WriteByte(quote)
}
res.WriteByte(b)
return nil
}
parser.writeFmt = func(s string) error {
if literal {
literal = false
res.WriteByte(quote)
}
res.WriteString(s)
return nil
}
parser.fallback = func(spec byte, pad bool) error {
return errors.New("strftime: unsupported specifier: %" + string(spec))
}
if err := parser.parse(); err != nil {
return "", err
}
parser.writeFmt("")
return res.String(), nil
}

View file

@ -1,215 +0,0 @@
package zenutil
import (
"strings"
"time"
)
// Strftime is internal.
func Strftime(fmt string, t time.Time) string {
var res strings.Builder
writeLit := res.WriteByte
writeFmt := func(fmt string) (int, error) {
return res.WriteString(t.Format(fmt))
}
strftimeGo(fmt, writeLit, writeFmt)
return res.String()
}
// StrftimeLayout is internal.
func StrftimeLayout(fmt string) string {
var res strings.Builder
strftimeGo(fmt, res.WriteByte, res.WriteString)
return res.String()
}
func strftimeGo(fmt string, writeLit func(byte) error, writeFmt func(string) (int, error)) {
// https://strftime.org/
fmts := map[byte]string{
'B': "January",
'b': "Jan",
'h': "Jan",
'm': "01",
'A': "Monday",
'a': "Mon",
'e': "_2",
'd': "02",
'j': "002",
'H': "15",
'I': "03",
'M': "04",
'S': "05",
'Y': "2006",
'y': "06",
'p': "PM",
'Z': "MST",
'z': "-0700",
'L': "000",
'f': "000000",
'N': "000000000",
'+': "Mon Jan _2 15:04:05 MST 2006",
'c': "Mon Jan _2 15:04:05 2006",
'F': "2006-01-02",
'D': "01/02/06",
'x': "01/02/06",
'r': "03:04:05 PM",
'T': "15:04:05",
'X': "15:04:05",
'R': "15:04",
'%': "%",
't': "\t",
'n': LineBreak,
}
unpaded := map[byte]string{
'm': "1",
'd': "2",
'I': "3",
'M': "4",
'S': "5",
}
parser(fmt, fmts, unpaded, writeLit, writeFmt)
}
// StrftimeUTS35 is internal.
func StrftimeUTS35(fmt string) string {
// https://nsdateformatter.com/
fmts := map[byte]string{
'B': "MMMM",
'b': "MMM",
'h': "MMM",
'm': "MM",
'A': "EEEE",
'a': "E",
'd': "dd",
'j': "DDD",
'H': "HH",
'I': "hh",
'M': "mm",
'S': "ss",
'Y': "yyyy",
'y': "yy",
'G': "YYYY",
'g': "YY",
'V': "ww",
'p': "a",
'Z': "zzz",
'z': "Z",
'L': "SSS",
'f': "SSSSSS",
'N': "SSSSSSSSS",
'+': "E MMM d HH:mm:ss zzz yyyy",
'c': "E MMM d HH:mm:ss yyyy",
'F': "yyyy-MM-dd",
'D': "MM/dd/yy",
'x': "MM/dd/yy",
'r': "hh:mm:ss a",
'T': "HH:mm:ss",
'X': "HH:mm:ss",
'R': "HH:mm",
'%': "%",
't': "\t",
'n': LineBreak,
}
unpaded := map[byte]string{
'm': "M",
'd': "d",
'j': "D",
'H': "H",
'I': "h",
'M': "m",
'S': "s",
}
const quote = '\''
var literal bool
var res strings.Builder
writeLit := func(b byte) error {
if b == quote {
res.WriteByte(quote)
return res.WriteByte(quote)
}
if !literal && ('a' <= b && b <= 'z' || 'A' <= b && b <= 'Z') {
literal = true
res.WriteByte(quote)
}
return res.WriteByte(b)
}
writeFmt := func(s string) (int, error) {
if literal {
literal = false
res.WriteByte(quote)
}
return res.WriteString(s)
}
parser(fmt, fmts, unpaded, writeLit, writeFmt)
writeFmt("")
return res.String()
}
func parser(
fmt string,
formats, unpadded map[byte]string,
writeLit func(byte) error, writeFmt func(string) (int, error)) {
const (
initial = iota
special
padding
)
state := initial
for _, b := range []byte(fmt) {
switch state {
case initial:
if b == '%' {
state = special
} else {
writeLit(b)
}
case special:
if b == '-' {
state = padding
continue
}
if s, ok := formats[b]; ok {
writeFmt(s)
} else {
writeLit('%')
writeLit(b)
}
state = initial
case padding:
if s, ok := unpadded[b]; ok {
writeFmt(s)
} else if s, ok := formats[b]; ok {
writeFmt(s)
} else {
writeLit('%')
writeLit('-')
writeLit(b)
}
state = initial
}
}
switch state {
case padding:
writeLit('%')
fallthrough
case special:
writeLit('-')
}
}

View file

@ -29,8 +29,10 @@ ObjC.import('stdlib')
var date=$.NSDatePicker.alloc.init
date.setDatePickerStyle($.NSDatePickerStyleClockAndCalendar)
date.setDatePickerElements($.NSDatePickerElementFlagYearMonthDay)
date.setDateValue($.NSDate.dateWithTimeIntervalSince1970({{.Date}}))
date.setFrameSize(date.fittingSize)
{{- if .Date}}
date.setDateValue($.NSDate.dateWithTimeIntervalSince1970({{.Date}}))
{{- end}}
var alert=$.NSAlert.alloc.init
alert.setAccessoryView(date)
alert.setMessageText({{json .Text}})

View file

@ -9,8 +9,10 @@ ObjC.import('stdlib')
var date = $.NSDatePicker.alloc.init
date.setDatePickerStyle($.NSDatePickerStyleClockAndCalendar)
date.setDatePickerElements($.NSDatePickerElementFlagYearMonthDay)
date.setDateValue($.NSDate.dateWithTimeIntervalSince1970({{.Date}}))
date.setFrameSize(date.fittingSize)
{{- if .Date}}
date.setDateValue($.NSDate.dateWithTimeIntervalSince1970({{.Date}}))
{{- end}}
var alert = $.NSAlert.alloc.init
alert.setAccessoryView(date)

View file

@ -220,7 +220,7 @@ type Progress struct {
// Date is internal.
type Date struct {
Date int64
Date *int64
Text string
Info string
Format string