From 8dbab0839a01fc4da3ee64d3753715fb1b8390fe Mon Sep 17 00:00:00 2001 From: "edward.lee%engineering.uiuc.edu" Date: Wed, 23 Jan 2008 02:18:27 +0000 Subject: [PATCH] Bug 394516 - Figure out a remaining-time rounding scheme for minutes -> hours/days. r=sdwilsh, r=l10n@mozilla.com (Pike), b-ff3=beltzner git-svn-id: svn://10.0.0.236/trunk@243789 18797224-902f-48f8-a5cc-f745e15eee43 --- mozilla/intl/locale/src/Makefile.in | 4 + mozilla/intl/locale/src/PluralForm.jsm | 135 ++++++++++++++++++ .../tests/unit/test_pluralForm_english.js | 55 +++++++ .../en-US/chrome/global/intl.properties | 7 + .../mozapps/downloads/downloads.properties | 26 ++-- .../mozapps/downloads/src/DownloadUtils.jsm | 130 ++++++++++++++--- 6 files changed, 333 insertions(+), 24 deletions(-) create mode 100644 mozilla/intl/locale/src/PluralForm.jsm create mode 100644 mozilla/intl/locale/tests/unit/test_pluralForm_english.js diff --git a/mozilla/intl/locale/src/Makefile.in b/mozilla/intl/locale/src/Makefile.in index 9e06dd68838..8346b58b383 100644 --- a/mozilla/intl/locale/src/Makefile.in +++ b/mozilla/intl/locale/src/Makefile.in @@ -81,6 +81,10 @@ EXPORT_RESOURCE = \ $(srcdir)/language.properties \ $(NULL) +EXTRA_JS_MODULES = \ + PluralForm.jsm \ + $(NULL) + # we don't want the shared lib, but we want to force the creation of a static lib. FORCE_STATIC_LIB = 1 diff --git a/mozilla/intl/locale/src/PluralForm.jsm b/mozilla/intl/locale/src/PluralForm.jsm new file mode 100644 index 00000000000..98c6d88ed22 --- /dev/null +++ b/mozilla/intl/locale/src/PluralForm.jsm @@ -0,0 +1,135 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Plural Form l10n Code. + * + * The Initial Developer of the Original Code is + * Edward Lee . + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +EXPORTED_SYMBOLS = [ "PluralForm" ]; + +/** + * This module provides the PluralForm object which contains a method to figure + * out which plural form of a word to use for a given number based on the + * current localization. + * + * List of methods: + * + * string pluralForm + * get(int aNum, string aWords) + */ + +const Cc = Components.classes; +const Ci = Components.interfaces; + +const kIntlProperties = "chrome://global/locale/intl.properties"; + +// These are the available plural functions that give the appropriate index +// based on the plural rule number specified +let gFunctions = [ + function(n) 0, + function(n) n!=1?1:0, + function(n) n>1?1:0, + function(n) n%10==1&&n%100!=11?1:n!=0?2:0, + function(n) n==1?0:n==2?1:2, + function(n) n==1?0:n==0||n%100>0&&n%100<20?1:2, + function(n) n%10==1&&n%100!=11?0:n%10>=2&&(n%100<10||n%100>=20)?2:1, + function(n) n%10==1&&n%100!=11?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2, + function(n) n==1?0:n>=2&&n<=4?1:2, + function(n) n==1?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2, + function(n) n%100==1?0:n%100==2?1:n%100==3||n%100==4?2:3 +]; + +let PluralForm = { + /** + * Get the correct plural form of a word based on the number + * + * @param aNum + * The number to decide which plural form to use + * @param aWords + * A semi-colon (;) separated string of words to pick the plural form + * @return The appropriate plural form of the word + */ + get: (function initGetPluralForm() + { + // initGetPluralForm gets called right away when this module is loaded and + // creates getPluralForm function. The function it creates is based on the + // value of pluralRule specified in the intl stringbundle. + // See: http://developer.mozilla.org/en/docs/Localization_and_Plurals + + // Get the plural rule number from the intl stringbundle + let ruleNum = Number(Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService).createBundle(kIntlProperties). + GetStringFromName("pluralRule")); + + // Default to "all plural" if the value is out of bounds or invalid + if (ruleNum < 0 || ruleNum >= gFunctions.length || isNaN(ruleNum)) { + log(["Invalid rule number: ", ruleNum, " -- defaulting to 0"]); + ruleNum = 0; + } + + // Return a function that gets the right plural form + let pluralFunc = gFunctions[ruleNum]; + return function(aNum, aWords) { + // Figure out which index to use for the semi-colon separated words + let index = pluralFunc(aNum ? Number(aNum) : 0); + let words = aWords ? aWords.split(/;/) : [""]; + + let ret = words[index]; + + // Check for array out of bounds or empty strings + if ((ret == undefined) || (ret == "")) { + // Display a message in the error console + log(["Index #", index, " of '", aWords, "' for value ", aNum, + " is invalid -- plural rule #", ruleNum]); + + // Default to the first entry (which might be empty, but not undefined) + ret = words[0]; + } + + return ret; + }; + })(), +}; + +/** + * Private helper function to log errors to the error console and command line + * + * @param aMsg + * Error message to log or an array of strings to concat + */ +function log(aMsg) +{ + let msg = "PluralForm.jsm: " + (aMsg.join ? aMsg.join("") : aMsg); + Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService). + logStringMessage(msg); + dump(msg + "\n"); +} diff --git a/mozilla/intl/locale/tests/unit/test_pluralForm_english.js b/mozilla/intl/locale/tests/unit/test_pluralForm_english.js new file mode 100644 index 00000000000..3293e90b8c4 --- /dev/null +++ b/mozilla/intl/locale/tests/unit/test_pluralForm_english.js @@ -0,0 +1,55 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Plural Form l10n Test Code. + * + * The Initial Developer of the Original Code is + * Edward Lee . + * Portions created by the Initial Developer are Copyright (C) 2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +/** + * This unit test makes sure the plural form for the default language (by + * development), English, is working for the PluralForm javascript module. + */ + +Components.utils.import("resource://gre/modules/PluralForm.jsm"); + +function run_test() +{ + // Make sure for good inputs, things work as expected + for (var num = 0; num <= 1000; num++) + do_check_eq(num == 1 ? "word" : "words", PluralForm.get(num, "word;words")); + + // Not having enough plural forms defaults to the first form + do_check_eq("word", PluralForm.get(2, "word")); + + // Empty forms defaults to the first form + do_check_eq("word", PluralForm.get(2, "word;")); +} diff --git a/mozilla/toolkit/locales/en-US/chrome/global/intl.properties b/mozilla/toolkit/locales/en-US/chrome/global/intl.properties index 741e0971118..8afc0f37363 100644 --- a/mozilla/toolkit/locales/en-US/chrome/global/intl.properties +++ b/mozilla/toolkit/locales/en-US/chrome/global/intl.properties @@ -6,6 +6,13 @@ # charset names and use canonical names exactly as listed there. # Also note that "UTF-8" should always be included in intl.charsetmenu.browser.static general.useragent.locale=en-US + +# LOCALIZATION NOTE (pluralRule): Pick the appropriate plural rule for your +# language. This will determine how many plural forms of a word you will need +# to provide and in what order. +# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals +pluralRule=1 + # Localization Note: font.language.group controls the initial setting of the # language drop-down in the fonts pref panel. Set it to the value of one of the # menuitems in the "selectLangs" menulist in diff --git a/mozilla/toolkit/locales/en-US/chrome/mozapps/downloads/downloads.properties b/mozilla/toolkit/locales/en-US/chrome/mozapps/downloads/downloads.properties index 91fe93264a0..50120e33e5b 100644 --- a/mozilla/toolkit/locales/en-US/chrome/mozapps/downloads/downloads.properties +++ b/mozilla/toolkit/locales/en-US/chrome/mozapps/downloads/downloads.properties @@ -1,3 +1,10 @@ +# LOCALIZATION NOTE (seconds, minutes, hours, days): Semi-colon list of plural +# forms. See: http://developer.mozilla.org/en/docs/Localization_and_Plurals +seconds=second;seconds +minutes=minute;minutes +hours=hour;hours +days=day;days + # LOCALIZATION NOTE (paused): — is the "em dash" (long dash) paused=Paused — #1 downloading=Downloading @@ -43,14 +50,17 @@ transferSameUnits=#1 of #3 #4 transferDiffUnits=#1 #2 of #3 #4 transferNoTotal=#1 #2 -# LOCALIZATION NOTE (timeMinutesLeft): number of minutes left (greater than 1) -# LOCALIZATION NOTE (timeSecondsLeft): number of seconds left (greater than 3) -# 3 min -> 2 min -> 60 secs -> 59 secs -> … -> 5 secs -> 4 secs -> few secs -# examples: 11 minutes left; 11 seconds left; -timeMinutesLeft=#1 minutes left -timeSecondsLeft=#1 seconds left -timeFewSeconds=A few seconds left -timeUnknown=Unknown time left +# LOCALIZATION NOTE (timePair): #1 time number; #2 time unit +# example: 1 minute; 11 hours +timePair=#1 #2 +# LOCALIZATION NOTE (timeLeftSingle): #1 time left +# example: 1 minute remaining; 11 hours remaining +timeLeftSingle=#1 remaining +# LOCALIZATION NOTE (timeLeftDouble): #1 time left; #2 time left sub units +# example: 11 hours, 2 minutes remaining; 1 day, 22 hours remaining +timeLeftDouble=#1, #2 remaining +timeFewSeconds=A few seconds remaining +timeUnknown=Unknown time remaining # LOCALIZATION NOTE (doneStatus): — is the "em dash" (long dash) # #1 download size for FINISHED or download state; #2 host (e.g., eTLD + 1, IP) diff --git a/mozilla/toolkit/mozapps/downloads/src/DownloadUtils.jsm b/mozilla/toolkit/mozapps/downloads/src/DownloadUtils.jsm index 847bdd7d92e..b71ab062ac3 100644 --- a/mozilla/toolkit/mozapps/downloads/src/DownloadUtils.jsm +++ b/mozilla/toolkit/mozapps/downloads/src/DownloadUtils.jsm @@ -58,10 +58,15 @@ EXPORTED_SYMBOLS = [ "DownloadUtils" ]; * * [double convertedBytes, string units] * convertByteUnits(int aBytes) + * + * [int time, string units, int subTime, string subUnits] + * convertTimeUnits(double aSecs) */ const Cc = Components.classes; const Ci = Components.interfaces; +const Cu = Components.utils +Cu.import("resource://gre/modules/PluralForm.jsm"); const kDownloadProperties = "chrome://mozapps/locale/downloads/downloads.properties"; @@ -73,13 +78,16 @@ let gStr = { transferSameUnits: "transferSameUnits", transferDiffUnits: "transferDiffUnits", transferNoTotal: "transferNoTotal", - timeMinutesLeft: "timeMinutesLeft", - timeSecondsLeft: "timeSecondsLeft", + timePair: "timePair", + timeLeftSingle: "timeLeftSingle", + timeLeftDouble: "timeLeftDouble", timeFewSeconds: "timeFewSeconds", timeUnknown: "timeUnknown", doneScheme: "doneScheme", doneFileScheme: "doneFileScheme", units: ["bytes", "kilobyte", "megabyte", "gigabyte"], + // Update timeSize in convertTimeUnits if changing the length of this array + timeUnits: ["seconds", "minutes", "hours", "days"], }; // Convert strings to those in the string bundle @@ -122,7 +130,7 @@ let DownloadUtils = { // Calculate the time remaining if we have valid values let seconds = (aSpeed > 0) && (aMaxBytes > 0) ? - Math.ceil((aMaxBytes - aCurrBytes) / aSpeed) : -1; + (aMaxBytes - aCurrBytes) / aSpeed : -1; // Update the bytes transferred and bytes total let (transfer = DownloadUtils.getTransferTotal(aCurrBytes, aMaxBytes)) { @@ -186,7 +194,8 @@ let DownloadUtils = { /** * Generate a "time left" string given an estimate on the time left and the * last time. The extra time is used to give a better estimate on the time to - * show. + * show. Both the time values are doubles instead of integers to help get + * sub-second accuracy for current and future estimates. * * @param aSeconds * Current estimate on number of seconds left for the download @@ -202,12 +211,22 @@ let DownloadUtils = { if (aSeconds < 0) return [gStr.timeUnknown, aLastSec]; - // Reuse the last seconds if the new one is only slighty longer - // This avoids jittering seconds, e.g., 41 40 38 40 -> 41 40 38 38 - // However, large changes are shown, e.g., 41 38 49 -> 41 38 49 - let (diff = aSeconds - aLastSec) { - if (diff > 0 && diff <= 10) - aSeconds = aLastSec; + // Apply smoothing only if the new time isn't a huge change -- e.g., if the + // new time is more than half the previous time; this is useful for + // downloads that start/resume slowly + if (aSeconds > aLastSec / 2) { + // Apply hysteresis to favor downward over upward swings + // 30% of down and 10% of up (exponential smoothing) + let (diff = aSeconds - aLastSec) { + aSeconds = aLastSec + (diff < 0 ? .3 : .1) * diff; + } + + // If the new time is similar, reuse something close to the last seconds, + // but subtract a little to provide forward progress + let diff = aSeconds - aLastSec; + let diffPct = diff / aLastSec * 100; + if (Math.abs(diff) < 5 || Math.abs(diffPct) < 5) + aSeconds = aLastSec - (diff < 0 ? .4 : .2); } // Decide what text to show for the time @@ -215,13 +234,24 @@ let DownloadUtils = { if (aSeconds < 4) { // Be friendly in the last few seconds timeLeft = gStr.timeFewSeconds; - } else if (aSeconds <= 60) { - // Show 2 digit seconds starting at 60 - timeLeft = replaceInsert(gStr.timeSecondsLeft, 1, aSeconds); } else { - // Show minutes - timeLeft = replaceInsert(gStr.timeMinutesLeft, 1, - Math.ceil(aSeconds / 60)); + // Convert the seconds into its two largest units to display + let [time1, unit1, time2, unit2] = + DownloadUtils.convertTimeUnits(aSeconds); + + let pair1 = replaceInsert(gStr.timePair, 1, time1); + pair1 = replaceInsert(pair1, 2, unit1); + let pair2 = replaceInsert(gStr.timePair, 1, time2); + pair2 = replaceInsert(pair2, 2, unit2); + + // Only show minutes for under 1 hour or the second pair is 0 + if (aSeconds < 3600 || time2 == 0) { + timeLeft = replaceInsert(gStr.timeLeftSingle, 1, pair1); + } else { + // We've got 2 pairs of times to display + timeLeft = replaceInsert(gStr.timeLeftDouble, 1, pair1); + timeLeft = replaceInsert(timeLeft, 2, pair2); + } } return [timeLeft, aSeconds]; @@ -315,8 +345,76 @@ let DownloadUtils = { return [aBytes, gStr.units[unitIndex]]; }, + + /** + * Converts a number of seconds to the two largest units. Time values are + * whole numbers, and units have the correct plural/singular form. + * + * @param aSecs + * Seconds to convert into the appropriate 2 units + * @return 4-item array [first value, its unit, second value, its unit] + */ + convertTimeUnits: function(aSecs) + { + // These are the maximum values for seconds, minutes, hours corresponding + // with gStr.timeUnits without the last item + let timeSize = [60, 60, 24]; + + let time = aSecs; + let scale = 1; + let unitIndex = 0; + + // Keep converting to the next unit while we have units left and the + // current one isn't the largest unit possible + while ((unitIndex < timeSize.length) && (time >= timeSize[unitIndex])) { + time /= timeSize[unitIndex]; + scale *= timeSize[unitIndex]; + unitIndex++; + } + + let value = convertTimeUnitsValue(time); + let units = convertTimeUnitsUnits(value, unitIndex); + + let extra = aSecs - value * scale; + let nextIndex = unitIndex - 1; + + // Convert the extra time to the next largest unit + for (let index = 0; index < nextIndex; index++) + extra /= timeSize[index]; + + let value2 = convertTimeUnitsValue(extra); + let units2 = convertTimeUnitsUnits(value2, nextIndex); + + return [value, units, value2, units2]; + }, }; +/** + * Private helper for convertTimeUnits that gets the display value of a time + * + * @param aTime + * Time value for display + * @return An integer value for the time rounded down + */ +function convertTimeUnitsValue(aTime) +{ + return Math.floor(aTime); +} + +/** + * Private helper for convertTimeUnits that gets the display units of a time + * + * @param aTime + * Time value for display + * @param aIndex + * Index into gStr.timeUnits for the appropriate unit + * @return The appropriate plural form of the unit for the time + */ +function convertTimeUnitsUnits(aTime, aIndex) +{ + return PluralForm.get(aTime, gStr.timeUnits[aIndex]); +} + /** * Private helper function to replace a placeholder string with a real string *