ソースを参照

Add Data + Number Formatting

joeybloggs 9 年 前
コミット
dafa33ab66
4 ファイル変更1143 行追加38 行削除
  1. 627 0
      calendar.go
  2. 373 0
      number.go
  3. 0 11
      plurals.go
  4. 143 27
      translator.go

+ 627 - 0
calendar.go

@@ -0,0 +1,627 @@
+package ut
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+	"time"
+)
+
+const (
+	am = "am"
+	pm = "pm"
+)
+
+// Standard Formats for Dates, Times & DateTimes
+// These are the options to pass to the Format method.
+const (
+	DateFormatFull = iota
+	DateFormatLong
+	DateFormatMedium
+	DateFormatShort
+	TimeFormatFull
+	TimeFormatLong
+	TimeFormatMedium
+	TimeFormatShort
+	DateTimeFormatFull
+	DateTimeFormatLong
+	DateTimeFormatMedium
+	DateTimeFormatShort
+)
+
+// Characters with special meaning in a datetime string:
+// Technically, all a-z,A-Z characters should be treated as if they represent a
+// datetime unit - but not all actually do. Any a-z,A-Z character that is
+// intended to be rendered as a literal a-z,A-Z character should be surrounded
+// by single quotes. There is currently no support for rendering a single quote
+// literal.
+const (
+	datetimeFormatUnitEra       = 'G'
+	datetimeFormatUnitYear      = 'y'
+	datetimeFormatUnitMonth     = 'M'
+	datetimeFormatUnitDayOfWeek = 'E'
+	datetimeFormatUnitDay       = 'd'
+	datetimeFormatUnitHour12    = 'h'
+	datetimeFormatUnitHour24    = 'H'
+	datetimeFormatUnitMinute    = 'm'
+	datetimeFormatUnitSecond    = 's'
+	datetimeFormatUnitPeriod    = 'a'
+	datetimeForamtUnitQuarter   = 'Q'
+	datetimeFormatUnitTimeZone1 = 'z'
+	datetimeFormatUnitTimeZone2 = 'v'
+
+	datetimeFormatTimeSeparator = ':'
+	datetimeFormatLiteral       = '\''
+)
+
+// The sequence length of datetime unit characters indicates how they should be
+// rendered.
+const (
+	datetimeFormatLength1Plus       = 1
+	datetimeFormatLength2Plus       = 2
+	datetimeFormatLengthAbbreviated = 3
+	datetimeFormatLengthWide        = 4
+	datetimeFormatLengthNarrow      = 5
+)
+
+// datetime formats are a sequences off datetime components and string literals
+const (
+	datetimePatternComponentUnit = iota
+	datetimePatternComponentLiteral
+)
+
+// A list of currently unsupported units:
+// These still need to be implemented. For now they are ignored.
+var (
+	datetimeFormatUnitCutset = []rune{
+		datetimeFormatUnitEra,
+		datetimeForamtUnitQuarter,
+		datetimeFormatUnitTimeZone1,
+		datetimeFormatUnitTimeZone2,
+	}
+)
+
+type datetimePatternComponent struct {
+	pattern       string
+	componentType int
+}
+
+// FmtDateFull formats the time with the current locales full date format
+func (c Calendar) FmtDateFull(t time.Time) (string, error) {
+	return c.Format(t, c.Formats.Date.Full)
+}
+
+// FmtDateLong formats the time with the current locales long date format
+func (c Calendar) FmtDateLong(t time.Time) (string, error) {
+	return c.Format(t, c.Formats.Date.Long)
+}
+
+// FmtDateMedium formats the time with the current locales medium date format
+func (c Calendar) FmtDateMedium(t time.Time) (string, error) {
+	return c.Format(t, c.Formats.Date.Medium)
+}
+
+// FmtDateShort formats the time with the current locales short date format
+func (c Calendar) FmtDateShort(t time.Time) (string, error) {
+	return c.Format(t, c.Formats.Date.Short)
+}
+
+// FmtTimeFull formats the time with the current locales full time format
+func (c Calendar) FmtTimeFull(t time.Time) (string, error) {
+	return c.Format(t, c.Formats.Time.Full)
+}
+
+// FmtTimeLong formats the time with the current locales long time format
+func (c Calendar) FmtTimeLong(t time.Time) (string, error) {
+	return c.Format(t, c.Formats.Time.Long)
+}
+
+// FmtTimeMedium formats the time with the current locales medium time format
+func (c Calendar) FmtTimeMedium(t time.Time) (string, error) {
+	return c.Format(t, c.Formats.Time.Medium)
+}
+
+// FmtTimeShort formats the time with the current locales short time format
+func (c Calendar) FmtTimeShort(t time.Time) (string, error) {
+	return c.Format(t, c.Formats.Time.Short)
+}
+
+// FmtDateTimeFull formats the time with the current locales full data & time format
+func (c Calendar) FmtDateTimeFull(t time.Time) (string, error) {
+	pattern := getDateTimePattern(c.Formats.DateTime.Full, c.Formats.Date.Full, c.Formats.Time.Full)
+	return c.Format(t, pattern)
+}
+
+// FmtDateTimeLong formats the time with the current locales long data & time format
+func (c Calendar) FmtDateTimeLong(t time.Time) (string, error) {
+	pattern := getDateTimePattern(c.Formats.DateTime.Long, c.Formats.Date.Long, c.Formats.Time.Long)
+	return c.Format(t, pattern)
+}
+
+// FmtDateTimeMedium formats the time with the current locales medium data & time format
+func (c Calendar) FmtDateTimeMedium(t time.Time) (string, error) {
+	pattern := getDateTimePattern(c.Formats.DateTime.Medium, c.Formats.Date.Medium, c.Formats.Time.Medium)
+	return c.Format(t, pattern)
+}
+
+// FmtDateTimeShort formats the time with the current locales short data & time format
+func (c Calendar) FmtDateTimeShort(t time.Time) (string, error) {
+	pattern := getDateTimePattern(c.Formats.DateTime.Short, c.Formats.Date.Short, c.Formats.Time.Short)
+	return c.Format(t, pattern)
+}
+
+// Format takes a time struct and a format and returns a formatted
+// string. Callers should use a DateFormat, TimeFormat, or DateTimeFormat
+// constant.
+func (c Calendar) Format(datetime time.Time, pattern string) (string, error) {
+	parsed, err := c.parseDateTimeFormat(pattern)
+	if err != nil {
+		return "", err
+	}
+
+	return c.formatDateTime(datetime, parsed)
+}
+
+// formatDateTime takes a time.Time and a sequence of parsed pattern components
+// and returns an internationalized string representation.
+func (c Calendar) formatDateTime(datetime time.Time, pattern []*datetimePatternComponent) (string, error) {
+	formatted := ""
+	for _, component := range pattern {
+		if component.componentType == datetimePatternComponentLiteral {
+			formatted += component.pattern
+		} else {
+			f, err := c.formatDateTimeComponent(datetime, component.pattern)
+			if err != nil {
+				return "", err
+			}
+			formatted += f
+		}
+	}
+
+	return strings.Trim(formatted, " ,"), nil
+}
+
+// formatDateTimeComponent renders a single component of a datetime format
+// pattern.
+func (c Calendar) formatDateTimeComponent(datetime time.Time, pattern string) (string, error) {
+	switch pattern[0:1] {
+	case string(datetimeFormatUnitEra):
+		return c.formatDateTimeComponentEra(datetime, len(pattern))
+	case string(datetimeFormatUnitYear):
+		return c.formatDateTimeComponentYear(datetime, len(pattern))
+	case string(datetimeFormatUnitMonth):
+		return c.formatDateTimeComponentMonth(datetime, len(pattern))
+	case string(datetimeFormatUnitDayOfWeek):
+		return c.formatDateTimeComponentDayOfWeek(datetime, len(pattern))
+	case string(datetimeFormatUnitDay):
+		return c.formatDateTimeComponentDay(datetime, len(pattern))
+	case string(datetimeFormatUnitHour12):
+		return c.formatDateTimeComponentHour12(datetime, len(pattern))
+	case string(datetimeFormatUnitHour24):
+		return c.formatDateTimeComponentHour24(datetime, len(pattern))
+	case string(datetimeFormatUnitMinute):
+		return c.formatDateTimeComponentMinute(datetime, len(pattern))
+	case string(datetimeFormatUnitSecond):
+		return c.formatDateTimeComponentSecond(datetime, len(pattern))
+	case string(datetimeFormatUnitPeriod):
+		return c.formatDateTimeComponentPeriod(datetime, len(pattern))
+	case string(datetimeForamtUnitQuarter):
+		return c.formatDateTimeComponentQuarter(datetime, len(pattern))
+	case string(datetimeFormatUnitTimeZone1):
+		fallthrough
+	case string(datetimeFormatUnitTimeZone2):
+		return c.formatDateTimeComponentTimeZone(datetime, len(pattern))
+	}
+
+	return "", errors.New("unknown datetime format unit: " + pattern[0:1])
+}
+
+// formatDateTimeComponentEra renders an era component.
+// TODO: not yet implemented
+func (c Calendar) formatDateTimeComponentEra(datetime time.Time, length int) (string, error) {
+	return "", nil
+}
+
+// formatDateTimeComponentYear renders a year component.
+func (c Calendar) formatDateTimeComponentYear(datetime time.Time, length int) (string, error) {
+	year := datetime.Year()
+	switch length {
+	case datetimeFormatLength1Plus:
+		return c.formatDateTimeComponentYearLengthWide(year), nil
+	case datetimeFormatLength2Plus:
+		return c.formatDateTimeComponentYearLength2Plus(year), nil
+	case datetimeFormatLengthWide:
+		return c.formatDateTimeComponentYearLengthWide(year), nil
+	}
+
+	return "", fmt.Errorf("unsupported year length: %d", length)
+}
+
+// formatDateTimeComponentYearLength2Plus renders a 2-digit year component.
+func (c Calendar) formatDateTimeComponentYearLength2Plus(year int) string {
+	yearShort := year % 100
+
+	if yearShort < 10 {
+		return fmt.Sprintf("0%d", yearShort)
+	}
+
+	return fmt.Sprintf("%d", yearShort)
+}
+
+// formatDateTimeComponentYearLength2Plus renders a full-year component - for
+// all modern dates, that's four digits.
+func (c Calendar) formatDateTimeComponentYearLengthWide(year int) string {
+	return fmt.Sprintf("%d", year)
+}
+
+// formatDateTimeComponentMonth renders a month component.
+func (c Calendar) formatDateTimeComponentMonth(datetime time.Time, length int) (string, error) {
+
+	month := datetime.Month()
+
+	switch length {
+	case datetimeFormatLength1Plus:
+		return c.formatDateTimeComponentMonth1Plus(month), nil
+	case datetimeFormatLength2Plus:
+		return c.formatDateTimeComponentMonth2Plus(month), nil
+	case datetimeFormatLengthAbbreviated:
+		return c.formatDateTimeComponentMonthAbbreviated(month), nil
+	case datetimeFormatLengthWide:
+		return c.formatDateTimeComponentMonthWide(month), nil
+	case datetimeFormatLengthNarrow:
+		return c.formatDateTimeComponentMonthNarrow(month), nil
+	}
+
+	return "", fmt.Errorf("unsupported month length: %d", length)
+}
+
+// formatDateTimeComponentMonth1Plus renders a numeric month component with 1 or
+// 2 digits depending on value.
+func (c Calendar) formatDateTimeComponentMonth1Plus(month time.Month) string {
+	return fmt.Sprintf("%d", month)
+}
+
+// formatDateTimeComponentMonth2Plus renders a numeric month component always
+// with 2 digits.
+func (c Calendar) formatDateTimeComponentMonth2Plus(month time.Month) string {
+	if month < 10 {
+		return fmt.Sprintf("0%d", month)
+	}
+	return fmt.Sprintf("%d", month)
+}
+
+// formatDateTimeComponentMonthAbbreviated renders an abbreviated text month
+// component.
+func (c Calendar) formatDateTimeComponentMonthAbbreviated(month time.Month) string {
+	return c.FormatNames.Months.Abbreviated[month]
+}
+
+// formatDateTimeComponentMonthWide renders a full text month component.
+func (c Calendar) formatDateTimeComponentMonthWide(month time.Month) string {
+	return c.FormatNames.Months.Wide[month]
+}
+
+// formatDateTimeComponentMonthNarrow renders a super-short month compontent -
+// not guaranteed to be unique for different months.
+func (c Calendar) formatDateTimeComponentMonthNarrow(month time.Month) string {
+	return c.FormatNames.Months.Narrow[month]
+}
+
+// formatDateTimeComponentDayOfWeek renders a day-of-week component.
+func (c Calendar) formatDateTimeComponentDayOfWeek(datetime time.Time, length int) (string, error) {
+	switch length {
+	case datetimeFormatLength1Plus:
+		return c.formatDateTimeComponentDayOfWeekWide(datetime.Weekday()), nil
+	case datetimeFormatLength2Plus:
+		return c.formatDateTimeComponentDayOfWeekShort(datetime.Weekday()), nil
+	case datetimeFormatLengthAbbreviated:
+		return c.formatDateTimeComponentDayOfWeekAbbreviated(datetime.Weekday()), nil
+	case datetimeFormatLengthWide:
+		return c.formatDateTimeComponentDayOfWeekWide(datetime.Weekday()), nil
+	case datetimeFormatLengthNarrow:
+		return c.formatDateTimeComponentDayOfWeekNarrow(datetime.Weekday()), nil
+	}
+
+	return "", fmt.Errorf("unsupported year day-of-week: %d", length)
+}
+
+// formatDateTimeComponentDayOfWeekAbbreviated renders an abbreviated text
+// day-of-week component.
+func (c Calendar) formatDateTimeComponentDayOfWeekAbbreviated(weekday time.Weekday) string {
+	return c.FormatNames.Days.Abbreviated[weekday]
+}
+
+// formatDateTimeComponentDayOfWeekAbbreviated renders a
+// shorter-then-abbreviated but still unique text day-of-week component.
+func (c Calendar) formatDateTimeComponentDayOfWeekShort(weekday time.Weekday) string {
+	return c.FormatNames.Days.Short[weekday]
+}
+
+// formatDateTimeComponentDayOfWeekWide renders a full text day-of-week
+// component.
+func (c Calendar) formatDateTimeComponentDayOfWeekWide(weekday time.Weekday) string {
+	return c.FormatNames.Days.Wide[weekday]
+}
+
+// formatDateTimeComponentDayOfWeekNarrow renders a super-short day-of-week
+// compontent - not guaranteed to be unique for different days.
+func (c Calendar) formatDateTimeComponentDayOfWeekNarrow(weekday time.Weekday) string {
+	return c.FormatNames.Days.Narrow[weekday]
+}
+
+// formatDateTimeComponentDay renders a day-of-year component.
+func (c Calendar) formatDateTimeComponentDay(datetime time.Time, length int) (string, error) {
+	day := datetime.Day()
+
+	switch length {
+	case datetimeFormatLength1Plus:
+		return fmt.Sprintf("%d", day), nil
+	case datetimeFormatLength2Plus:
+		if day < 10 {
+			return fmt.Sprintf("0%d", day), nil
+		}
+		return fmt.Sprintf("%d", day), nil
+	}
+
+	return "", fmt.Errorf("unsupported day-of-year: %d", length)
+}
+
+// formatDateTimeComponentHour12 renders an hour-component using a 12-hour
+// clock.
+func (c Calendar) formatDateTimeComponentHour12(datetime time.Time, length int) (string, error) {
+	hour := datetime.Hour()
+	if hour > 12 {
+		hour = hour - 12
+	}
+
+	switch length {
+	case datetimeFormatLength1Plus:
+		return fmt.Sprintf("%d", hour), nil
+	case datetimeFormatLength2Plus:
+		if hour < 10 {
+			return fmt.Sprintf("0%d", hour), nil
+		}
+		return fmt.Sprintf("%d", hour), nil
+	}
+
+	return "", fmt.Errorf("unsupported hour-12: %d", length)
+}
+
+// formatDateTimeComponentHour24 renders an hour-component using a 24-hour
+// clock.
+func (c Calendar) formatDateTimeComponentHour24(datetime time.Time, length int) (string, error) {
+	hour := datetime.Hour()
+
+	switch length {
+	case datetimeFormatLength1Plus:
+		return fmt.Sprintf("%d", hour), nil
+	case datetimeFormatLength2Plus:
+		if hour < 10 {
+			return fmt.Sprintf("0%d", hour), nil
+		}
+		return fmt.Sprintf("%d", hour), nil
+	}
+
+	return "", fmt.Errorf("unsupported hour-24: %d", length)
+}
+
+// formatDateTimeComponentMinute renders a minute component.
+func (c Calendar) formatDateTimeComponentMinute(datetime time.Time, length int) (string, error) {
+	minute := datetime.Minute()
+
+	switch length {
+	case datetimeFormatLength1Plus:
+		return fmt.Sprintf("%d", minute), nil
+	case datetimeFormatLength2Plus:
+		if minute < 10 {
+			return fmt.Sprintf("0%d", minute), nil
+		}
+		return fmt.Sprintf("%d", minute), nil
+	}
+
+	return "", fmt.Errorf("unsupported minute: %d", length)
+}
+
+// formatDateTimeComponentSecond renders a second component
+func (c Calendar) formatDateTimeComponentSecond(datetime time.Time, length int) (string, error) {
+	second := datetime.Second()
+
+	switch length {
+	case datetimeFormatLength1Plus:
+		return fmt.Sprintf("%d", second), nil
+	case datetimeFormatLength2Plus:
+		if second < 10 {
+			return fmt.Sprintf("0%d", second), nil
+		}
+		return fmt.Sprintf("%d", second), nil
+	}
+
+	return "", fmt.Errorf("unsupported second: %d", length)
+}
+
+// formatDateTimeComponentPeriod renders a period component (AM/PM).
+func (c Calendar) formatDateTimeComponentPeriod(datetime time.Time, length int) (string, error) {
+	hour := datetime.Hour()
+
+	switch length {
+	case datetimeFormatLength1Plus:
+		return c.formatDateTimeComponentPeriodWide(hour), nil
+	case datetimeFormatLengthAbbreviated:
+		return c.formatDateTimeComponentPeriodAbbreviated(hour), nil
+	case datetimeFormatLengthWide:
+		return c.formatDateTimeComponentPeriodWide(hour), nil
+	case datetimeFormatLengthNarrow:
+		return c.formatDateTimeComponentPeriodNarrow(hour), nil
+	}
+
+	return "", fmt.Errorf("unsupported day-period: %d", length)
+}
+
+// formatDateTimeComponentPeriodAbbreviated renders an abbreviated period
+// component.
+func (c Calendar) formatDateTimeComponentPeriodAbbreviated(hour int) string {
+	if hour < 12 {
+		return c.FormatNames.Periods.Abbreviated[am]
+	}
+
+	return c.FormatNames.Periods.Abbreviated[pm]
+}
+
+// formatDateTimeComponentPeriodWide renders a full period component.
+func (c Calendar) formatDateTimeComponentPeriodWide(hour int) string {
+	if hour < 12 {
+		return c.FormatNames.Periods.Wide[am]
+	}
+
+	return c.FormatNames.Periods.Wide[pm]
+}
+
+// formatDateTimeComponentPeriodNarrow renders a super-short period component.
+func (c Calendar) formatDateTimeComponentPeriodNarrow(hour int) string {
+	if hour < 12 {
+		return c.FormatNames.Periods.Narrow[am]
+	}
+
+	return c.FormatNames.Periods.Narrow[pm]
+}
+
+// formatDateTimeComponentQuarter renders a calendar quarter component - this
+// is calendar quarters and not fiscal quarters.
+//  - Q1: Jan-Mar
+//  - Q2: Apr-Jun
+//  - Q3: Jul-Sep
+//  - Q4: Oct-Dec
+// TODO: not yet implemented
+func (c Calendar) formatDateTimeComponentQuarter(datetime time.Time, length int) (string, error) {
+	return "", nil
+}
+
+// formatDateTimeComponentTimeZone renders a time zone component.
+// TODO: this has not yet been implemented
+func (c Calendar) formatDateTimeComponentTimeZone(datetime time.Time, length int) (string, error) {
+	return "", nil
+}
+
+// parseDateTimeFormat takes a format pattern string and returns a sequence of
+// components.
+func (c Calendar) parseDateTimeFormat(pattern string) ([]*datetimePatternComponent, error) {
+	// every thing between single quotes should become a literal
+	// all non a-z, A-Z characters become a literal
+	// everything else, repeat character sequences become a component
+	format := []*datetimePatternComponent{}
+	for i := 0; i < len(pattern); {
+		char := pattern[i : i+1]
+
+		skip := false
+		// for units we don't support yet, just skip over them
+		for _, r := range datetimeFormatUnitCutset {
+			if char == string(r) {
+				skip = true
+				break
+			}
+		}
+
+		if skip {
+			i++
+			continue
+		}
+
+		if char == string(datetimeFormatLiteral) {
+			// find the next single quote
+			// create a literal out of everything between the quotes
+			// and set i to the position after the second quote
+
+			if i == len(pattern)-1 {
+				return []*datetimePatternComponent{}, errors.New("malformed datetime format")
+			}
+
+			nextQuote := strings.Index(pattern[i+1:], string(datetimeFormatLiteral))
+			if nextQuote == -1 {
+				return []*datetimePatternComponent{}, errors.New("malformed datetime format")
+			}
+
+			component := &datetimePatternComponent{
+				pattern:       pattern[i+1 : nextQuote+i+1],
+				componentType: datetimePatternComponentLiteral,
+			}
+
+			format = append(format, component)
+			i = nextQuote + i + 2
+			continue
+
+		}
+		if (char >= "a" && char <= "z") || (char >= "A" && char <= "Z") {
+			// this represents a format unit
+			// find the entire sequence of the same character
+			endChar := lastSequenceIndex(pattern[i:]) + i
+
+			component := &datetimePatternComponent{
+				pattern:       pattern[i : endChar+1],
+				componentType: datetimePatternComponentUnit,
+			}
+
+			format = append(format, component)
+			i = endChar + 1
+			continue
+
+		}
+		if char == string(datetimeFormatTimeSeparator) {
+			component := &datetimePatternComponent{
+				// pattern:       c.TimeSeparator,
+				pattern:       string(datetimeFormatTimeSeparator),
+				componentType: datetimePatternComponentLiteral,
+			}
+			format = append(format, component)
+			i++
+			continue
+
+		}
+
+		component := &datetimePatternComponent{
+			pattern:       char,
+			componentType: datetimePatternComponentLiteral,
+		}
+
+		format = append(format, component)
+		i++
+		continue
+
+	}
+
+	return format, nil
+}
+
+// getDateTimePattern combines a date pattern and a time pattern into a datetime
+// pattern. The datetimePattern argument includes a {0} placeholder for the time
+// pattern, and a {1} placeholder for the date component.
+func getDateTimePattern(datetimePattern, datePattern, timePattern string) string {
+	return strings.Replace(strings.Replace(datetimePattern, "{1}", datePattern, 1), "{0}", timePattern, 1)
+}
+
+// lastSequenceIndex looks at the first character in a string and returns the
+// last digits of the first sequence of that character. For example:
+//  - ABC: 0
+//  - AAB: 1
+//  - ABA: 0
+//  - AAA: 2
+func lastSequenceIndex(str string) int {
+	if len(str) == 0 {
+		return -1
+	}
+
+	if len(str) == 1 {
+		return 0
+	}
+
+	sequenceChar := str[0:1]
+	lastPos := 0
+	for i := 1; i < len(str); i++ {
+		if str[i:i+1] != sequenceChar {
+			break
+		}
+
+		lastPos = i
+	}
+
+	return lastPos
+}

+ 373 - 0
number.go

@@ -0,0 +1,373 @@
+package ut
+
+import (
+	"errors"
+	"fmt"
+	"log"
+	"math"
+	"regexp"
+	"strconv"
+	"strings"
+)
+
+// numberFormat is a struct that contains all the information about number
+// formatting for a specific locale that we need to do number, currency, and
+// percentage formatting
+type numberFormat struct {
+	positivePrefix   string
+	positiveSuffix   string
+	negativePrefix   string
+	negativeSuffix   string
+	multiplier       int
+	minDecimalDigits int
+	maxDecimalDigits int
+	minIntegerDigits int
+	groupSizeFinal   int // only the right-most (least significant) group
+	groupSizeMain    int // all other groups
+}
+
+var (
+	// numberFormats keeps a copy of all numberFormat instances that have been
+	// loaded before, to prevent parsing a single number format string multiple
+	// times. There is vey little danger of this list consuming too much memory,
+	// since the data for each of these is pretty small in size, and the same
+	// formats are used by multiple locales.
+	numberFormats           = map[string]*numberFormat{}
+	numberFormatsNoDecimals = map[string]*numberFormat{}
+
+	// prefixSuffixRegex is a regular expression that is used to parse number
+	// formats
+	prefixSuffixRegex = regexp.MustCompile(`(.*?)[#,\.0]+(.*)`)
+)
+
+// FmtCurrency takes a float number and a currency key and returns a string
+// with a properly formatted currency amount with the correct currency symbol.
+// If a symbol cannot be found for the reqested currency, this will panic, use
+// FmtCurrencySafe for non panicing variant.
+func (n Number) FmtCurrency(currency string, number float64) string {
+
+	format := n.parseFormat(n.Formats.Currency, true)
+	result := n.formatNumber(format, number)
+
+	return strings.Replace(result, "¤", n.Currencies[currency].Symbol, -1)
+}
+
+// FmtCurrencySafe takes a float number and a currency key and returns a string
+// with a properly formatted currency amount with the correct currency symbol.
+// If a symbol cannot be found for the reqested currency, the the key is used
+// instead. If the currency key requested is not recognized, it is used as the
+// symbol, and an error is returned with the formatted string.
+func (n Number) FmtCurrencySafe(currency string, number float64) (formatted string, err error) {
+
+	format := n.parseFormat(n.Formats.Currency, true)
+	result := n.formatNumber(format, number)
+
+	c, ok := n.Currencies[currency]
+	if !ok {
+		s := "**** WARNING **** unknown currency: " + currency
+		err = errors.New(s)
+		log.Println(s)
+		return
+	}
+
+	formatted = strings.Replace(result, "¤", c.Symbol, -1)
+	return
+}
+
+// FmtCurrencyWhole does exactly what FormatCurrency does, but it leaves off
+// any decimal places. AKA, it would return $100 rather than $100.00.
+// If a symbol cannot be found for the reqested currency, this will panic, use
+// FmtCurrencyWholeSafe for non panicing variant.
+func (n Number) FmtCurrencyWhole(currency string, number float64) string {
+	format := n.parseFormat(n.Formats.Currency, false)
+	result := n.formatNumber(format, number)
+
+	return strings.Replace(result, "¤", n.Currencies[currency].Symbol, -1)
+}
+
+// FmtCurrencyWholeSafe does exactly what FormatCurrency does, but it leaves off
+// any decimal places. AKA, it would return $100 rather than $100.00.
+func (n Number) FmtCurrencyWholeSafe(currency string, number float64) (formatted string, err error) {
+	format := n.parseFormat(n.Formats.Currency, false)
+	result := n.formatNumber(format, number)
+
+	c, ok := n.Currencies[currency]
+	if !ok {
+		s := "**** WARNING **** unknown currency: " + currency
+		err = errors.New(s)
+		log.Println(s)
+		return
+	}
+
+	formatted = strings.Replace(result, "¤", c.Symbol, -1)
+	return
+}
+
+// FmtNumber takes a float number and returns a properly formatted string
+// representation of that number according to the locale's number format.
+func (n Number) FmtNumber(number float64) string {
+	return n.formatNumber(n.parseFormat(n.Formats.Decimal, true), number)
+}
+
+// FmtNumberWhole does exactly what FormatNumber does, but it leaves off any
+// decimal places. AKA, it would return 100 rather than 100.01.
+func (n Number) FmtNumberWhole(number float64) string {
+	return n.formatNumber(n.parseFormat(n.Formats.Decimal, false), number)
+}
+
+// FmtPercent takes a float number and returns a properly formatted string
+// representation of that number as a percentage according to the locale's
+// percentage format.
+func (n Number) FmtPercent(number float64) string {
+	return n.formatNumber(n.parseFormat(n.Formats.Percent, true), number)
+}
+
+// parseFormat takes a format string and returns a numberFormat instance
+func (n Number) parseFormat(pattern string, includeDecimalDigits bool) *numberFormat {
+	// processed := false
+	// if includeDecimalDigits {
+	// 	_, processed = numberFormats[pattern]
+	// } else {
+	// 	_, processed = numberFormatsNoDecimals[pattern]
+	// }
+
+	// if !processed {
+	format := new(numberFormat)
+	patterns := strings.Split(pattern, ";")
+
+	matches := prefixSuffixRegex.FindAllStringSubmatch(patterns[0], -1)
+	if len(matches) > 0 {
+		if len(matches[0]) > 1 {
+			format.positivePrefix = matches[0][1]
+		}
+		if len(matches[0]) > 2 {
+			format.positiveSuffix = matches[0][2]
+		}
+	}
+
+	// default values for negative prefix & suffix
+	format.negativePrefix = string(n.Symbols.Negative) + string(format.positivePrefix)
+	format.negativeSuffix = format.positiveSuffix
+
+	// see if they are in the pattern
+	if len(patterns) > 1 {
+		matches = prefixSuffixRegex.FindAllStringSubmatch(patterns[1], -1)
+
+		if len(matches) > 0 {
+			if len(matches[0]) > 1 {
+				format.negativePrefix = matches[0][1]
+			}
+			if len(matches[0]) > 2 {
+				format.negativeSuffix = matches[0][2]
+			}
+		}
+	}
+
+	pat := patterns[0]
+
+	if strings.Index(pat, "%") != -1 {
+		format.multiplier = 100
+	} else if strings.Index(pat, "‰") != -1 {
+		format.multiplier = 1000
+	} else {
+		format.multiplier = 1
+	}
+
+	pos := strings.Index(pat, ".")
+
+	if pos != -1 {
+		pos2 := strings.LastIndex(pat, "0")
+		if pos2 > pos {
+			format.minDecimalDigits = pos2 - pos
+		}
+
+		pos3 := strings.LastIndex(pat, "#")
+		if pos3 >= pos2 {
+			format.maxDecimalDigits = pos3 - pos
+		} else {
+			format.maxDecimalDigits = format.minDecimalDigits
+		}
+
+		pat = pat[0:pos]
+	}
+
+	p := strings.Replace(pat, ",", "", -1)
+	pos = strings.Index(p, "0")
+	if pos != -1 {
+		format.minIntegerDigits = strings.LastIndex(p, "0") - pos + 1
+	}
+
+	p = strings.Replace(pat, "#", "0", -1)
+	pos = strings.LastIndex(pat, ",")
+	if pos != -1 {
+		format.groupSizeFinal = strings.LastIndex(p, "0") - pos
+		pos2 := strings.LastIndex(p[0:pos], ",")
+		if pos2 != -1 {
+			format.groupSizeMain = pos - pos2 - 1
+		} else {
+			format.groupSizeMain = format.groupSizeFinal
+		}
+	}
+
+	if includeDecimalDigits {
+		numberFormats[pattern] = format
+	} else {
+		format.maxDecimalDigits = 0
+		format.minDecimalDigits = 0
+		numberFormatsNoDecimals[pattern] = format
+	}
+
+	// }
+
+	if includeDecimalDigits {
+		return numberFormats[pattern]
+	}
+
+	return numberFormatsNoDecimals[pattern]
+}
+
+// formatNumber takes an arbitrary numberFormat and a number and applies that
+// format to that number, returning the resulting string
+func (n Number) formatNumber(format *numberFormat, number float64) string {
+	negative := number < 0
+
+	// apply the multiplier first - this is mainly used for percents
+	value := math.Abs(number * float64(format.multiplier))
+	stringValue := ""
+
+	// get the initial string value, with the maximum # decimal digits
+	if format.maxDecimalDigits >= 0 {
+		stringValue = numberRound(value, format.maxDecimalDigits)
+	} else {
+		stringValue = fmt.Sprintf("%f", value)
+	}
+
+	// separate the integer from the decimal parts
+	pos := strings.Index(stringValue, ".")
+	integer := stringValue
+	decimal := ""
+	if pos != -1 {
+		integer = stringValue[:pos]
+		decimal = stringValue[pos+1:]
+	}
+
+	// make sure the minimum # decimal digits are there
+	for len(decimal) < format.minDecimalDigits {
+		decimal = decimal + "0"
+	}
+
+	// make sure the minimum # integer digits are there
+	for len(integer) < format.minIntegerDigits {
+		integer = "0" + integer
+	}
+
+	// if there's a decimal portion, prepend the decimal point symbol
+	if len(decimal) > 0 {
+		decimal = string(n.Symbols.Decimal) + decimal
+	}
+
+	// put the integer portion into properly sized groups
+	if format.groupSizeFinal > 0 && len(integer) > format.groupSizeFinal {
+		if len(integer) > format.groupSizeMain {
+			groupFinal := integer[len(integer)-format.groupSizeFinal:]
+			groupFirst := integer[:len(integer)-format.groupSizeFinal]
+			integer = strings.Join(
+				chunkString(groupFirst, format.groupSizeMain),
+				n.Symbols.Group,
+			) + n.Symbols.Group + groupFinal
+		}
+	}
+
+	// append/prepend negative/positive prefix/suffix
+	formatted := ""
+	if negative {
+		formatted = format.negativePrefix + integer + decimal + format.negativeSuffix
+	} else {
+		formatted = format.positivePrefix + integer + decimal + format.positiveSuffix
+	}
+
+	// replace percents and permilles with the local symbols (likely to be exactly the same)
+	formatted = strings.Replace(formatted, "%", string(n.Symbols.Percent), -1)
+	formatted = strings.Replace(formatted, "‰", string(n.Symbols.PerMille), -1)
+
+	return formatted
+}
+
+// chunkString takes a string and chunks it into size-sized pieces in a slice.
+// If the length of the string is not divisible by the size, then the first
+// chunk in the slice will be padded to compensate.
+func chunkString(str string, size int) []string {
+	if str == "" {
+		return []string{}
+	}
+
+	if size == 0 {
+		return []string{str}
+	}
+
+	chunks := make([]string, int64(math.Ceil(float64(len(str))/float64(size))))
+
+	for len(str) < len(chunks)*size {
+		str = " " + str
+	}
+
+	for i := 0; i < len(chunks); i++ {
+		start := i * size
+		stop := int64(math.Min(float64(start+size), float64(len(str))))
+		chunks[i] = str[start:stop]
+	}
+
+	chunks[0] = strings.TrimLeft(chunks[0], " ")
+
+	return chunks
+}
+
+// numberRound takes a number and returns a string containing a rounded to the
+// even with the number of decimal places requested.  If this would result in
+// the right most decimal place(s) containing "0"s, then all "0"s on the end of
+// the decimal portion will be truncated.
+func numberRound(number float64, decimals int) string {
+	if number == float64(int64(number)) {
+		return strconv.FormatInt(int64(number), 10)
+	}
+
+	str := fmt.Sprintf("%f", number)
+	pos := strings.Index(str, ".")
+
+	if pos != -1 && len(str) > (pos+decimals) {
+		str = str[0 : pos+decimals+1]
+	}
+
+	backToNum, _ := strconv.ParseFloat(str, 64)
+	difference := number - backToNum
+	half := 0.5
+	for i := 0; i < decimals; i++ {
+		half = half / 10
+	}
+
+	roundUp := false
+	if difference > half {
+		roundUp = true
+	} else if difference == half {
+		// for halfs, round to even
+		lastDigit := str[:len(str)-1]
+		roundUp = lastDigit == "1" || lastDigit == "3" || lastDigit == "5" || lastDigit == "7" || lastDigit == "9"
+	}
+
+	if roundUp {
+		// multiply, then ceil, then divide
+		multiplier := math.Pow(float64(10), float64(decimals))
+		multiplied := strconv.FormatFloat(math.Ceil(number*multiplier), 'f', 0, 64)
+
+		if len(multiplied) > decimals {
+			str = multiplied[:len(multiplied)-decimals] + "." + multiplied[len(multiplied)-decimals:]
+		} else {
+			str = "0." + strings.Repeat("0", decimals-len(multiplied)) + multiplied
+		}
+	}
+
+	str = strings.TrimRight(str, "0")
+	str = strings.TrimRight(str, ".")
+
+	return str
+}

