1
\$\begingroup\$

Simple truncating method for characters, words, and sentences for a personal utility library I've been working on.

Improvements & criticism welcomed! Especially around default param usage, as I've not utilized it much to date.

const truncate = ((
  value = '',
  int = 0,
  elipsis = '\u2026', /* ... */
  prune = (val, num, at) => val.split(at).splice(0, num).join(at)
) => {
  const sentences = (val = value, num = int, end = elipsis) => prune(val, '.', num) + end;
  const words = (val = value, num = int, end = elipsis) => prune(val, ' ', num) + end;
  const characters = (val = value, num = int, end = elipsis) => val.substring(0, num) + end;
  return {sentences, words, characters};
})();

console.clear();
console.log([
  truncate.sentences("This is a sentence. There are many like it but you won't see the 2nd one", 1, '.'),
  truncate.words("These are words but you won't see what's after this because it'll be hidden", 10),
  truncate.characters("This won't exceed 31 characters so we cutting off the ending", 31, '!')
]);

\$\endgroup\$
2
  • 1
    \$\begingroup\$ What does 10 at truncate.words("These are words but you won't see what's after this because it'll be hidden", 10) reflect at output? Is [ ".", "…", "This won't exceed 31 characters!" ] the expected result? \$\endgroup\$ Jan 2 '19 at 21:42
  • \$\begingroup\$ Looks like i messed up a few things when I renamed the variables and didn't test: jsfiddle.net/darcher/j36mzp4w <-- original \$\endgroup\$
    – darcher
    Jan 3 '19 at 14:30
1
\$\begingroup\$

Review

There was a slight bug in your code. The last two arguments for prune were in the wrong order.

