diff --git a/README.md b/README.md index 7076e3d..8949ad7 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Available Commands: Flags: -c, --custom string Custom abbreviation set + --from-front Shorten from the front -h, --help help for abbreviate -l, --language string Language to select (default "en-us") --list List all abbreviate sets by language @@ -68,9 +69,12 @@ Examples: $ abbreviate original strategy-limited stg-ltd -$ abbreviate original strategy-limited --max 11 +$ abbreviate original --max 11 strategy-limited strategy-ltd +$ abbreviate original --max 11 --from-front strategy-limited +stg-limited + $ abbreviate camel --max 99 strategy-limited strategyLimited ``` diff --git a/cmd/camel.go b/cmd/camel.go index 59031c9..275c138 100644 --- a/cmd/camel.go +++ b/cmd/camel.go @@ -29,7 +29,7 @@ var camelCmd = &cobra.Command{ Long: `Abbreviate a string and convert it to camel case.`, Args: validateArgPresent, Run: func(cmd *cobra.Command, args []string) { - abbr := domain.AsPascal(matcher, args[0], optMax) + abbr := domain.AsPascal(matcher, args[0], optMax, optFrmFront) ch := string(abbr[0]) diff --git a/cmd/original.go b/cmd/original.go index 68f87dc..2a5dbe7 100644 --- a/cmd/original.go +++ b/cmd/original.go @@ -27,7 +27,7 @@ var originalCmd = &cobra.Command{ Short: "Abbreviate the string using the original word boundary separators", Args: validateArgPresent, Run: func(cmd *cobra.Command, args []string) { - abbr := domain.AsOriginal(matcher, args[0], optMax) + abbr := domain.AsOriginal(matcher, args[0], optMax, optFrmFront) fmt.Printf("%s", abbr) if optNewline { diff --git a/cmd/pascal.go b/cmd/pascal.go index 06a02a1..98aea8a 100644 --- a/cmd/pascal.go +++ b/cmd/pascal.go @@ -28,7 +28,7 @@ var pascalCmd = &cobra.Command{ Long: `Abbreviate a string and convert it to pascal case.`, Args: validateArgPresent, Run: func(cmd *cobra.Command, args []string) { - abbr := domain.AsPascal(matcher, args[0], optMax) + abbr := domain.AsPascal(matcher, args[0], optMax, optFrmFront) fmt.Printf("%s", abbr) if optNewline { diff --git a/cmd/root.go b/cmd/root.go index e2dca00..ff6102d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -34,6 +34,7 @@ var ( optSet = "common" optCustom = "" optMax = 0 + optFrmFront = false data = packr.New("abbreviate", "../data") matcher *domain.Matcher @@ -119,6 +120,7 @@ func init() { rootCmd.PersistentFlags().StringVarP(&optSet, "set", "s", optSet, "Abbreviation set") rootCmd.PersistentFlags().StringVarP(&optCustom, "custom", "c", optCustom, "Custom abbreviation set") rootCmd.PersistentFlags().IntVarP(&optMax, "max", "m", optMax, "Maximum length of string, keep on abbreviating while the string is longer than this limit") + rootCmd.PersistentFlags().BoolVarP(&optFrmFront, "from-front", "", optFrmFront, "Shorten from the front") } func validateArgPresent(cmd *cobra.Command, args []string) error { diff --git a/cmd/snake.go b/cmd/snake.go index 1027e4d..8454676 100644 --- a/cmd/snake.go +++ b/cmd/snake.go @@ -35,7 +35,7 @@ Where a string is not shortened, it will be converted to snake case anyway, even if this means that the string will end up longer.`, Args: validateArgPresent, Run: func(cmd *cobra.Command, args []string) { - abbr := domain.AsSnake(matcher, args[0], optSnakeSeperator, optMax) + abbr := domain.AsSnake(matcher, args[0], optSnakeSeperator, optMax, optFrmFront) fmt.Printf("%s", abbr) if optNewline { diff --git a/domain/shorteners.go b/domain/shorteners.go index 0e49210..08fa76f 100644 --- a/domain/shorteners.go +++ b/domain/shorteners.go @@ -96,31 +96,31 @@ func (all Sequences) Len() int { type Shortener func(matcher Matcher, original string, max int) string // AsOriginal discovers words using camel case and non letter characters, -// starting from the back until the string has less than 'max' characters +// starting from the back or the front until the string has less than 'max' characters // or it can't shorten any more. -func AsOriginal(matcher *Matcher, original string, max int) string { +func AsOriginal(matcher *Matcher, original string, max int, frmFront bool) string { if len(original) < max { return original } shortened := NewSequences(original) - for pos := len(shortened) - 1; pos >= 0 && shortened.Len() > max; pos-- { + shorten(shortened, max, frmFront, func(pos int) { str := shortened[pos] abbr := matcher.Match(strings.ToLower(str)) if isTitleCase(str) { abbr = makeTitle(abbr) } shortened[pos] = abbr - } + }) return shortened.String() } // AsSnake discovers words using camel case and non letter characters, -// starting from the back until the string has less than 'max' characters +// starting from the back or the front until the string has less than 'max' characters // or it can't shorten any more. This inserts the specified separator // where a sequence is not alpha-numeric -func AsSnake(matcher *Matcher, original, separator string, max int) string { +func AsSnake(matcher *Matcher, original, separator string, max int, frmFront bool) string { if original == "" { return "" } @@ -142,20 +142,20 @@ func AsSnake(matcher *Matcher, original, separator string, max int) string { return shortened.String() } - for pos := len(shortened) - 1; pos >= 0 && shortened.Len() > max; pos-- { + shorten(shortened, max, frmFront, func(pos int) { str := shortened[pos] abbr := matcher.Match(str) shortened[pos] = abbr - } + }) return shortened.String() } // AsPascal discovers words using camel case and non letter characters, -// starting from the back until the string has less than 'max' characters +// starting from the back or the front until the string has less than 'max' characters // or it can't shorten any more. Word boundaries are a capital letter at // the start of each word -func AsPascal(matcher *Matcher, original string, max int) string { +func AsPascal(matcher *Matcher, original string, max int, frmFront bool) string { if original == "" { return "" } @@ -177,12 +177,12 @@ func AsPascal(matcher *Matcher, original string, max int) string { return shortened.String() } - for pos := len(shortened) - 1; pos >= 0 && shortened.Len() > max; pos-- { + shorten(shortened, max, frmFront, func(pos int) { str := strings.ToLower(shortened[pos]) abbr := matcher.Match(str) abbr = makeTitle(abbr) shortened[pos] = abbr - } + }) return shortened.String() } @@ -228,3 +228,16 @@ func lastChar(str string) (string, rune) { return str[0 : l-1], []rune(str)[l-1:][0] } + +func shorten(sequences Sequences, max int, frmFront bool, shorten func(int)) Sequences { + if frmFront { + for pos := 0; pos < len(sequences) && sequences.Len() > max; pos++ { + shorten(pos) + } + } else { + for pos := len(sequences) - 1; pos >= 0 && sequences.Len() > max; pos-- { + shorten(pos) + } + } + return sequences +} diff --git a/domain/shorteners_test.go b/domain/shorteners_test.go index 7636dd3..22a1190 100644 --- a/domain/shorteners_test.go +++ b/domain/shorteners_test.go @@ -19,30 +19,37 @@ ltd=limited`) name string original string max int + frmFront bool want string }{ {name: "Length longer than origin with '-'", original: "aaa-bbb-ccc", max: 99, want: "aaa-bbb-ccc"}, {name: "Length is 0 with '-'", original: "aaa-bbb-ccc", max: 0, want: "a-b-c"}, {name: "Partial abbreviation with '-'", original: "aaa-bbb-ccc", max: 10, want: "aaa-bbb-c"}, + {name: "Partial abbreviation with '-', start from the front", original: "aaa-bbb-ccc", max: 10, frmFront: true, want: "a-bbb-ccc"}, {name: "Length longer than origin with camel case", original: "AaaBbbCcc", max: 99, want: "AaaBbbCcc"}, {name: "Length is 0 with camel case", original: "AaaBbbCcc", max: 0, want: "ABC"}, {name: "Length is 0 with camel case, matching case", original: "aaaBbbCcc", max: 0, want: "aBC"}, {name: "Partial abbreviation with camel case", original: "AaaBbbCcc", max: 8, want: "AaaBbbC"}, + {name: "Partial abbreviation with camel case, start from the front", original: "AaaBbbCcc", max: 8, frmFront: true, want: "ABbbCcc"}, {name: "Doesn't match wrong casing", original: "AaaBBbCcc", max: 0, want: "ABBbC"}, {name: "Mixed camel case and non word separators", original: "AaaBbb-ccc", max: 0, want: "AB-c"}, {name: "Mixed camel case and non word separators with same borders", original: "Aaa-Bbb-Ccc", max: 0, want: "A-B-C"}, {name: "Real example, full short", original: "strategy-limited", max: 0, want: "stg-ltd"}, {name: "Real example, shorter than total", original: "strategy-limited", max: 13, want: "strategy-ltd"}, + {name: "Real example, shorter than total, start from the front", original: "strategy-limited", max: 13, frmFront: true, want: "stg-limited"}, {name: "Real example, max same as shorted", original: "strategy-limited", max: 12, want: "strategy-ltd"}, + {name: "Real example, max same as shorted, start from the front", original: "strategy-limited", max: 12, frmFront: true, want: "stg-limited"}, {name: "Real example, max on separator", original: "strategy-limited", max: 9, want: "stg-ltd"}, {name: "Real example, max shorter than first word", original: "strategy-limited", max: 6, want: "stg-ltd"}, {name: "Real example, no short", original: "strategy-limited", max: 99, want: "strategy-limited"}, {name: "Real example, with numbers #1", original: "strategy-limited99", max: 15, want: "strategy-ltd99"}, {name: "Real example, with numbers #2", original: "strategy-limited-99", max: 15, want: "strategy-ltd-99"}, + {name: "Real example, with numbers, start from the front #1", original: "strategy-limited99", max: 15, frmFront: true, want: "stg-limited99"}, + {name: "Real example, with numbers, start from the front #2", original: "strategy-limited-99", max: 15, frmFront: true, want: "stg-limited-99"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := AsOriginal(matcher, tt.original, tt.max); got != tt.want { + if got := AsOriginal(matcher, tt.original, tt.max, tt.frmFront); got != tt.want { t.Errorf("AsOriginal('%s', %d) = '%v', want '%v'", tt.original, tt.max, got, tt.want) } }) @@ -162,6 +169,7 @@ ltd=limited`) original string separator string max int + frmFront bool want string }{ {name: "Length is 0 with '-'", original: "aaa-bbb-ccc", separator: "_", max: 0, want: "a_b_c"}, @@ -170,24 +178,31 @@ ltd=limited`) {name: "Length is 0 with camel case", original: "AaaBbbCcc", separator: "_", max: 0, want: "a_b_c"}, {name: "Length is 0 with camel case, matching case", original: "aaaBbbCcc", separator: "_", max: 0, want: "a_b_c"}, {name: "Partial abbreviation with camel case", original: "AaaBbbCcc", separator: "_", max: 8, want: "aaa_b_c"}, + {name: "Partial abbreviation with camel case, start from the front", original: "AaaBbbCcc", separator: "_", max: 8, frmFront: true, want: "a_b_ccc"}, {name: "Doesn't match wrong casing", original: "AaaBBbCcc", separator: "_", max: 0, want: "a_b_bb_c"}, {name: "Mixed camel case and non word separators", original: "AaaBbb-ccc", separator: "_", max: 0, want: "a_b_c"}, {name: "Mixed camel case and non word separators with same borders", separator: "_", original: "Aaa-Bbb-Ccc", max: 0, want: "a_b_c"}, {name: "Real example, full short", original: "strategy-limited", separator: "_", max: 0, want: "stg_ltd"}, {name: "Real example, shorter than total", original: "strategy-limited", separator: "_", max: 13, want: "strategy_ltd"}, + {name: "Real example, shorter than total, start from the front", original: "strategy-limited", separator: "_", max: 13, frmFront: true, want: "stg_limited"}, {name: "Real example, max same as shorted", original: "strategy-limited", separator: "_", max: 12, want: "strategy_ltd"}, + {name: "Real example, max same as shorted, start from the front", original: "strategy-limited", separator: "_", max: 12, frmFront: true, want: "stg_limited"}, {name: "Real example, max on separator", original: "strategy-limited", separator: "_", max: 9, want: "stg_ltd"}, {name: "Real example, max shorter than first word", original: "strategy-limited", separator: "_", max: 6, want: "stg_ltd"}, {name: "Real example, no short", original: "strategy-limited", separator: "_", max: 99, want: "strategy_limited"}, {name: "Real example, with numbers #1", original: "strategy-limited99", separator: "_", max: 15, want: "strategy_ltd_99"}, {name: "Real example, with numbers #2", original: "strategy-limited-99", separator: "_", max: 15, want: "strategy_ltd_99"}, + {name: "Real example, with numbers, start from the front #1", original: "strategy-limited99", separator: "_", max: 15, frmFront: true, want: "stg_limited_99"}, + {name: "Real example, with numbers, start from the front #2", original: "strategy-limited-99", separator: "_", max: 15, frmFront: true, want: "stg_limited_99"}, {name: "Multiple separators", original: "strategy---limited--99", separator: "_", max: 15, want: "strategy_ltd_99"}, + {name: "Multiple separators, start from the front", original: "strategy---limited--99", separator: "_", max: 15, frmFront: true, want: "stg_limited_99"}, {name: "Other separator", original: "strategy-limited-99", separator: "+", max: 15, want: "strategy+ltd+99"}, + {name: "Other separator, start from the front", original: "strategy-limited-99", separator: "+", max: 15, frmFront: true, want: "stg+limited+99"}, {name: "Empty string", original: "", separator: "+", max: 15, want: ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := AsSnake(matcher, tt.original, tt.separator, tt.max); got != tt.want { + if got := AsSnake(matcher, tt.original, tt.separator, tt.max, tt.frmFront); got != tt.want { t.Errorf("AsSnake() = %v, want %v", got, tt.want) } }) @@ -205,30 +220,37 @@ ltd=limited`) name string original string max int + frmFront bool want string }{ {name: "Length longer than origin with '-'", original: "aaa-bbb-ccc", max: 99, want: "AaaBbbCcc"}, {name: "Length is 0 with '-'", original: "aaa-bbb-ccc", max: 0, want: "ABC"}, {name: "Partial abbreviation with '-'", original: "aaa-bbb-ccc", max: 8, want: "AaaBbbC"}, + {name: "Partial abbreviation with '-', start from the front", original: "aaa-bbb-ccc", max: 8, frmFront: true, want: "ABbbCcc"}, {name: "Length longer than origin with camel case", original: "AaaBbbCcc", max: 99, want: "AaaBbbCcc"}, {name: "Length is 0 with camel case", original: "AaaBbbCcc", max: 0, want: "ABC"}, {name: "Length is 0 with camel case, matching case", original: "aaaBbbCcc", max: 0, want: "ABC"}, {name: "Partial abbreviation with camel case", original: "AaaBbbCcc", max: 8, want: "AaaBbbC"}, + {name: "Partial abbreviation with camel case, start from the front", original: "AaaBbbCcc", max: 8, frmFront: true, want: "ABbbCcc"}, {name: "Doesn't match wrong casing", original: "AaaBBbCcc", max: 0, want: "ABBbC"}, {name: "Mixed camel case and non word separators", original: "AaaBbb-ccc", max: 0, want: "ABC"}, {name: "Mixed camel case and non word separators with same borders", original: "Aaa-Bbb-Ccc", max: 0, want: "ABC"}, {name: "Real example, full short", original: "strategy-limited", max: 0, want: "StgLtd"}, {name: "Real example, shorter than total", original: "strategy-limited", max: 13, want: "StrategyLtd"}, + {name: "Real example, shorter than total, start from the front", original: "strategy-limited", max: 13, frmFront: true, want: "StgLimited"}, {name: "Real example, max same as shorted", original: "strategy-limited", max: 12, want: "StrategyLtd"}, + {name: "Real example, max same as shorted", original: "strategy-limited", max: 12, frmFront: true, want: "StgLimited"}, {name: "Real example, max on separator", original: "strategy-limited", max: 9, want: "StgLtd"}, {name: "Real example, max shorter than first word", original: "strategy-limited", max: 6, want: "StgLtd"}, {name: "Real example, no short", original: "strategy-limited", max: 99, want: "StrategyLimited"}, {name: "Real example, with numbers #1", original: "strategy-limited99", max: 15, want: "StrategyLtd99"}, {name: "Real example, with numbers #2", original: "strategy-limited-99", max: 15, want: "StrategyLtd99"}, + {name: "Real example, with numbers #1, start from the front", original: "strategy-limited99", max: 15, frmFront: true, want: "StgLimited99"}, + {name: "Real example, with numbers #2, start from the front", original: "strategy-limited-99", max: 15, frmFront: true, want: "StgLimited99"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := AsPascal(matcher, tt.original, tt.max); got != tt.want { + if got := AsPascal(matcher, tt.original, tt.max, tt.frmFront); got != tt.want { t.Errorf("AsPascal('%s', %d) = '%v', want '%v'", tt.original, tt.max, got, tt.want) } })