Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle download count round up to 1E+3K for when 999,500 <= count <=999,999 #4255

Merged
merged 9 commits into from
Sep 14, 2021

Conversation

erdembayar
Copy link
Contributor

@erdembayar erdembayar commented Sep 9, 2021

Bug

Fixes: NuGet/Home#8800
Fixes: NuGet/Home#10864

Regression? Last working version:

Description

For number higher than 999.5 fractional number(it's less than 1000) it round up to 1000 but it ends up 1E+3K, 1E+3M, 1E+3G text. This problem can be solved in many different ways, but I selected current approach which introduce least breaking change on UI screen, my change affect 0.1% ~ 1/1000 cases, so 999/1000 cases users wouldn't notice it any difference.


Update

Added localization after PR review comment. Now english users see 1B instead of 1G, but depending on localization other users see differently.

Previous experience on PMUI:
image

image

New experience on PMUI:
image

image

Original number - Previous experience - New experience
image

PR Checklist

  • PR has a meaningful title

  • PR has a linked issue.

  • Described changes

  • Tests

    • Automated tests added
    • OR
    • Test exception
    • OR
    • N/A
  • Documentation

    • Documentation PR or issue filled
    • OR
    • N/A

@erdembayar erdembayar requested a review from a team as a code owner September 9, 2021 15:57
++exp;
}

string format = v < 999 ? "{0:G3}{1}" : "{0:G4}{1}"; // If number is 999561 then v = 999.56 so with G3 which result in 1000k ~ 1E+03K, but with G4 it result in 999.6K.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added logic for handling v >= 999, we handle with 1 more precision , but I didn't for v< 999 otherwise it's kind of breaking experience for many users seeing too much detailed information.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't showing a 4th digit of precision when all other cases only show 3 also a kind of "breaking" experience?

How about if (v > 999.0) { v = 999.0; }?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zivkan
Ok. Taken your suggestion. Please check now.

@@ -64,15 +64,17 @@ public static string NumberToString(long number, IFormatProvider culture)

