Bug 1260509 - Implement String.prototype.padStart / padEnd; r=jorendorff
authorMorgan Phillips <winter2718@gmail.com>
Thu, 31 Mar 2016 08:04:12 -0700
changeset 291290 d3ba5b5019c6cd9348787e42ad0596c4d535cd14
parent 291289 329a66b8e67c1c9367fbd496752e0488723aa79b
child 291291 b6ea6a3bb8a6fc355b46403919d8c70e798c7007
push id19656
push usergwagner@mozilla.com
push dateMon, 04 Apr 2016 13:43:23 +0000
treeherderb2g-inbound@e99061fde28a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjorendorff
bugs1260509
milestone48.0a1
Bug 1260509 - Implement String.prototype.padStart / padEnd; r=jorendorff
js/src/builtin/String.js
js/src/jsstr.cpp
js/src/tests/ecma_7/String/shell.js
js/src/tests/ecma_7/String/string-pad-start-end.js
--- a/js/src/builtin/String.js
+++ b/js/src/builtin/String.js
@@ -1,14 +1,63 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /*global intl_Collator: false, */
 
+/**
+ * A helper function implementing the logic for both String.prototype.padStart
+ * and String.prototype.padEnd as described in ES7 Draft March 29, 2016
+ */
+function String_pad(maxLength, fillString, padEnd=false) {
+
+    // Steps 1-2.
+    RequireObjectCoercible(this);
+    let str = ToString(this);
+
+    // Steps 3-4.
+    let intMaxLength = ToLength(maxLength);
+    let strLen = str.length;
+
+    // Step 5.
+    if (intMaxLength <= strLen)
+        return str;
+
+    // Steps 6-7.
+    let filler = fillString === undefined ? " " : ToString(fillString);
+
+    // Step 8.
+    if (filler === "")
+        return str;
+
+    // Step 9.
+    let fillLen = intMaxLength - strLen;
+
+    // Step 10.
+    let truncatedStringFiller = callFunction(String_repeat, filler,
+                                             fillLen / filler.length);
+
+    truncatedStringFiller += callFunction(String_substr, filler, 0,
+                                          fillLen % filler.length);
+
+    // Step 11.
+    if (padEnd === true)
+        return str + truncatedStringFiller;
+    return truncatedStringFiller + str;
+}
+
+function String_pad_start(maxLength, fillString=" ") {
+    return callFunction(String_pad, this, maxLength, fillString, false);
+}
+
+function String_pad_end(maxLength, fillString=" ") {
+    return callFunction(String_pad, this, maxLength, fillString, true);
+}
+
 /* ES6 Draft Oct 14, 2014 21.1.3.19 */
 function String_substring(start, end) {
     // Steps 1-3.
     RequireObjectCoercible(this);
     var str = ToString(this);
 
     // Step 4.
     var len = str.length;
--- a/js/src/jsstr.cpp
+++ b/js/src/jsstr.cpp
@@ -4183,16 +4183,18 @@ static const JSFunctionSpec string_metho
     /* Java-like methods. */
     JS_FN(js_toString_str,     str_toString,          0,0),
     JS_FN(js_valueOf_str,      str_toString,          0,0),
     JS_FN("toLowerCase",       str_toLowerCase,       0,JSFUN_GENERIC_NATIVE),
     JS_FN("toUpperCase",       str_toUpperCase,       0,JSFUN_GENERIC_NATIVE),
     JS_INLINABLE_FN("charAt",  str_charAt,            1,JSFUN_GENERIC_NATIVE, StringCharAt),
     JS_INLINABLE_FN("charCodeAt", str_charCodeAt,     1,JSFUN_GENERIC_NATIVE, StringCharCodeAt),
     JS_SELF_HOSTED_FN("substring", "String_substring", 2,0),
+    JS_SELF_HOSTED_FN("padStart", "String_pad_start", 2,0),
+    JS_SELF_HOSTED_FN("padEnd", "String_pad_end", 2,0),
     JS_SELF_HOSTED_FN("codePointAt", "String_codePointAt", 1,0),
     JS_FN("includes",          str_includes,          1,JSFUN_GENERIC_NATIVE),
     JS_FN("contains",          str_contains,          1,JSFUN_GENERIC_NATIVE),
     JS_FN("indexOf",           str_indexOf,           1,JSFUN_GENERIC_NATIVE),
     JS_FN("lastIndexOf",       str_lastIndexOf,       1,JSFUN_GENERIC_NATIVE),
     JS_FN("startsWith",        str_startsWith,        1,JSFUN_GENERIC_NATIVE),
     JS_FN("endsWith",          str_endsWith,          1,JSFUN_GENERIC_NATIVE),
     JS_FN("trim",              str_trim,              0,JSFUN_GENERIC_NATIVE),
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_7/String/string-pad-start-end.js
@@ -0,0 +1,99 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ */
+
+// `this` must be object coercable.
+
+for (let badThis of [null, undefined]) {
+    assertThrowsInstanceOf(() => {
+        String.prototype.padStart.call(badThis, 42, "oups");
+    }, TypeError);
+
+    assertThrowsInstanceOf(() => {
+        String.prototype.padEnd.call(badThis, 42, "oups");
+    }, TypeError);
+}
+
+let proxy = new Proxy({}, {
+get(t, name) {
+  if (name === Symbol.toPrimitive || name === "toString") return;
+  if (name === "valueOf") return () => 42;
+  throw "This should not be reachable";
+}
+});
+
+assertEq("42bloop", String.prototype.padEnd.call(proxy, 7, "bloopie"));
+
+// maxLength must convert to an integer
+
+assertEq("lame", "lame".padStart(0, "foo"));
+assertEq("lame", "lame".padStart(0.1119, "foo"));
+assertEq("lame", "lame".padStart(-0, "foo"));
+assertEq("lame", "lame".padStart(NaN, "foo"));
+assertEq("lame", "lame".padStart(-1, "foo"));
+assertEq("lame", "lame".padStart({toString: () => 0}, "foo"));
+
+assertEq("lame", "lame".padEnd(0, "foo"));
+assertEq("lame", "lame".padEnd(0.1119, "foo"));
+assertEq("lame", "lame".padEnd(-0, "foo"));
+assertEq("lame", "lame".padEnd(NaN, "foo"));
+assertEq("lame", "lame".padEnd(-1, "foo"));
+assertEq("lame", "lame".padEnd({toString: () => 0}, "foo"));
+
+assertThrowsInstanceOf(() => {
+    "lame".padStart(Symbol("9900"), 0);
+}, TypeError);
+
+assertThrowsInstanceOf(() => {
+    "lame".padEnd(Symbol("9900"), 0);
+}, TypeError);
+
+// The fill argument must be string coercable.
+
+assertEq("nulln.", ".".padStart(6, null));
+assertEq(".nulln", ".".padEnd(6, null));
+
+assertEq("[obje.", ".".padStart(6, {}));
+assertEq(".[obje", ".".padEnd(6, {}));
+
+assertEq("1,2,3.", ".".padStart(6, [1, 2, 3]));
+assertEq(".1,2,3", ".".padEnd(6, [1, 2, 3]));
+
+assertEq("aaaaa.", ".".padStart(6, {toString: () => "a"}));
+assertEq(".aaaaa", ".".padEnd(6, {toString: () => "a"}));
+
+// undefined is converted to " "
+
+assertEq("     .", ".".padStart(6, undefined));
+assertEq(".     ", ".".padEnd(6, undefined));
+
+assertEq("     .", ".".padStart(6));
+assertEq(".     ", ".".padEnd(6));
+
+// The empty string has no effect
+
+assertEq("Tilda", "Tilda".padStart(100000, ""));
+assertEq("Tilda", "Tilda".padEnd(100000, ""));
+
+assertEq("Tilda", "Tilda".padStart(100000, {toString: () => ""}));
+assertEq("Tilda", "Tilda".padEnd(100000, {toString: () => ""}));
+
+// Test repetition against a bruteforce implementation
+
+let filler = "space";
+let truncatedFiller = "";
+for (let i = 0; i < 2500; i++) {
+    truncatedFiller += filler[i % filler.length];
+    assertEq(truncatedFiller + "goto", "goto".padStart(5 + i, filler));
+    assertEq("goto" + truncatedFiller, "goto".padEnd(5 + i, filler));
+}
+
+// [Argument] Length
+
+assertEq(1, String.prototype.padStart.length)
+assertEq(1, String.prototype.padEnd.length)
+
+if (typeof reportCompare === "function")
+    reportCompare(true, true);
+