You have a lot of duplicated code. Good code is DRY (Don't Repeat Yourself)

The code is clean and style is consistent.

Some points

  • I can not see the point of arguments as part of the singleton pattern. Not that there's is anything wrong with it, it just seems to add code for no benefit.

  • Default parameters should be set at the bottom of the call stack (the function where the arguments are used). This means that you don't need to repeat the defaults each time you pass on arguments and lets you use a shorter syntax.

  • You can reduce the source size by moving the three truncate functions into the returned singleton. No need to type their names twice.

  • Naming is somewhat poor,

    • Don't name variables by their type. int ??? I assume you mean its an integer, but that gives no information as to what it does. Maybe count. Don't name variables by their type, that is inferred.
    • num ??? LOL again, you are naming for what they are. Name for what the represent. Maybe count.
    • value Is string or string like, but value implies a quantitative like property. A clearer name could be str or string.
    • truncate truncates but internally you prune Be consistent when naming different parts of your code. prune may be better as truncate.

Design

You are mixing function roles. Eg truncate.words calls prune and then appends end. Adding end is better done in prune. When you find yourself repeating code ask yourself why? Am I mixing roles?

Singletons provide great encapsulation, and I think are one of the best ways to maintain a trusted state. However that trust is only as strong as the weakest link. You can add one more level of encapsulation by freezing the singleton you return using Object.freeze, it also gives a small increase in performance.

Rewrite

Less code means less room for bugs. This is part of the DRY philosophy. The aim of the rewrite is to reduce repeated source code.

The first version is not as DRY as it could be (the repeated (...args) => truncate(".", ...args)) but there is a point where striving to DRY out code makes it more obscure.

The first example is preferable, the second example is just to demonstrate how code can become less clears as it is compressed.

const truncate = (() => {
  const truncate = (at, str = "", count = 1, end = "\u2026") =>
    (at === "" ? str.substring(0, count) : str.split(at).splice(0, count).join(at)) + end;
  return Object.freeze({
    sentences: (...args) => truncate(".", ...args),
    words: (...args) => truncate(" ", ...args),
    characters: (...args) => truncate("", ...args),
  });
})();



const l=d=>Log.appendChild(Object.assign(document.createElement("div"),{textContent: d}));
l(truncate.sentences("This is a sentence. There are many like it", 1, "."))
l(truncate.words("These are words but you won't see what's after this  foo", 10))
l(truncate.characters("This won't exceed 31 characters so ", 31, "!"))
<code id="Log"></code>

This example is on the border of too DRY

const truncate = (() => {
  const API = {}, types = [["sentences", "."], ["words", " "], ["characters", ""]];
  const trunc = (at, str = "", count = 1, end = "\u2026") =>
    (at === "" ? str.substring(0, count) : str.split(at).splice(0, count).join(at)) + end;
  types.forEach(type => API[type[0]] = (...args) => trunc(type[1], ...args));
  return Object.freeze(API);
})();


const l=d=>Log.appendChild(Object.assign(document.createElement("div"),{textContent: d}));
l(truncate.sentences("This is a sentence. There are many like it", 1, "."))
l(truncate.words("These are words but you won't see what's after this  foo", 10))
l(truncate.characters("This won't exceed 31 characters so ", 31, "!"))
<code id="Log"></code>

\$\endgroup\$
1
  • \$\begingroup\$ This is why I love peer review, really helps to get different perspectives and in this case, greatly elevated information. I really appreciate this feedback. \$\endgroup\$
    – darcher
    Jan 3 '19 at 14:23
1
\$\begingroup\$

The parameters of sentences and words appear to be out of order when passed to prune(). Not sure why the parameters passed to prune are hard-coded at the example, though still passed to prune(). Since words does not pass a parameter for at, 10 is used, which is not the expected result, though can be remedied by defining the parameter at at words and sentences functions, given that each function call expects all three parameters, and at is not defined at any of the functions at the code at the question; instead num is set to int.

Adjusted body of prune to include an immediately invoked arrow function to handle the resulting array of .split() where delimiter character ! is not contained in string by checking if the element at index 0 of array is equal to val, if true spread val to array else use existing array to chain .splice() to, then concatenate an empty string or at following .join() call. At which point the same function can be substituted for sentences, words and characters.

const truncate = ((
  value = '',
  int = 0,
  elipsis = '\u2026', /* ... */
  prune = (val, at, num) => 
            ((s, k = s[0] === val) => 
              `${(k ? [...val] : s)
                 .splice(0, num)
                 .join(k ? '' : at)}${k ? at : ''}`)
            (val.split(at))
) => {
  const sentences = (val = value, at, num = int, end = elipsis) => prune(val, at, num) + end;
  const words = (val = value, at, num = int, end = elipsis) => prune(val, at, num) + end;
  const characters = (val = value, at, num = int, end = elipsis) => prune(val, at, num) + end;
  return {sentences, words, characters};
})();

console.clear();
console.log([
  truncate.sentences("This is a sentence. There are many like it but you won't see the 2nd one", '.', 1),
  truncate.words("These are words but you won't see what's after this because it'll be hidden", ' ', 10),
  truncate.characters("This won't exceed 31 characters so we cutting off the ending", '!', 31)
]);

Alternatively, though not consistent with the sentences, words can be composed with at being set as a default parameter

const words = ({val = value, at = ' ', num = int, end = elipsis} = {}) => prune(val, at, num) + end;

which would be called with

truncate.words({val: "These are words but you won't see what's after this because it'll be hidden", num: 10})

providing the ability to call the function without explicitly setting and passing at as a parameter.

Substituting a single function for sentences, words and characters

const truncate = (
  value = '',
  at = '',
  num = 0,
  elipsis = '\u2026', /* ... */
) => ((val, k = val[0] === value) => 
       `${(k ? [...value] : val).splice(0, num)
          .join(k ? '' : at)}${k ? at : ''}${elipsis}`)
     (value.split(at));

console.clear();
console.log([
  truncate("This is a sentence. There are many like it but you won't see the 2nd one", '.', 1),
  truncate("These are words but you won't see what's after this because it'll be hidden", ' ', 10),
  truncate("This won't exceed 31 characters so we cutting off the ending", '!', 31)
]);

\$\endgroup\$
1
  • \$\begingroup\$ Really good feedback, I can certainly see my shortcomings and like your use of template literals here. I can certainly see the benefit. \$\endgroup\$
    – darcher
    Jan 3 '19 at 14:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.