while (v >= 1000)
{
v /= 1000;
v = Math.Round(v / 1000, 2);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On PMUI we don't see 3 digit precision, most 2 digits like below, otherwise previous logic having too detailed info causes problem.
image

@@ -47,8 +47,8 @@ private static bool IsHttpUrl(Uri uri)
return (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
}

private static readonly string[] _scalingFactor = new string[] {
String.Empty,
private static readonly string[] ScalingFactor = new string[] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

@erdembayar erdembayar Sep 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is IDE suggested fix, it looks rule is bit different for static readonly variable.

private static readonly INuGetProjectContext EmptyNuGetProjectContext = new EmptyNuGetProjectContext();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this whole algorithm is too complex and there's a bug here where billions will look like 1.23G because G is specified for billions instead of B. Does this scaling factor need to know about byte size and item count? If so then it would need to differentiate them.

I think we should simplify this method and get rid of the loop which is confusing and made you add this silly looking if statement:

// If number is 999561 then v = 999.56 so with G3 which result in 1000k ~ 1E+03K.
if (v > 999)
{
    v = 999;
}

What do you think about this instead?

public static string NumberToString(long number, IFormatProvider culture)
{
    const double thousand = 1000D;
    const double million = 1000000D;
    const double billion = 1000000000D;
    const double trillion = 1000000000000D;

    if (number >= trillion)
    {
        return (number / trillion).ToString("0.##T", culture);
    }

    if (number >= billion)
    {
        return (number / billion).ToString("0.##B", culture);
    }

    if (number >= million)
    {
        return (number / million).ToString("0.##M", culture);
    }

    if (number >= thousand)
    {
        return (number / thousand).ToString("0.##K", culture);
    }

    return number.ToString("G0", culture);
}

Then you can get rid of the ScalingFactor array as well and this method makes waaaaay more sense.

We might need to methods:
ByteCountToString(): Returns the number as bytes so KB, MB, GB, TB (kilobytes, megabytes, gigabytes, terabytes)
CountToString(): Returns a number as a count so K, M, B, T (thousand, million, billion, trillion)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because G is specified for billions instead of B.

G is the SI (standard international) prefix. If we want to use B in English, then we need to make it a string resource to allow the translators to localize this into the correct value for each language. So that part of it is by-design, I think. See also NuGet/Home#10864

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well yeah while we're here we might as well localize it! Fix two bugs with one stone

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeffkl @zivkan
I took Jeff's code as base and added localization text, please check now.
Number - Previous experience - New experience
image

Copy link
Contributor

@jeffkl jeffkl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to ping me to discuss if you want, I really think we should fix this code as a whole

@@ -47,8 +47,8 @@ private static bool IsHttpUrl(Uri uri)
return (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
}

private static readonly string[] _scalingFactor = new string[] {
String.Empty,
private static readonly string[] ScalingFactor = new string[] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this whole algorithm is too complex and there's a bug here where billions will look like 1.23G because G is specified for billions instead of B. Does this scaling factor need to know about byte size and item count? If so then it would need to differentiate them.

I think we should simplify this method and get rid of the loop which is confusing and made you add this silly looking if statement:

// If number is 999561 then v = 999.56 so with G3 which result in 1000k ~ 1E+03K.
if (v > 999)
{
    v = 999;
}

What do you think about this instead?

public static string NumberToString(long number, IFormatProvider culture)
{
    const double thousand = 1000D;
    const double million = 1000000D;
    const double billion = 1000000000D;
    const double trillion = 1000000000000D;

    if (number >= trillion)
    {
        return (number / trillion).ToString("0.##T", culture);
    }

    if (number >= billion)
    {
        return (number / billion).ToString("0.##B", culture);
    }

    if (number >= million)
    {
        return (number / million).ToString("0.##M", culture);
    }

    if (number >= thousand)
    {
        return (number / thousand).ToString("0.##K", culture);
    }

    return number.ToString("G0", culture);
}

Then you can get rid of the ScalingFactor array as well and this method makes waaaaay more sense.

We might need to methods:
ByteCountToString(): Returns the number as bytes so KB, MB, GB, TB (kilobytes, megabytes, gigabytes, terabytes)
CountToString(): Returns a number as a count so K, M, B, T (thousand, million, billion, trillion)

@aortiz-msft aortiz-msft self-requested a review September 10, 2021 17:43
Copy link
Contributor

@aortiz-msft aortiz-msft left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved for insertion into dev branch.

@erdembayar
Copy link
Contributor Author

@jeffkl
Yes, I thought same approach as yours. But I didn't want to make breaking change this time, let's keep it as it's now.
With 0.##K format it introduces extra precision compare to previous experience, we better to consult PM before making any breaking change.
image

We previously had discussion on replacing G with B then decided not to act that time. For people on other side of world current one is correct.
Also as far I can find only used with download count, it's not used with size conversion with bytes, so not concern for now.

@erdembayar erdembayar requested a review from jeffkl September 10, 2021 18:37
@jeffkl
Copy link
Contributor

jeffkl commented Sep 10, 2021

I don't think changing the precision would be a breaking change but we could keep it as-is with 0.#K right? We can localize the G/B in a subsequent commit but I think we need to simplify this method, especially if the unit test coverage is great which it looks like is based on your added test case data.

Copy link
Contributor

@jeffkl jeffkl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, thanks for localizing it too. Update the PR description to say it fixes NuGet/Home#10864 ?

{
return string.Format(
culture,
"{0:G3}{1}",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better to include the {0:G3} in the localized string? Maybe some cultures prefer to have a space between the number and the suffix, while others don't. For example 123 M vs 123M

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zivkan
I moved into localized string, please review again.

</data>
<data name="Thousand" xml:space="preserve">
<value>K</value>
<comment>1.000 in short format notation</comment>
Copy link
Member

@zivkan zivkan Sep 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

English uses , for thousands separators, so while all of these comments use . which is probably more common across the world, given our code and comments are in English, it looks a bit odd, and this one (thousands) can be mistaken for the number one (precise to 3 decimal places).

Copy link
Contributor

@dominoFire dominoFire left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments on strings. I just want to make sure loc team will not get confused with two different meanings in the same string resource

</data>
<data name="Trillion" xml:space="preserve">
<value>{0:G3}T</value>
<comment>Trillions in short format notation</comment>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please specify which trillions are you referring to:

Suggested change
<comment>Trillions in short format notation</comment>
<comment>American trillions in short format notation</comment>

Copy link
Contributor Author

@erdembayar erdembayar Sep 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dominoFire
Does EU and US trillions use different notations?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://en.wikipedia.org/wiki/Long_and_short_scale

Thanks you. Now I understand what milliard actually means, previously I always thought it's just another regional spelling for million.

@erdembayar erdembayar merged commit 0d46dd8 into dev Sep 14, 2021
@erdembayar erdembayar deleted the dev-eryondon-8800 branch September 14, 2021 00:37
[InlineData(1939, "1.94K", "en-US")]
[InlineData(1939, "1,94K", "da-DK")]
[InlineData(9456, "9.46K", "en-US")]
[InlineData(9456, "9,46K", "da-DK")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[InlineData(9456, "9,46K", "da-DK")]

Could we also add tests for B vs G, since Andy pointed out that G will be used for locales following the metric system?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aortiz-msft
I thought about it but I believe it's available after localization team make translation for each different cultures. @dominoFire Please correct me if I'm wrong.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, since NuGet.Client's localization happens in a different repo, we can't have unit tests that work the same and still pass both locally (that doesn't run MicroBuild's localization task) and on CI (which does run MicroBuild's localization task).

I believe that if we migrated to the same localization system/format that the rest of the dotnet org uses, then local builds would include it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
6 participants