Bug 394516 - Figure out a remaining-time rounding scheme for minutes -> hours/days. r=sdwilsh, r=l10n@mozilla.com (Pike), b-ff3=beltzner

This commit is contained in:
edward.lee@engineering.uiuc.edu 2008-01-22 18:18:24 -08:00
Родитель e72a2e5040
Коммит 390fadeb5d
6 изменённых файлов: 333 добавлений и 24 удалений

Просмотреть файл

@ -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

Просмотреть файл

@ -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 <edward.lee@engineering.uiuc.edu>.
* 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");
}

Просмотреть файл

@ -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 <edward.lee@engineering.uiuc.edu>.
* 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;"));
}

Просмотреть файл

@ -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

Просмотреть файл

@ -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)

Просмотреть файл

@ -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
*