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

feat(collections): add joinToString #1223

merged 7 commits into from
Oct 17, 2021

Conversation

majidsajadi
Copy link
Contributor

No description provided.

Copy link
Contributor

@LionC LionC left a comment

Choose a reason for hiding this comment

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

Thanks for implementing this! I left some minor comments.

collections/join_to_string.ts Outdated Show resolved Hide resolved
collections/join_to_string.ts Outdated Show resolved Hide resolved
collections/join_to_string.ts Outdated Show resolved Hide resolved
collections/join_to_string.ts Outdated Show resolved Hide resolved
collections/join_to_string.ts Outdated Show resolved Hide resolved
collections/join_to_string_test.ts Show resolved Hide resolved
@LionC LionC mentioned this pull request Sep 12, 2021
44 tasks
@majidsajadi
Copy link
Contributor Author

@LionC updated. let me know if anything else you have in mind.

* Example:
*
* ```ts
* import { joinToString } from "./join_to_string.ts";
Copy link
Contributor

Choose a reason for hiding this comment

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

See #1227 , I think we should use the same import as in the README 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.

oh, nice. that was something that bothered me a lot. I didn't know somebody fixed this in another PR.

@kt3k
Copy link
Member

kt3k commented Sep 13, 2021

@majidsajadi @LionC How about adding limit and truncated options as well from kotlin's version? https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/join-to-string.html

The current proposed API looks a little too trivial to me to be included in std..

@majidsajadi
Copy link
Contributor Author

majidsajadi commented Sep 13, 2021

@majidsajadi @LionC How about adding limit and truncated options as well from kotlin's version? https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/join-to-string.html

The current proposed API looks a little too trivial to me to be included in std..

I guess limit and truncated options are something that would be very useful. the current implemented version can be replaced by a one-liner. array.map(selector).join(separator). adding more options makes the module valuable.

P.S. how about transform? that's something I guess we can use a lot?

@kt3k
Copy link
Member

kt3k commented Sep 13, 2021

@majidsajadi

P.S. how about transform? that's something I guess we can use a lot?

I guess transform is already mapped to selector in our case

@LionC
Copy link
Contributor

LionC commented Sep 13, 2021

I am fine with adding limit and truncate. I think in TS, having a subobject might make more sense for safer typing:

type JoinToStringOptions = {
  separator?: string;
  prefix?: string;
  suffix?: string;
  truncate?: {
    after: number;
    with?: string;
  };
};

That will ensure that if you truncate, you need to set a limit and the optional truncate suffix thing (in kotlin you do not really have objet literals, you need to call constructors, so they tend to flatten parameters).

We could also think abot adding a lastSeparator string option that sets a different separator for the very last join, wich allows for the classic A, B, C and D thing for e.g. log messages.

I want to note though @kt3k, that the current version should already be faster than .map().join(), which is a good reason in itself.

@majidsajadi
Copy link
Contributor Author

type JoinToStringOptions = {
  separator?: string;
  prefix?: string;
  suffix?: string;
  truncate?: {
    after: number;
    with?: string;
  };
};

we settle on this? should i update the MR? @kt3k @LionC

@kt3k
Copy link
Member

kt3k commented Sep 13, 2021

we settle on this? should i update the MR? @kt3k @LionC

That design looks good to me. Let's settle on it. It'd be helpful if you could update it

array: readonly T[],
selector: (el: T) => string,
{
separator = ", ",
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 space separator is more appropriate

Suggested change
separator = ", ",
separator = " ",

Copy link
Contributor

Choose a reason for hiding this comment

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

Array.join and Kotlin's joinToString both have the comma in their default, why should we deviate?

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
separator = ", ",
separator = ",",

Agree with @LionC here, I think it's better if it match Array.prototype.join with a single comma without space.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@zhangyuannie @LionC Array.prototype.join default separator is "," but Kotlin's joinToString default separator is ", ".

I copied my default parameters from Kotlin's joinToString but I guess it's better to go with Array.prototype.join in this case. more familiar API for JS developers.

Copy link
Contributor

@zhangyuannie zhangyuannie left a comment

Choose a reason for hiding this comment

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

Personally, I'm against this API as it stands right now since

array.map(selector).join(separator)

is more idiomatic and faster.

I agree with @kt3k that we should include limit and truncated for this to be included in std.

I want to note though @kt3k, that the current version should already be faster than .map().join(), which is a good reason in itself.

@LionC Do you have any source for your claim?
With an array of length 500 with { num: Math.random() * 100 } as element and obj => "" + Math.floor(obj.num) as selector, current implementation takes 500ms while map-join takes 150ms.

Comment on lines 50 to 62
let ret = prefix;

array.forEach((it, index) => {
if (index > 0) {
ret += separator;
}

ret += selector(it);
});

ret += suffix;

return ret;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
let ret = prefix;
array.forEach((it, index) => {
if (index > 0) {
ret += separator;
}
ret += selector(it);
});
ret += suffix;
return ret;
return `${prefix ?? ""}${array.map(selector).join(separator)}${suffix ?? ""}}`
  1. User might pass null for prefix and suffix, I don't think they want null literally in the string.
  2. While string concatenation is heavily optimised in v8, I think map is more idiomatic and faster. When we introduce limit and truncated, we should still use an intermediate array instead of doing string concatenation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

User might pass null for prefix and suffix, I don't think they want null literally in the string.

i guess passing null for prefix or suffix throw Type 'null' is not assignable to type 'string'. error.

Copy link
Contributor

Choose a reason for hiding this comment

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

Only if you use TypeScript with type checking enabled 😉. With the core team's push on removing type check by default for TypeScript, I think it might be worthy to start adding these trivial type guards. Feel free to discard that suggestion through, I don't have too strong of an opinion on this.

Copy link
Contributor

Choose a reason for hiding this comment

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

I am very strongly against introducing special code to protect JS consumers from passing the wrong types - that is what TS is for.

array: readonly T[],
selector: (el: T) => string,
{
separator = ", ",
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
separator = ", ",
separator = ",",

Agree with @LionC here, I think it's better if it match Array.prototype.join with a single comma without space.

@majidsajadi
Copy link
Contributor Author

@kt3k review, please.

Comment on lines +10 to +11
limit?: number;
truncated?: string;
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.

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

let result = "";

let index = -1;
while (++index < array.length) {
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

Comment on lines +72 to +74
result = prefix + result + suffix;

return result;
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}`;

@CLAassistant
Copy link

CLAassistant commented Oct 16, 2021

CLA assistant check
All committers have signed the CLA.

Copy link
Member

@kt3k kt3k left a comment

Choose a reason for hiding this comment

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

Thank you @majidsajadi for your contribution!

LGTM

@kt3k
Copy link
Member

kt3k commented Oct 16, 2021

@majidsajadi Sorry we changed the CLA a little bit (changed the contact email address), and we need you to sign it again..

@majidsajadi
Copy link
Contributor Author

@kt3k Done.

@kt3k
Copy link
Member

kt3k commented Oct 17, 2021

Thanks!

@kt3k kt3k merged commit 2ce4b7e into denoland:main Oct 17, 2021
traceypooh pushed a commit to traceypooh/deno_std that referenced this pull request Nov 14, 2021
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.

None yet

6 participants