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

Pluralizing/singularizing strings? #50

Closed
ryanmasondavies opened this issue Apr 10, 2013 · 6 comments
Closed

Pluralizing/singularizing strings? #50

ryanmasondavies opened this issue Apr 10, 2013 · 6 comments

Comments

@ryanmasondavies
Copy link

It seems like a fairly common requirement to have a word that needs to be singular or plural based on some value, e.g:

// would expect, in this case, for the output to be "I have 3 flavors."
NSArray *data = @{@"flavors": @[@"vanilla", @"strawberry", @"chocolate"]};
GRMustacheTemplate *template = [GRMustacheTemplate templateFromString:@"I have {{flavors.count}} flavors" error:NULL];

// would expect, in this case, for the output to be "I have 1 flavor."
NSArray *data = @{@"flavors": @[@"flapjack"]};
GRMustacheTemplate *template = [GRMustacheTemplate templateFromString:@"I have {{flavors.count}} flavor" error:NULL];

Right now the only way I've got this working is to create an 'isOne' filter. I don't like this workaround because it introduces a conditional into the template, which seems like it negates the idea of 'logicless templates':

I have {{flavors.count}} flavor{{^isOne}}s{{/}}.

Another issue here is that not all words are pluralized simply by adding an 's', lengthening the template into having to deal with singular and plural cases individually.

Ideally I'd like to pass the count to a rendered block:

