From 105b26c4715f98aef24153a74a043fde7a014419 Mon Sep 17 00:00:00 2001 From: Wei Lim Date: Wed, 19 Mar 2025 10:29:10 -0700 Subject: [PATCH] prompt: support case-insensitive sorting --- cli/azd/pkg/prompt/prompter.go | 8 +++- cli/azd/pkg/stringutil/stringutil.go | 43 +++++++++++++++++++++ cli/azd/pkg/stringutil/stringutil_test.go | 47 +++++++++++++++++++++++ 3 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 cli/azd/pkg/stringutil/stringutil.go create mode 100644 cli/azd/pkg/stringutil/stringutil_test.go diff --git a/cli/azd/pkg/prompt/prompter.go b/cli/azd/pkg/prompt/prompter.go index aad0b359d8d..e55d8e117dc 100644 --- a/cli/azd/pkg/prompt/prompter.go +++ b/cli/azd/pkg/prompt/prompter.go @@ -11,7 +11,6 @@ import ( "os" "slices" "strconv" - "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/MakeNowJust/heredoc/v2" @@ -22,6 +21,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/cloud" "github.com/azure/azure-dev/cli/azd/pkg/environment" "github.com/azure/azure-dev/cli/azd/pkg/input" + "github.com/azure/azure-dev/cli/azd/pkg/stringutil" ) type LocationFilterPredicate func(loc account.Location) bool @@ -157,7 +157,7 @@ func (p *DefaultPrompter) PromptResourceGroupFrom( } slices.SortFunc(groups, func(a, b *azapi.Resource) int { - return strings.Compare(a.Name, b.Name) + return stringutil.CompareLower(a.Name, b.Name) }) canCreateNeResourceGroup := !options.DisableCreateNew @@ -218,6 +218,10 @@ func (p *DefaultPrompter) getSubscriptionOptions(ctx context.Context) ([]string, return nil, nil, nil, fmt.Errorf("listing accounts: %w", err) } + slices.SortFunc(subscriptionInfos, func(a, b account.Subscription) int { + return stringutil.CompareLower(a.Name, b.Name) + }) + // The default value is based on AZURE_SUBSCRIPTION_ID, falling back to whatever default subscription in // set in azd's config. defaultSubscriptionId := os.Getenv(environment.SubscriptionIdEnvVarName) diff --git a/cli/azd/pkg/stringutil/stringutil.go b/cli/azd/pkg/stringutil/stringutil.go new file mode 100644 index 00000000000..bb9d0cdcefd --- /dev/null +++ b/cli/azd/pkg/stringutil/stringutil.go @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package stringutil + +import ( + "unicode" + "unicode/utf8" +) + +// CompareLower returns an integer comparing two strings in a case-insensitive manner. +// The result will be 0 if a == b, -1 if a < b, and +1 if a > b. +// +// This comparison does not obey locale-specific rules. +// To compare strings based on case-insensitive, locale-specific ordering, see [golang.org/x/text/collate]. +func CompareLower(a, b string) int { + for { + rb, nb := utf8.DecodeRuneInString(b) + if nb == 0 { + // len(a) > len(b), a > b. + return 1 + } + + ra, na := utf8.DecodeRuneInString(a) + if na == 0 { + // len(b) > len(a), b > a. + return -1 + } + + rb = unicode.ToLower(rb) + ra = unicode.ToLower(ra) + + if ra > rb { + return 1 + } else if ra < rb { + return -1 + } + + // Trim slices to the next rune. + a = a[na:] + b = b[nb:] + } +} diff --git a/cli/azd/pkg/stringutil/stringutil_test.go b/cli/azd/pkg/stringutil/stringutil_test.go new file mode 100644 index 00000000000..2e9cd321700 --- /dev/null +++ b/cli/azd/pkg/stringutil/stringutil_test.go @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package stringutil + +import ( + "slices" + "testing" +) + +func TestCompareLowerSort(t *testing.T) { + unordered := []string{ + "Zebra", + "apple", + "applesauce", + "Banana", + "CHERRY", + "date", + "", + "Apple", + "café", + "cafe", + "APPLE", + } + + expected := []string{ + "", + "apple", + "Apple", + "APPLE", + "applesauce", + "Banana", + "cafe", + "café", + "CHERRY", + "date", + "Zebra", + } + + slices.SortFunc(unordered, func(a, b string) int { + return CompareLower(a, b) + }) + + if !slices.Equal(unordered, expected) { + t.Errorf("incorrect sort order:\ngot: %q\nwant: %q", unordered, expected) + } +}