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

feat(collections): add joinToString #1223

Merged
merged 7 commits into from
Oct 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions collections/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,36 @@ const commonInterests = intersect(lisaInterests, kimInterests);
assertEquals(commonInterests, ["Cooking", "Music"]);
```

### joinToString

Transforms the elements in the given array to strings using the given selector.
Joins the produced strings into one using the given `separator` and applying the
given `prefix` and `suffix` to the whole string afterwards. If the array could
be huge, you can specify a non-negative value of `limit`, in which case only the
first `limit` elements will be appended, followed by the `truncated` string.
Returns the resulting string.

```ts
import { joinToString } from "https://deno.land/std@$STD_VERSION/collections/mod.ts";
import { assertEquals } from "https://deno.land/std@$STD_VERSION/testing/asserts.ts";

const users = [
{ name: "Kim" },
{ name: "Anna" },
{ name: "Tim" },
];

const message = joinToString(users, (it) => it.name, {
suffix: " are winners",
prefix: "result: ",
separator: " and ",
limit: 1,
truncated: "others",
});

assertEquals(message, "result: Kim and others are winners");
```

### mapEntries

Applies the given transformer to all entries in the given record and returns a
Expand Down
75 changes: 75 additions & 0 deletions collections/join_to_string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.

/**
* Options for joinToString
*/
export type JoinToStringOptions = {
separator?: string;
prefix?: string;
suffix?: string;
limit?: number;
truncated?: string;
Comment on lines +10 to +11
Copy link
Contributor

@LionC LionC Oct 10, 2021

Choose a reason for hiding this comment

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

I am not a fan of that type, as it allows to set truncated without setting limit, which does not make sense. I think this should be

Suggested change
limit?: number;
truncated?: string;
truncate?: {
limit: number;
truncation?: string;
};

or something similar to leverage the compiler catching wrong usage

Copy link
Contributor Author

@majidsajadi majidsajadi Oct 16, 2021

Choose a reason for hiding this comment

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

if we use your suggested type:

export function joinToString<T>(
  array: readonly T[],
  selector: (el: T) => string,
  {
    separator = ",",
    prefix = "",
    suffix = "",
    truncate = {
      truncated: "...",
      limit: -1,
    }
  }: Readonly<JoinToStringOptions>,
)
 {
      truncated: "...",
      limit: -1,
    }

this is the default value for the truncate option. what if the user overrides this with only limit since the truncated suppose to be optional. the truncated would be undefined.

joinToString(someArray, { 
truncate: {
limit: 3
}
});

I know I can handle the default values in the function body but I guess the code would be a little bit messy and I don't know if we want to go that way. the implemented type is just like the Kotlin version of this method so I guess its some what okay.

@LionC

Copy link
Member

Choose a reason for hiding this comment

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

I agree with @majidsajadi . Let's keep the implementation simple. I think it's mostly obvious that truncated is only meaningful with limit set in this particular case.

};

/**
* Transforms the elements in the given array to strings using the given selector.
* Joins the produced strings into one using the given `separator` and applying the given `prefix` and `suffix` to the whole string afterwards.
* If the array could be huge, you can specify a non-negative value of `limit`, in which case only the first `limit` elements will be appended, followed by the `truncated` string.
* Returns the resulting string.
*
* Example:
*
* ```ts
* import { joinToString } from "https://deno.land/std@$STD_VERSION/collections/mod.ts";
* import { assertEquals } from "https://deno.land/std@$STD_VERSION/testing/asserts.ts";
*
* const users = [
* { name: "Kim" },
* { name: "Anna" },
* { name: "Tim" },
* ];
*
* const message = joinToString(users, (it) => it.name, {
* suffix: " are winners",
* prefix: "result: ",
* separator: " and ",
* limit: 1,
* truncated: "others",
* });
*
* assertEquals(message, "result: Kim and others are winners");
* ```
*/
export function joinToString<T>(
array: readonly T[],
selector: (el: T) => string,
{
separator = ",",
prefix = "",
suffix = "",
limit = -1,
truncated = "...",
}: Readonly<JoinToStringOptions> = {},
): string {
let result = "";

let index = -1;
while (++index < array.length) {
Copy link
Contributor

@LionC LionC Oct 10, 2021

Choose a reason for hiding this comment

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

Nitpick: I am personally not the biggest fan of implicit mutation with increment operators, as it gets missed quite easily when reading the code, hiding the core step of the loop. I always find it a lot cleaner to just have a separate

index += 1

line

Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this not a for loop? Or a do .. while loop? The index initialisation with -1 looks odd to me.

Copy link
Contributor Author

@majidsajadi majidsajadi Oct 16, 2021

Choose a reason for hiding this comment

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

i need to access the index of the element. i cant use forEach since I need to have the option to break the loop.
i know we can use array.entries but I thought it would affect the performance (maybe? I'm not sure.)

I saw this pattern (using while for loop through the arrays) in lodash a lot. so I used that here too. but I'm open to any suggestion that makes the code more clear.

@LionC

const el = array[index];

if (index > 0) {
result += separator;
}

if (limit > -1 && index >= limit) {
result += truncated;
break;
}

result += selector(el);
}

result = prefix + result + suffix;

return result;
Comment on lines +72 to +74
Copy link
Contributor

@LionC LionC Oct 10, 2021

Choose a reason for hiding this comment

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

Nitpick: Could be shortened to directly return:

Suggested change
result = prefix + result + suffix;
return result;
return `${prefix}${result}${suffix}`;

}
146 changes: 146 additions & 0 deletions collections/join_to_string_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.

import { assertEquals } from "../testing/asserts.ts";
import { joinToString } from "./join_to_string.ts";

Deno.test({
name: "[collections/joinToString] no mutation",
fn() {
const arr = [
{ name: "Kim", age: 22 },
{ name: "Anna", age: 31 },
{ name: "Tim", age: 58 },
];
joinToString(arr, (it) => it.name);

assertEquals(arr, [
{ name: "Kim", age: 22 },
{ name: "Anna", age: 31 },
{ name: "Tim", age: 58 },
]);
},
});

Deno.test({
name: "[collections/joinToString] identity",
fn() {
const arr = ["Kim", "Anna", "Tim"];

const out = joinToString(arr, (it) => it);

assertEquals(out, "Kim,Anna,Tim");
},
});

Deno.test({
name: "[collections/joinToString] normal mapppers",
fn() {
const arr = [
{ name: "Kim", age: 22 },
{ name: "Anna", age: 31 },
{ name: "Tim", age: 58 },
];
const out = joinToString(arr, (it) => it.name);

assertEquals(out, "Kim,Anna,Tim");
},
});

Deno.test({
name: "[collections/joinToString] separator",
fn() {
const arr = [
{ name: "Kim", age: 22 },
{ name: "Anna", age: 31 },
{ name: "Tim", age: 58 },
];
const out = joinToString(arr, (it) => it.name, { separator: " and " });

assertEquals(out, "Kim and Anna and Tim");
},
});

Deno.test({
name: "[collections/joinToString] prefix",
fn() {
const arr = [
{ name: "Kim", age: 22 },
{ name: "Anna", age: 31 },
{ name: "Tim", age: 58 },
];
const out = joinToString(arr, (it) => it.name, {
prefix: "winners are: ",
});

assertEquals(out, "winners are: Kim,Anna,Tim");
},
});

Deno.test({
name: "[collections/joinToString] suffix",
fn() {
const arr = [
{ name: "Kim", age: 22 },
{ name: "Anna", age: 31 },
{ name: "Tim", age: 58 },
];
const out = joinToString(arr, (it) => it.name, {
suffix: " are winners",
});

assertEquals(out, "Kim,Anna,Tim are winners");
},
});

Deno.test({
name: "[collections/joinToString] limit",
fn() {
const arr = [
{ name: "Kim", age: 22 },
{ name: "Anna", age: 31 },
{ name: "Tim", age: 58 },
];
const out = joinToString(arr, (it) => it.name, {
limit: 2,
});

assertEquals(out, "Kim,Anna,...");
},
});

Deno.test({
name: "[collections/joinToString] truncated",
fn() {
const arr = [
{ name: "Kim", age: 22 },
{ name: "Anna", age: 31 },
{ name: "Tim", age: 58 },
];
const out = joinToString(arr, (it) => it.name, {
limit: 2,
truncated: "...!",
});

assertEquals(out, "Kim,Anna,...!");
},
});

Deno.test({
name: "[collections/joinToString] all options",
fn() {
const arr = [
{ name: "Kim", age: 22 },
{ name: "Anna", age: 31 },
{ name: "Tim", age: 58 },
];
const out = joinToString(arr, (it) => it.name, {
suffix: " are winners",
prefix: "result: ",
separator: " and ",
limit: 1,
truncated: "others",
});

assertEquals(out, "result: Kim and others are winners");
},
});
majidsajadi marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions collections/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export * from "./union.ts";
export * from "./without_all.ts";
export * from "./unzip.ts";
export * from "./zip.ts";
export * from "./join_to_string.ts";
export * from "./max_with.ts";
export * from "./min_with.ts";
export * from "./includes_value.ts";
Expand Down