I have {{flavors.count}} {{#pluralize(flavors.count)}}flavor{{/}}.

The issue with this at the moment is that, as far as I can tell, rendered blocks can't accept arguments. Variadic filters can, but they can't accept literals:

I have {{flavors.count}} {{pluralize(flavors.count, "flavor")}}.

Any ideas on how to move ahead with this? Perhaps someone else has run into this scenario?

Also, thanks for the library. Usually I avoid depending on third parties, but your library is so well implemented that I couldn't pass it up. 👍

@groue
Copy link
Owner

groue commented Apr 10, 2013

Hi Ryan!

Thanks for the nice comments!

I'd follow the same idea as the localize standard helper: it localizes the content of the template itself, not an argument: {{#localize}}Hello{{/}} renders Bonjour in French.

This pattern fits well your own template: "flavor" is part of the template content, just as "Hello" above. But instead of rendering "goût", we want to render "flavor" or "flavors" depending on the flavors count.

So we'll render the following template: {{# pluralize(flavors.count) }}flavor{{/}}.

pluralize(flavors.count) will evaluate to a rendering object (aka "lambda" in orthodox Mustache lingo) that renders the inner rendering with the correct spelling, depending on flavors.count. Now we know how to write our pluralize filter:

// Let's first assume a category on NSString that performs the pluralization:

@interface NSString(Pluralize)
- (NSString *)pluralize:(NSUInteger)count;
@end

@implementation NSString(Pluralize)
- (NSString *)pluralize:(NSUInteger)count
{
    // Naive implementation
    if (count < 2) return self;
    return [self stringByAppendingString:@"s"];
}

@end

// Now the actual rendering:

id data = @{
    @"dogCount": @1,
    @"catCount": @2,

    // The pluralize filter...
    @"pluralize": [GRMustacheFilter filterWithBlock:^id(NSNumber *countNumber) {

        // ... extracts the count from its argument...
        NSUInteger count = [countNumber unsignedIntegerValue];

        // ... and actually returns an object that processes the section it renders for:
        return [GRMustache renderingObjectWithBlock:^NSString *(GRMustacheTag *tag, GRMustacheContext *context, BOOL *HTMLSafe, NSError *__autoreleasing *error) {

            // First get the word to pluralize by performing a first "classic" rendering of the section...
            NSString *word = [tag renderContentWithContext:context HTMLSafe:HTMLSafe error:error];

            // ... and finally use our count
            return [word pluralize:count];
        }];
    }],
};

NSString *templateString = @"I have {{dogCount}} {{#pluralize(dogCount)}}dog{{/}} and {{catCount}} {{#pluralize(catCount)}}cat{{/}}.";
NSString *rendering = [GRMustacheTemplate renderObject:data fromString:templateString error:NULL];

The result is I have 1 dog and 2 cats. as expected.

This technique is generally very useful in GRMustache: having filters that return rendering objects.

Note that you can now even pluralize dynamic words: {{# pluralize(n) }}{{ word }}{{/}} would work well also, and render "dogs" or "cats" depending on the value of word.

I hope this approach looks sensible to you!

@groue
Copy link
Owner

groue commented Apr 11, 2013

Better reading your initial message, I realize {{#pluralize(flavors.count)}}flavor{{/}} was exactly your goal. Phew, we have an agreement here :-)

@groue
Copy link
Owner

groue commented Apr 11, 2013

Extending the topic:

Such general purpose tools may want to enter your own library of reusable components, that would extend the standard library.

The way to achieve this is to use GRMustacheConfiguration class:

// Add `pluralize` to the standard GRMustache library,
// once and for all for the whole application:

id myLibrary = @{
    @"pluralize": [GRMustacheFilter filterWithBlock:^id(NSNumber *countNumber) {
        NSUInteger count = [countNumber unsignedIntegerValue];
        return [GRMustache renderingObjectWithBlock:^NSString *(GRMustacheTag *tag, GRMustacheContext *context, BOOL *HTMLSafe, NSError *__autoreleasing *error) {
            NSString *word = [tag renderContentWithContext:context HTMLSafe:HTMLSafe error:error];
            return [word pluralize:count];
        }];
    }],
};
GRMustacheContext *baseContext = [GRMustacheConfiguration defaultConfiguration].baseContext;
baseContext = [baseContext contextByAddingObject:myLibrary];
[GRMustacheConfiguration defaultConfiguration].baseContext = baseContext;

// And now render, assuming `pluralize` is generally available.

id data = @{
    @"dogCount": @1,
    @"catCount": @2,
};

NSString *templateString = @"I have {{dogCount}} {{#pluralize(dogCount)}}dog{{/}} and {{catCount}} {{#pluralize(catCount)}}cat{{/}}.";
NSString *rendering = [GRMustacheTemplate renderObject:data fromString:templateString error:NULL];

@ryanmasondavies
Copy link
Author

Thanks very much for the quick and extensive response.

Yes, {{#pluralize(flavors.count)}}flavor{{/}} was exactly what I was looking for. I didn't think to combine both a filter and rendering object. Since you've given a wonderfully thorough explanation, perhaps it's worth putting this example in a guide? 😃

I've implemented it in combination with @mattt's InflectorKit, and it works great:

@"pluralize": [GRMustacheFilter filterWithBlock:^id(NSNumber *count) {
    return [GRMustache renderingObjectWithBlock:^NSString *(GRMustacheTag *tag, GRMustacheContext *context, BOOL *HTMLSafe, NSError *__autoreleasing *error) {
        NSString *word = [tag renderContentWithContext:context HTMLSafe:HTMLSafe error:error];
        return [count isEqualToNumber:@1] ? word : [word pluralizedString];
    }];
}]

Thanks for the info about extending the standard library. That'll come in handy down the line. 👍

@groue
Copy link
Owner

groue commented Apr 11, 2013

It's not easy to find a good enough title for this kind of guide. Why would anybody click a "combining filters and rendering objects" without any clue on what he can achieve with such a combination? Anyway, I may use your use case in the FAQ : "Can I pluralize/singularize words? Yesir!" -> boom link to our sample code :-)

I'm quite honored that GRMustache is member of the happy few third-party libraries you dare integrate, along with the amazingly useful ones of @mattt :-)

Happy Mustache, Ryan!

@groue groue closed this as completed Apr 11, 2013
@ryanmasondavies
Copy link
Author

That sounds like a better idea. Thanks again! 👨

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

No branches or pull requests

2 participants