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
- We give a sequence of characters. For example: "abc"
- We give a starting string. For example: "a"
- 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
2 Answers 2
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.
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.
Explore related questions
See similar questions with these tags.