10
\$\begingroup\$

Given an unique string that represents a sequence of characters, the class should implement three methods (getNextChar, getNextString and the genSequence generator).

How it works

  1. We give a sequence of characters. For example: "abc"
  2. We give a starting string. For example: "a"
  3. The generator yields the following strings infinitely.

The next string of an existing string works like a dictionary, only the length doesn't change as long as there are still characters to fulfill.

a
b
c
aa
ab
ac
ba
..
cc
aaa
aab

This is not homework, but a user in Stack Overflow chat presented the challenge and I considered taking it to a higher level.

class SeqString {

    constructor (sequence) {

        this._sequence = sequence;
        this._first    = sequence[0];
        this._last     = sequence.slice(-1);

        return this;
    }

    getNextChar (char) {

        let seq   = this._sequence,
            index = (seq.indexOf(char) + 1) % seq.length;

        return seq[index];
    }

    getNextString (str) {

        let last = str.slice(-1),
            init = str.slice(0, str.length - 1),

            lastOfSeq  = this._last,
            firstOfSeq = this._first;

        // If it is overflowing
        if (last === lastOfSeq) {

            let trail = 1,
                i     = init.length;

            // Look for an index that won't overflow
            while (i-- && init[i] === lastOfSeq) ++trail;

            // If there is no such index, then the result is a new sequence
            // with an increased length
            if (i === -1) return firstOfSeq.repeat(trail + 1);

            // If there is, change the matching character at the index
            // and reset every character after that
            str = init.slice(0, i) + this.getNextChar(init[i]) +
                  firstOfSeq.repeat(trail);

            return str;

        }

        return init + this.getNextChar(last);
    }

    *genSequence (str) {

        let newStr = this.getNextString(str);

        yield newStr;
        yield* this.genSequence(newStr);    
    }

}

Specific questions

  • How can I improve my class functions, while maintaining a balance between performance and readability?
  • Tips on how I could make the code more readable for others.

Test cases

I have made a function test, to help debugging.

function test (sequence, startStr, times = 10) {

    let it = new SeqString(sequence).genSequence(startStr);

    while (times--)
        console.log(it.next().value);
}

Examples of output

Alphaset tests

const lowerAlpha = "abcdefghijklmnopqrstuvwxyz";
const alpha      = lowerAlpha + lowerAlpha.toUpperCase();

test(alpha, 'abc', 1); // abd 
test(alpha, 'BZZ', 1); // Caa
test(alpha, 'Z', 1); // aa 
test(alpha, 'ZZ', 1); // aaa

A long sequence for test(alpha, "Stack"): http://lpaste.net/137336

Other tests

test("pen", "p", 10);

e
n
pp
..
np

test("0123456789", "5", 10);

6
7
8
9
00
01
02
..

It should

  • Use ES6 features
  • Expose a generator
  • Handle every kind of unique sequence
  • Be readable
\$\endgroup\$
2
\$\begingroup\$

Well, to me it looks pretty good. I've made a few minor tweaks, mainly renaming variables, removing unneeded temp variables and renaming the class and methods, to make the code a little more self documenting.

There was one thing that struct me as odd. Your class only works with charSets which contain unique chars. "ppen" will output "p" ten times.

I would definitely add a pruning feature to remove any duplicates, that would make it a more robust class. Something like this first snippet below.