+ 0 - 11
plurals.go

@@ -5,8 +5,6 @@ import "math"
 // PluralRule denotes the type of plural rules
 type PluralRule int
 
-// PluralRule types
-
 // TODO: change this to integer for efficiency and use stringify lib to generate string values.
 const (
 	PluralRuleZero  PluralRule = iota // zero
@@ -17,15 +15,6 @@ const (
 	PluralRuleOther                   // other - required—general plural form—also used if the language only has a single form
 )
 
-// const (
-// 	PluralRuleZero  PluralRule = "zero"  // zero
-// 	PluralRuleOne              = "one"   // singular
-// 	PluralRuleTwo              = "two"   // dual
-// 	PluralRuleFew              = "few"   // paucal
-// 	PluralRuleMany             = "many"  // also used for fractions if they have a separate class
-// 	PluralRuleOther            = "other" // required—general plural form—also used if the language only has a single form
-// )
-
 // NumberValue should be one of these types:
 // int, float
 type NumberValue interface{}

+ 143 - 27
translator.go

@@ -1,39 +1,41 @@
 package ut
 
 import (
+	"errors"
 	"fmt"
 	"log"
+	"time"
 )
 
 type translation struct {
 	text string
 }
 
-// map[key]map[plural type othe, many, few, single]*translation
+// map[key]map[plural type other, many, few, single]*translation
 type translations map[PluralRule]map[string]*translation
 type groups map[string][]*translation
 
 // Translator holds the locale translation instance
 type Translator struct {
-	Locale       *Locale
+	locale       *Locale
 	ruler        PluralRuler
 	translations translations
 	groups       groups
 }
 
 func newTranslator(locale string) (*Translator, error) {
-	return nil, nil
-	// loc, err := GetLocale(locale)
-	// if err != nil {
-	// 	return nil, err
-	// }
 
-	// return &Translator{
-	// 	Locale:       loc,
-	// 	ruler:        pluralRules[loc.PluralRule],
-	// 	translations: make(translations),
-	// 	groups:       make(groups),
-	// }, nil
+	loc, err := GetLocale(locale)
+	if err != nil {
+		return nil, err
+	}
+
+	return &Translator{
+		locale:       loc,
+		ruler:        pluralRules[loc.PluralRule],
+		translations: make(translations),
+		groups:       make(groups),
+	}, nil
 }
 
 // Add registers a new translation to the Translator using the
@@ -71,31 +73,145 @@ func (t *Translator) T(key string, a ...interface{}) string {
 	return t.P(key, 0, a...)
 }
 
+// TSafe translates the text associated with the given key with the
+// arguments passed in, if the key or rule cannot be found it returns an error
+func (t *Translator) TSafe(key string, a ...interface{}) (string, error) {
+	return t.PSafe(key, 0, a...)
+}
+
 // P translates the plural text associated with the given key with the
 // arguments passed in
 func (t *Translator) P(key string, count interface{}, a ...interface{}) string {
 
+	trans, err := t.PSafe(key, count, a...)
+	if err != nil {
+		log.Println(err.Error())
+		return err.Error()
+	}
+
+	return trans
+}
+
+// PSafe translates the plural text associated with the given key with the
+// arguments passed in, if the key or rule cannot be found it returns an error
+func (t *Translator) PSafe(key string, count interface{}, a ...interface{}) (string, error) {
+
 	rule := t.ruler.FindRule(count)
 
 	trans, ok := t.translations[rule][key]
 	if !ok {
-		s := "***** WARNING:***** Translation Key " + key + " Not Found"
-		log.Println(s)
-		return s
+		return "", errors.New("***** WARNING:***** Translation Key " + key + " Not Found")
 	}
 
-	return fmt.Sprintf(trans.text, a...)
+	return fmt.Sprintf(trans.text, a...), nil
+}
+
+// FmtDateFull formats the time with the current locales full date format
+func (t *Translator) FmtDateFull(dt time.Time) (string, error) {
+	return t.locale.Calendar.FmtDateFull(dt)
+}
+
+// FmtDateLong formats the time with the current locales long date format
+func (t *Translator) FmtDateLong(dt time.Time) (string, error) {
+	return t.locale.Calendar.FmtDateLong(dt)
+}
+
+// FmtDateMedium formats the time with the current locales medium date format
+func (t *Translator) FmtDateMedium(dt time.Time) (string, error) {
+	return t.locale.Calendar.FmtDateMedium(dt)
+}
+
+// FmtDateShort formats the time with the current locales short date format
+func (t *Translator) FmtDateShort(dt time.Time) (string, error) {
+	return t.locale.Calendar.FmtDateShort(dt)
+}
+
+// FmtDateTimeFull formats the time with the current locales full data & time format
+func (t *Translator) FmtDateTimeFull(dt time.Time) (string, error) {
+	return t.locale.Calendar.FmtDateTimeFull(dt)
+}
+
+// FmtDateTimeLong formats the time with the current locales long data & time format
+func (t *Translator) FmtDateTimeLong(dt time.Time) (string, error) {
+	return t.locale.Calendar.FmtDateTimeLong(dt)
+}
+
+// FmtDateTimeMedium formats the time with the current locales medium data & time format
+func (t *Translator) FmtDateTimeMedium(dt time.Time) (string, error) {
+	return t.locale.Calendar.FmtDateTimeMedium(dt)
+}
+
+// FmtDateTimeShort formats the time with the current locales short data & time format
+func (t *Translator) FmtDateTimeShort(dt time.Time) (string, error) {
+	return t.locale.Calendar.FmtDateTimeShort(dt)
+}
+
+// FmtTimeFull formats the time with the current locales full time format
+func (t *Translator) FmtTimeFull(dt time.Time) (string, error) {
+	return t.locale.Calendar.FmtTimeFull(dt)
+}
+
+// FmtTimeLong formats the time with the current locales long time format
+func (t *Translator) FmtTimeLong(dt time.Time) (string, error) {
+	return t.locale.Calendar.FmtTimeLong(dt)
+}
+
+// FmtTimeMedium formats the time with the current locales medium time format
+func (t *Translator) FmtTimeMedium(dt time.Time) (string, error) {
+	return t.locale.Calendar.FmtTimeMedium(dt)
+}
+
+// FmtTimeShort formats the time with the current locales short time format
+func (t *Translator) FmtTimeShort(dt time.Time) (string, error) {
+	return t.locale.Calendar.FmtTimeShort(dt)
 }
 
-// // TSafe translates the text associated with the given key with the
-// // arguments passed in just like T() but doesn't panic, but instead
-// // returns an error
-// func (t *Translator) TSafe(key string, a ...interface{}) (string, error) {
+// FmtCurrencySafe takes a float number and a currency key and returns a string
+// with a properly formatted currency amount with the correct currency symbol.
+// If a symbol cannot be found for the reqested currency, the the key is used
+// instead. If the currency key requested is not recognized, it is used as the
+// symbol, and an error is returned with the formatted string.
+func (t *Translator) FmtCurrencySafe(currency string, number interface{}) (string, error) {
+	return t.locale.Number.FmtCurrencySafe(currency, toFloat64(number))
+}
+
+// FmtCurrencyWholeSafe does exactly what FormatCurrency does, but it leaves off
+// any decimal places. AKA, it would return $100 rather than $100.00.
+func (t *Translator) FmtCurrencyWholeSafe(currency string, number interface{}) (string, error) {
+	return t.locale.Number.FmtCurrencyWholeSafe(currency, toFloat64(number))
+}
 
-// 	trans, ok := t.translations[key]
-// 	if !ok {
-// 		return "", errors.New("*** Translation Key " + key + " Not Found")
-// 	}
+// FmtCurrency takes a float number and a currency key and returns a string
+// with a properly formatted currency amount with the correct currency symbol.
+// If a symbol cannot be found for the reqested currency, this will panic, use
+// FmtCurrencySafe for non panicing variant.
+func (t *Translator) FmtCurrency(currency string, number interface{}) string {
+	return t.locale.Number.FmtCurrency(currency, toFloat64(number))
+}
+
+// FmtCurrencyWhole does exactly what FormatCurrency does, but it leaves off
+// any decimal places. AKA, it would return $100 rather than $100.00.
+// If a symbol cannot be found for the reqested currency, this will panic, use
+// FmtCurrencyWholeSafe for non panicing variant.
+func (t *Translator) FmtCurrencyWhole(currency string, number interface{}) string {
+	return t.locale.Number.FmtCurrencyWhole(currency, toFloat64(number))
+}
 
-// 	return fmt.Sprintf(trans.singular, a...), nil
-// }
+// FmtNumber takes a float number and returns a properly formatted string
+// representation of that number according to the locale's number format.
+func (t *Translator) FmtNumber(number interface{}) string {
+	return t.locale.Number.FmtNumber(toFloat64(number))
+}
+
+// FmtNumberWhole does exactly what FormatNumber does, but it leaves off any
+// decimal places. AKA, it would return 100 rather than 100.01.
+func (t *Translator) FmtNumberWhole(number interface{}) string {
+	return t.locale.Number.FmtNumberWhole(toFloat64(number))
+}
+
+// FmtPercent takes a float number and returns a properly formatted string
+// representation of that number as a percentage according to the locale's
+// percentage format.
+func (t *Translator) FmtPercent(number interface{}) string {
+	return t.locale.Number.FmtPercent(toFloat64(number))
+}