Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

list-ops: add to track #207

Merged
merged 13 commits into from
Feb 21, 2017
Merged

list-ops: add to track #207

merged 13 commits into from
Feb 21, 2017

Conversation

javaeeeee
Copy link
Contributor

dibs: I will implement list-ops

@jtigger
Copy link
Contributor

jtigger commented Dec 24, 2016

Poke!

@javaeeeee
Copy link
Contributor Author

javaeeeee commented Dec 25, 2016 via email

@javaeeeee
Copy link
Contributor Author

javaeeeee commented Dec 25, 2016 via email

@jtigger
Copy link
Contributor

jtigger commented Dec 27, 2016

Merry Christmas, @javaeeeee! 🎄 ...and Happy (Belated) Boxing Day!

Estimated arrival of this code is first Days of January. :)

Sounds lovely, sir. Happy New Year! 🍾 🎆

@javaeeeee
Copy link
Contributor Author

Hello! Thanks for congratulations, @jtigger! Sorry for the delay. This implementation is inspired by the one in the C# track. Added some tests. The example implementation is the simplest possible.

@jtigger jtigger changed the title dibs: I will implement list-ops list-ops: add to track Jan 10, 2017
@jtigger jtigger self-assigned this Jan 10, 2017
Copy link
Contributor

@jtigger jtigger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great stuff, Dmitry!

Bunch of comments and questions inline.

Generics

Clearly this exercise is going to appear late in the game. We're still hashing out how we'll introduce generics (please acquaint and chime in on #230).

Seems like we should either not use type variables or include test cases (or modify existing test cases) that exercise those variables.

append()

I'm really struggling with this operation:

  • it gets away from the focus on immutability which I'm really loving;
  • it's redundant in functionality with concat() just with a different interface. It's not just "bad" API design, it means a chunk of the exercise feels a bit like busy work.


@Test
@Ignore
public void fileteredEmptyListShouldBeAnEmptyList() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: s/filetered/filtered/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected and renamed. Thank you. 🥇

= Collections.emptyList();

@Test
public void lengthOfAnEmptyListShouldBeZero() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here (and in mapOfAnEmptyListShouldBeAnEmptyList(), fileteredEmptyListShouldBeAnEmptyList()), the subject becomes a property of the list whereas in the other tests, the subject is the list itself.

🤔
Genuine question: is it "better" to have consistent test method naming? or to have a variety to break up the monotony that comes with consistency??! :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed. Just failed to come up with consistent names on the first pass. :)