const CharStitcher = (function(){

    class CharStitcher {

        constructor (string) {
            string = CharStitcher.pruneChars(string);
            this._str = string;
            this._1st = string[0];
            this._last = string[ string.length -1 ];
            return this;
        }

        next (char) {
            return this._str[ ( this._str.indexOf(char) + 1 ) % this._str.length ];
        }

        genString (str) {

            let lastChar = str.slice(-1);
            let otherChars = str.slice(0, str.length - 1);

            if (lastChar === this._last) {
                let charAt = 1;
                let upTo = otherChars.length;
                while ( otherChars[upTo] === this._last && --upTo ) ++charAt;
                if ( upTo === -1 ) return this._1st.repeat( charAt + 1 );
                return otherChars.slice( 0, upTo ) + this.next( otherChars[upTo] ) + this._1st.repeat( charAt );
            }

            return otherChars + this.next(lastChar);
        }

        *stitch (str, newStr) {
            newStr = this.genString(str);
            yield newStr, yield* this.stitch(newStr);    
        }

    }

    CharStitcher.pruneChars = function (source, pruned){
      pruned = {}
      source.split('').forEach(function(char){
        if(!Object.prototype.hasOwnProperty.call(pruned, char)){
          pruned[char] = undefined;
        }
      })
      return Object.keys(pruned).join('');
    }

    return CharStitcher;

})();

Otherwise, I've removed all the comments, I feel it remains just as readable as your initial version.

class CharStitcher {

    constructor (string) {
        this._str = string;
        this._1st = string[0];
        this._last = string[ string.length -1 ];
        return this;
    }

    next (char) {
        return this._str[ ( this._str.indexOf(char) + 1 ) % this._str.length ];
    }

    genString (str) {

        let lastChar = str.slice(-1);
        let otherChars = str.slice(0, str.length - 1);

        if (lastChar === this._last) {
            let charAt = 1;
            let upTo = otherChars.length;
            while ( otherChars[upTo] === this._last && --upTo ) ++charAt;
            if ( upTo === -1 ) return this._1st.repeat( charAt + 1 );
            return otherChars.slice( 0, upTo ) + this.next( otherChars[upTo] ) + this._1st.repeat( charAt );
        }

        return otherChars + this.next(lastChar);
    }

    *stitch (str, newStr) {
        newStr = this.genString(str);
        yield newStr;
        yield* this.stitch(newStr);    
    }

}

I've also rewritten your test a little bit, also just minor tweaks.

function test (charSet, startStr, times = 10) {
    let progress = new CharStitcher(charSet).stitch(startStr);
    while ( times-- ) console.log(progress.next().value);
}

test("pen", "p", 10);

class CharStitcher {

    constructor (string) {
        this._str = string;
        this._1st = string[0];
        this._last = string[ string.length -1 ];
        return this;
    }

    next (char) {
        return this._str[ ( this._str.indexOf(char) + 1 ) % this._str.length ];
    }

    genString (str) {

        let lastChar = str.slice(-1);
        let otherChars = str.slice(0, str.length - 1);

        if (lastChar === this._last) {

            let charAt = 1;
            let upTo = otherChars.length;

            while ( otherChars[upTo] === this._last && upTo-- && ++charAt);

            if ( upTo === -1 ) return this._1st.repeat( charAt + 1 );
            
            return otherChars.slice( 0, upTo ) + this.next( otherChars[upTo] ) + this._1st.repeat( charAt );

        }

        return otherChars + this.next(lastChar);

    }

    *stitch (str, newStr) {

        newStr = this.genString(str);
        
        yield newStr;
        
        yield* this.stitch(newStr);    

    }

}


function test (charSet, startStr, times = 10) {
    let progress = new CharStitcher(charSet).stitch(startStr);
    while ( times-- ) console.log(progress.next().value);
}

test("pen", "p", 10);

Again, nothing major, hope my comments are helpful.

\$\endgroup\$
1
\$\begingroup\$

Why return this at the end of the constructor? I understand the point of doing it in other methods (e.g. to support chaining) but don't believe it is necessary to do so from a constructor.


It is advisable to use const for any value that doesn't need to be re-assigned - this avoid accidental re-assignment later. For example, in getNextChar(), seq and index are never re-assigned so they could be declared with const. And some people may argue that declaring those variables to only be used once or twice right before a return statement uses excess memory, which is perhaps why the suggested code in r10y's answer has them eliminated.

\$\endgroup\$

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.