Boostport/address

View on GitHub
format.go

Summary

Maintainability
A
0 mins
Test Coverage
A
96%
package address

import (
    "bytes"
    "fmt"
    "html/template"
    "regexp"
    "strings"

    textLanguage "golang.org/x/text/language"
    "golang.org/x/text/language/display"
)

var isAlphabetRegex = regexp.MustCompile(`[A-Za-z]`)

var collapseWhitespaceRegex = regexp.MustCompile(`[\s\p{Zs}]{2,}`)

var collapseBRRegex = regexp.MustCompile(`(?:\s|\p{Zs}|<br>){2,}`)

var funcMap = template.FuncMap{
    "toUpper": strings.ToUpper,
    "len": func(slice []string) int {
        return len(slice)
    },
    "inc": func(i int) int {
        return i + 1
    },
    "join": strings.Join,
}

// DefaultFormatter formats an address using the country's address format and includes the name of the country.
// If Latinize is set to true, in countries where a latinized address format is provided, the latinized format is used.
type DefaultFormatter struct {
    Output   Outputter
    Latinize bool
}

// Format formats an address. The language must be a valid ISO 639-1 language code. It is used to convert the keys
// in administrative areas, localities and dependent localities into their actual names. If the provided language
// does not have any translations, it falls back to the default language used by the country.
func (d DefaultFormatter) Format(address Address, language string) string {

    language = generated.normalizeLanguage(address.Country, language)

    format, isLatinized := getFormat(address.Country, d.Latinize)

    if isLatinized {
        format += "%n%country"
    } else {
        format = "%country%n" + format
    }

    t := d.Output.TransformFormat(format, map[Field]struct{}{})

    compiled := template.Must(template.New("").Funcs(funcMap).Parse(t))

    buf := bytes.NewBuffer([]byte{})

    compiled.Execute(buf, address.toFormatData(generated.getCountry(address.Country), language))

    return collapseBRRegex.ReplaceAllString(collapseWhitespaceRegex.ReplaceAllString(strings.TrimSpace(buf.String()), "\n"), "<br>")
}

// PostalLabelFormatter formats an address for postal labels. It uppercases address fields as required by the country's
// addressing standards. If the address it in the same country as the origin country, the country is omitted.
// The country name is added to the address, both in the language of the origin country as well as English, following
// recommendations of the Universal Postal Union, to avoid difficulties in transit.
// The OriginCountryCode field should be set to the ISO 3166-1 country code of the originating country.
// If Latinize is set to true, in countries where a latinized address format is provided, the latinized format is used.
type PostalLabelFormatter struct {
    Output            Outputter
    OriginCountryCode string
    Latinize          bool
}

// Format formats an address. The language must be a valid ISO 639-1 language code. It is used to convert the keys
// in administrative areas, localities and dependent localities into their actual names. If the provided language
// does not have any translations, it falls back to the default language used by the country.
func (f PostalLabelFormatter) Format(address Address, language string) string {

    language = generated.normalizeLanguage(address.Country, language)

    format, isLatinized := getFormat(address.Country, f.Latinize)

    countryData := generated.getCountry(address.Country)

    addressData := address.toFormatData(countryData, language)

    // Include the country since this is an international mail
    if generated.hasCountry(f.OriginCountryCode) && strings.ToUpper(f.OriginCountryCode) != strings.ToUpper(address.Country) {

        originLanguage, _ := textLanguage.Make(fmt.Sprintf("und-%s", f.OriginCountryCode)).Base()

        destinationCountry := textLanguage.MustParseRegion(address.Country)

        oNamer := display.Regions(textLanguage.MustParse(originLanguage.String()))
        namer := display.Regions(textLanguage.English)

        englishDestination := namer.Name(destinationCountry)

        var translatedDestination string

        if oNamer != nil {
            translatedDestination = oNamer.Name(destinationCountry)
        } else {
            translatedDestination = englishDestination
        }

        if translatedDestination != englishDestination {
            addressData.Country = fmt.Sprintf("%s - %s", strings.ToUpper(translatedDestination), strings.ToUpper(englishDestination))
        } else {
            addressData.Country = strings.ToUpper(englishDestination)
        }

        if addressData.Country != "" {
            if isLatinized {
                format += "%n%country"
            } else {
                format = "%country%n" + format
            }
        }
    }

    if isAlphabetRegex.MatchString(addressData.AdministrativeAreaPostalKey) {
        addressData.AdministrativeArea = addressData.AdministrativeAreaPostalKey
    }

    t := f.Output.TransformFormat(format, countryData.Upper)

    compiled := template.Must(template.New("").Funcs(funcMap).Parse(t))

    buf := bytes.NewBuffer([]byte{})

    compiled.Execute(buf, addressData)

    return collapseBRRegex.ReplaceAllString(collapseWhitespaceRegex.ReplaceAllString(strings.TrimSpace(buf.String()), "\n"), "<br>")
}

// Outputter defines an interface to transform an address format in Google's format into a Go template that is merged
// with the address data to produce a formatted address.
type Outputter interface {
    TransformFormat(format string, upper map[Field]struct{}) string
}

// HTMLOutputter outputs the formatted address as an HTML fragments. The address fields are annotated using the class
// attribute and `<br>`s are used for new lines.
type HTMLOutputter struct{}

// TransformFormat transforms an address format in Google's format into a HTML template. The upper map is used to
// determine which fields should be converted to UPPERCASE.
func (h HTMLOutputter) TransformFormat(format string, upper map[Field]struct{}) string {

    r := strings.NewReplacer(
        "%country", fmt.Sprintf(`{{if ne .%s "" }}<span class="country">{{.%s}}</span>{{end}}`, Country, toUpper(Country, "", upper)),
        "%N", fmt.Sprintf(`{{if ne .%s "" }}<span class="name">{{.%s}}</span>{{end}}`, Name, toUpper(Name, "", upper)),
        "%O", fmt.Sprintf(`{{if ne .%s "" }}<span class="organization">{{.%s}}</span>{{end}}`, Organization, toUpper(Organization, "", upper)),
        "%A", fmt.Sprintf(`{{$numLines:=.%s|len}}{{range $lineNo, $line := .%s}}{{$realLineNo := inc $lineNo}}<span class="address-line-{{$realLineNo}}">{{%s}}</span>{{if ne $numLines $realLineNo}}<br>{{end}}{{end}}`, StreetAddress, StreetAddress, toUpper(StreetAddress, "$line", upper)),
        "%D", fmt.Sprintf(`{{if ne .%s "" }}<span class="dependent-locality">{{.%s}}</span>{{end}}`, DependentLocality, toUpper(DependentLocality, "", upper)),
        "%C", fmt.Sprintf(`{{if ne .%s "" }}<span class="locality">{{.%s}}</span>{{end}}`, Locality, toUpper(Locality, "", upper)),
        "%S", fmt.Sprintf(`{{if ne .%s "" }}<span class="administrative-area">{{.%s}}</span>{{end}}`, AdministrativeArea, toUpper(AdministrativeArea, "", upper)),
        "%Z", fmt.Sprintf(`{{if ne .%s "" }}<span class="post-code">{{.%s}}</span>{{end}}`, PostCode, toUpper(PostCode, "", upper)),
        "%X", fmt.Sprintf(`{{if ne .%s "" }}<span class="sorting-code">{{.%s}}</span>{{end}}`, SortingCode, toUpper(SortingCode, "", upper)),
        "%n", "<br>",
    )

    return r.Replace(format)
}

// StringOutputter outputs the formatted address as a string and `\n`s are used for new lines.
type StringOutputter struct{}

// TransformFormat transforms an address format in Google's format into a string template. The upper map is used to
// determine which fields should be converted to UPPERCASE.
func (s StringOutputter) TransformFormat(format string, upper map[Field]struct{}) string {

    r := strings.NewReplacer(
        "%country", fmt.Sprintf("{{.%s}}", toUpper(Country, "", upper)),
        "%N", fmt.Sprintf("{{.%s}}", toUpper(Name, "", upper)),
        "%O", fmt.Sprintf("{{.%s}}", toUpper(Organization, "", upper)),
        "%A", fmt.Sprintf(`{{ join .%s }}`, toUpper(StreetAddress, fmt.Sprintf(`%s "\n"`, StreetAddress), upper)),
        "%D", fmt.Sprintf("{{.%s}}", toUpper(DependentLocality, "", upper)),
        "%C", fmt.Sprintf("{{.%s}}", toUpper(Locality, "", upper)),
        "%S", fmt.Sprintf("{{.%s}}", toUpper(AdministrativeArea, "", upper)),
        "%Z", fmt.Sprintf("{{.%s}}", toUpper(PostCode, "", upper)),
        "%X", fmt.Sprintf("{{.%s}}", toUpper(SortingCode, "", upper)),
        "%n", "\n",
    )

    return r.Replace(format)
}

func toUpper(field Field, fieldName string, upperCaseFields map[Field]struct{}) string {

    fieldToUse := field.String()

    if fieldName != "" {
        fieldToUse = fieldName
    }

    for upperField := range upperCaseFields {
        if field == upperField {
            return fmt.Sprintf("%s | toUpper", fieldToUse)
        }
    }

    return fmt.Sprintf("%s", fieldToUse)
}

func getFormat(countryCode string, latinized bool) (string, bool) {

    countryData := generated.getCountry(countryCode)

    if latinized && countryData.LatinizedFormat != "" {
        return countryData.LatinizedFormat, true
    }

    if countryData.Format != "" && countryData.LatinizedFormat == "" {
        return countryData.Format, true
    }

    if countryData.Format != "" {
        return countryData.Format, false
    }

    return generated.getCountry("ZZ").Format, true
}