@Test
@Ignore
public void shouldReturnCorrectLengthOfAnNonEmptyList() {
final int expected = 4;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔
Thinking out loud: I find that the greater the space between definition and use, the "harder" it is to read. What do you think of moving L34 to L41?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved. Just stuck to habitual code conventions.

public void shouldReverseAnEmptyList() {
final List<Integer> actual = ListOps.reverse(EMPTY_LIST);

assertNotNull(actual);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍
I've said it before, but I really appreciate how little bits like this smooth-out the TDD experience: the more we keep folks in test failures (rather than test errors), the smoother the experience.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. :)

.collect(Collectors.toList());
Collections.reverse(reversedList);
final List<Integer> expected
= Collections.unmodifiableList(reversedList);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exercise is clearly going to be placed late in the track, so addressing 2nd order topics like leaning toward immutability seems appropriate. But let's talk about it.

😃
It's particularly important in this exercise to hand in immutable input into ListOps! Love that this is your go-to.

🤔
How can we make the case for making the expected value immutable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems that it's unnecessary as we don't pass it anywhere. That's an example where code reviews really shine. 👍


public static <T> List<T> map(final List<T> list,
UnaryOperator<T> mapper) {
return list.stream().map(mapper).collect(Collectors.toList());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hahaha!! Cheater! 😆

👍
I joke, but actually, relying on the JSL for this functionality gives me great confidence that the tests are solid. Not sure if that was your intention, but great move!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried to keep it simple. The example is needed only to test the test. No pun intended. :)

return list.stream().filter(predicate).collect(Collectors.toList());
}

public static <U, T> U reduce(final List<T> list,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍
I love how this mirrors exactly the interface for streams... what a great way to introduce this part of the API!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. It makes sense to expose Java API here like the C# one was used in the C# track

= Stream.concat(list1.stream(), list2.stream())
.collect(Collectors.toList());
Collections.sort(sortedCancatenatedList);
final List<Integer> expected = Collections.unmodifiableList(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔
It can significantly increase the readability of a test when the expectation is as close to "literal" as possible. Otherwise, the reader must apply the operations in their head and visualize the result.

For example:

final List<Integer> expected = Collections.unmodifiableList(
  IntStream.range(0, 4, 1, 6).boxed().collect(Collectors.toList())
);

There's another example of this in reduceShouldConcatenateTwoNonEmptyLists(), below 👇 .

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems that it also can be modifiable here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise, didn't touch the code. Lists to concatenate are used both to create the result and to test the concat() method. Could you take a look again here and elaborate more, please.

public void shouldReturnIdentityWhenAnEmptyListIsReduced() {
final int expected = 0;
final int actual
= ListOps.reduce(EMPTY_LIST, 0, (x, y) -> x + y, Integer::sum);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍
Nice touch! I love how you're seeking in little bits of the Java FP stuff in! (Here, both the method reference and that there's a set of functions ready for use!)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. :)

final List<Integer> sortedCancatenatedList
= Stream.concat(list1.stream(), list2.stream())
.collect(Collectors.toList());
Collections.sort(sortedCancatenatedList);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔
Concatenation is about connecting a set of things, in series. Should we be sorting?

Copy link
Contributor Author

@javaeeeee javaeeeee Jan 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the second pass, it looks like it could be even harmful. :) Thank you.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both sorts were unnecessary, so removing them sped up the test. Overengineered for lists containing same elements.

@javaeeeee
Copy link
Contributor Author

javaeeeee commented Jan 11, 2017 via email

@javaeeeee
Copy link
Contributor Author

javaeeeee commented Jan 12, 2017

For all tests removed unnecessary unmodifiable for expected lists and placed expected results closer to assertions.

As to the append() method, it's not difficult to remove it from the exercise. An idea why it could stay here is that while it can be bad for production code for the reasons you mentioned, but here, in an exercise, it could prod a learner to write some additional lines of code. Anyway, your decision is needed here about what to do with this code.

@javaeeeee
Copy link
Contributor Author

javaeeeee commented Jan 12, 2017

As to the topic of Generics seems that we should use it because it's an important language feature. As to the tests, the C# track has tests for various data types only for the reduce operation. The example the Java track uses the most complex form of the reduce() method because it can reduce the list of Integers into an Integer (sum) and into a List of Integers (concatenation) and there are tests for that. Also, the C# has the tests for reduce for Strings, that were not added to the Java version to speed up the release of the exercise.

It's not difficult to add tests using various data types and that could explain to a learner why Generics can simplify the implementation.

@jtigger
Copy link
Contributor

jtigger commented Jan 16, 2017

@javaeeeee said:

As to the append() method, it's not difficult to remove it from the exercise. An idea why it could stay here is that while it can be bad for production code for the reasons you mentioned, but here, in an exercise, it could prod a learner to write some additional lines of code. Anyway, your decision is needed here about what to do with this code.

Well, I've convinced myself that my idea is great! 😆

No, seriously, if you're actually on the fence, then my vote is to remove append(). However, if you see additional benefit to the practitioner, I'm super open to that.

I'm evaluating the additional lines of code that append() would provoke and asking, "is there an additional learning experience stimulated by this requirement?" The learnings I'm hoping for are: a language feature, interesting design quandaries, showing-off the benefit of well-factored code... I'm sure there's other categories. Do you see added value here?

@jtigger
Copy link
Contributor

jtigger commented Jan 16, 2017

Hey @javaeeeee I meant to get to your PR, this morning. The work day is getting started, so I'm gonna have to kick it to tomorrow. Sorry for the delay.

Copy link
Contributor

@jtigger jtigger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, sorry for the delay, Dmitry.

We're definitely making progress!

I seem to be really harping on making test code as simple as possible. In particular, setting up expected values as literally as possible.

I'm painfully aware that my feedback is rather pedantic and particularly opinionated. I hope that I'm doing a good job of motivating these changes.

At the heart of it a desire to create a delightful experience for our users. Not unlike in literature, refinement is often about seeking concise ways of expressing an idea.

It's a kind of empathy to put ourselves in the shoes of someone learning Java (and/or seeing these language features for the first time) and asking ourselves how much effort it would take to understand the problem/question/test.

Empathy skillfully conveyed lands as respect. It's that we took the time to minimize how much translation/interpretation we ask of our users to understand the question/problem we're posing. They may not consciously know it, but they'll feel the difference.

@@ -53,23 +53,22 @@ public void shouldReverseAnEmptyList() {

@Test
@Ignore
public void shouldReverseNonEmptyList() {
public void shouldReverseANonEmptyList() {
final int length = 100;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 extracting a value to a variable is a great move where the input and output need to agree.

🤔 this larger number forces us to use IntStream.range() to generate the list (and applying an operation on top of it; here sorting). If we start with a smaller value, we could start with the transformed value.

Example:

final List<Integer> list = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8);
final List<Integer> reversed = Arrays.asList(8, 7, 6, 5, 4, 3, 2, 1, 0);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated code.

final List<Integer> list = Collections.unmodifiableList(
IntStream.range(startValue, length)
.boxed()
.collect(Collectors.toList())
);
final List<Integer> actual
= ListOps.reverse(list);
final List<Integer> expected = reversedList;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 what do you think of using reversedList as the expected value directly in the assert? I'm thinking this could increase the readability of the assert itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed reversedList altogether.

final List<Integer> list
= Collections.unmodifiableList(Arrays.asList(1, 3, 5, 7));
final List<Integer> actual = ListOps.map(list, x -> x + 1);
final List<Integer> expected
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 (redundant suggestion) this "literal" (as close as we get in Java) is short enough that it could fit inline in the assert, below. The motivation, here, is to bring the declaration of the value as close to its use as possible (in this case, reducing that distance to zero).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated code.

@@ -111,12 +110,11 @@ public void fileteredEmptyListShouldBeAnEmptyList() {
@Test
@Ignore
public void shouldFilterNonEmptyList() {
final List<Integer> expected
= Collections.unmodifiableList(Arrays.asList(1, 3));
final List<Integer> list = Collections.unmodifiableList(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 I think it is really important that we take the opportunity to show-off the Java Streams API as much as we can, here; using the IntStream.range() idiom being one of those. But in this specific case, as written, it feels a little awkward to use Arrays.asList() for a two-element list and then upshift to IntStream to create a four-element list.

💡 you could create a large input list and use a highly-selective function in filter() that yields a short list. For example, a list from [1, 21) where values are evenly divisible by 5?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created a longer list and introduced a filter. Extracted predicate for reuse.

@@ -343,9 +325,8 @@ public void shouldConcatenateSeveralLists() {
sortedCancatenatedList.addAll(list3);
sortedCancatenatedList.addAll(list4);
Collections.sort(sortedCancatenatedList);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ I'm confused as to what's motivating the sort. I was expecting to see a few chunks that are, in series, appended. In fact, I'm expecting that the order is preserved. Makes me feel like I'm missing a subtle point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I see that it was unnecessary.

@javaeeeee
Copy link
Contributor Author

javaeeeee commented Jan 18, 2017 via email

@javaeeeee
Copy link
Contributor Author

javaeeeee commented Jan 18, 2017 via email

@javaeeeee
Copy link
Contributor Author

javaeeeee commented Jan 18, 2017 via email

@javaeeeee
Copy link
Contributor Author

javaeeeee commented Jan 18, 2017 via email

@javaeeeee
Copy link
Contributor Author

Thank you, @jtigger, for thorough review and advice. 👍

Copy link
Contributor

@jtigger jtigger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another round of comments. I'm calling out each instance of short lists being generated with IntStream.

There's also a request for another maintainers to chime in on the question about using the more "complete" version of reduce().

@Ignore
public void shouldReturnTheCorrectLengthOfAnNonEmptyList() {
final List<Integer> list = Collections.unmodifiableList(
IntStream.range(0, 4)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 use Arrays.asList(...) for short lists.

public void shouldReverseANonEmptyList() {
final List<Integer> list = Collections.unmodifiableList(
Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8)
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

public void shouldConcatenateOneNonEmptyList() {
final List<Integer> list
= Collections.unmodifiableList(
IntStream.range(0, 4)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 use Arrays.asList(...) for short lists.

);
final List<Integer> actual = ListOps.concat(list);
final List<Integer> expected
= IntStream.range(0, 4)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 use Arrays.asList(...) for short lists.

public void shouldConcatenateOneEmptyAndOneNonEmptyLists() {
final List<Integer> list
= Collections.unmodifiableList(
IntStream.range(0, 4)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 use Arrays.asList(...) for short lists.

accumulator,
combiner);
final List<Integer> expected
= IntStream.range(0, 5)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 use Arrays.asList(...) for short lists.

@Ignore
public void shouldReduceTwoNonEmptyListsAndReturnConcatenation() {
final List<Integer> listOne = Collections.unmodifiableList(
IntStream.range(0, 5)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 use Arrays.asList(...) for short lists.

.collect(Collectors.toList())
);
final List<Integer> listTwo = Collections.unmodifiableList(
IntStream.range(5, 10)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 use Arrays.asList(...) for short lists.

accumulator,
combiner);
final List<Integer> expected
= IntStream.range(0, 10)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 use Arrays.asList(...) for short lists.


@Test
@Ignore
public void shouldReduceWithAnticommutativeAccumulator() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 wow... okay, I'm conflicted.

On the one hand, I love the notion of really taking it deep in this exercise. This test does that — it's an invitation to really dig into the Java Streams API.

On the other hand, it's really deep for someone just getting started. This might mean we're throwing a bone to those who are already familiar with the API and haven't dug deeply into this more complete form of reduce. Most of what I see in the wild sticks with collect(), but that's missing out. It might also be too big of a jump in this one exercise...

👥 @exercism/java: can I get another opinion on this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jtigger Hello, John, added some fun with collect(). :)

@javaeeeee
Copy link
Contributor Author

javaeeeee commented Jan 24, 2017 via email

@jtigger
Copy link
Contributor

jtigger commented Jan 25, 2017

"what if we made it feasible to express expected literally? For example, if
we made each listN shorter, the concatenated list would fit on one line."

Do you mean that we shouldn't compute expected, but just provide the range
for it or an explicit result in some other form?

I try to be really careful with the auxiliary verb "should" because I believe that decisions are contextual. If we take that word as a shorthand for "what seems appropriate in this case", then we're talking the same language. :)

I do think we "should" aim to craft examples that take a little thought-power as possible to read. If the target outcome is literal, that's often the easiest to read. Otherwise, I'm asking the reader to envision the expectation, rather than just see it.


"On the other hand, it's really deep for someone just getting started."

Thought about levels of difficulty including previous discussions about Generics. We could provide two test files, one for those 'more comfortable' and the one for 'less comfortable' as done in cs50x. So, we'll offer an opportunity for beginners to start with simple things and an opportunity for those who are preparing for an interview to master some advanced stuff. The exercism site allows one to upload several solutions.

Yeah, man! @matthewmorgan and I have had a number of conversations along these lines. I could have sworn we created an issue in discussion or x-common around this, but I can't seem to find it.

At the time, the idea would be that we specifically add "bonus" requirements sections in the common description; some would simply add to the depth of the exercise, some would often cause the practitioner to likely seriously restructure their solution.

But this seems like a track-specific version... hadn't occurred to me, before that we could just add this to the HINT.md file and include the separate test class.

I'd be totally down for that.

Copy link
Contributor

@jtigger jtigger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point, this PR has been very long running; I'm eager to land this plane.

Highlights:

  • The diff reveals a number of places where there are trailing spaces. Please trim these.
  • There still remain (at least one) place(s) where actuals are calculated where they can be expressed literally. Please don't wait for me to point out each instance, but proactively comb for such instances and address them. Thanks!
  • I question the wisdom of increasing the scope of this PR. I'm starting to experience reviewer fatigue, personally.

Details in individual comments.

Thanks for your efforts!

);
final List<Integer> list2 = Collections.unmodifiableList(
IntStream.range(1, 6).boxed().collect(Collectors.toList())
Arrays.asList(1, 2, 3, 4, 5, 6)
);
final List<Integer> expected
= Stream.concat(list1.stream(), list2.stream())
.collect(Collectors.toList());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 expected can be feasibly expressed literally, here.

assertEquals(10, actual);
}

@Test
@Ignore
public void shouldReduceWithAnticommutativeAccumulator() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 coming back to this topic with a "fresh" mind... mentioning "parallelization" without a requirement to support parallelization in the exercise is a likely source of confusion. That's a problem.

I see two paths:

  1. add the requirement to be able to parallelize stream operations.
  2. remove the notion of parallelization from this exercise (and attendant APIs that are required by it).

We've packed so much into this exercise, as it is, I'm personally leaning toward option 2.

@javaeeeee, do you see another option? What can we do to avoid this confusion?


@Test
@Ignore
public void shouldReturnZeroWhenAnEmptyListIsCollected() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😱 OH GEEEZ! More scope?!?! :)

This is already the longest running PR I've seen on this track. I strongly prefer we first land this thing before adding any more to the exercise.

It opens a can of worms as to whether to include both reduce and collect; the two are very similar. And it duplicates the concern of invoking the notion of parallelizable processing of stream operations.

Would you mind stashing this work somewhere? I suspect we'll break this thing up into chunks and it won't seem so overwhelming... later.

@jtigger
Copy link
Contributor

jtigger commented Feb 10, 2017

Hey, @javaeeeee, we doing okay, here? I'm not trying to rush you, just want to make sure we're still together in this.

@javaeeeee
Copy link
Contributor Author

javaeeeee commented Feb 10, 2017 via email

@jtigger
Copy link
Contributor

jtigger commented Feb 10, 2017

No rush, @javaeeeee. I had a lot of constructive criticism in my last round of review. I wanted to make sure I didn't hit a nerve. :)

Please take your time, Dimitry. We're not in a rush here. This should be the fun thing you do in your spare time — not another source of "work."

@javaeeeee
Copy link
Contributor Author

javaeeeee commented Feb 10, 2017 via email

@javaeeeee
Copy link
Contributor Author

javaeeeee commented Feb 13, 2017

@jtigger Hello, John,

Sorry once again for the delay.

  • Removed trailing spaces.

  • Removed collect().

  • Removed part of the exercise that relied on parallelization.

@stkent
Copy link
Contributor

stkent commented Feb 18, 2017

Merged with master to resolve conflicts in config.json.

@jtigger
Copy link
Contributor

jtigger commented Feb 21, 2017

By the power invested in me, Dmitry, I do declare your PR as one fantastic piece of work.

Thank you for your patience and stick-to-it-ness. :) This exercise is a great add to the track.

@jtigger jtigger merged commit 41fe578 into exercism:master Feb 21, 2017
@javaeeeee
Copy link
Contributor Author

javaeeeee commented Feb 21, 2017 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants