Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.

Add Path.Join() methods. #16561

Merged
merged 6 commits into from
Feb 28, 2018
Merged

Add Path.Join() methods. #16561

merged 6 commits into from
Feb 28, 2018

Conversation

JeremyKuhne
Copy link
Member

@JeremyKuhne JeremyKuhne commented Feb 25, 2018

@JeremyKuhne JeremyKuhne added * NO MERGE * The PR is not ready for merge yet (see discussion for detailed reasons) area-System.IO labels Feb 25, 2018
@JeremyKuhne JeremyKuhne added this to the 2.1.0 milestone Feb 25, 2018
@JeremyKuhne JeremyKuhne self-assigned this Feb 25, 2018
@@ -10,8 +10,7 @@ internal static partial class PathInternal
/// <summary>
/// Returns true if the path ends in a directory separator.
/// </summary>
internal static bool EndsInDirectorySeparator(string path) =>
!string.IsNullOrEmpty(path) && IsDirectorySeparator(path[path.Length - 1]);
internal static bool EndsInDirectorySeparator(ReadOnlySpan<char> path) => IsDirectorySeparator(path[path.Length - 1]);
Copy link
Member

Choose a reason for hiding this comment

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

Does this need to handle path.Length == 0 ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not currently, but I think it is better to have it.

return true;
}

if (path2.Length == 0)
Copy link
Member

Choose a reason for hiding this comment

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

Could you avoid this duplication by acting on temp, which is whichever of path1 and path2 is not empty?

return true;
}

bool needsSeparator = !(PathInternal.EndsInDirectorySeparator(path1) || PathInternal.EndsInDirectorySeparator(path2));
Copy link
Member

Choose a reason for hiding this comment

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

Should this be checking path2 begins with directory separator?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, thanks

Copy link
Member

Choose a reason for hiding this comment

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

Missing test I guess

Copy link
Member Author

Choose a reason for hiding this comment

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

I've got the tests, just trying to juggle multiple changes unsuccessfully.

JeremyKuhne added a commit to JeremyKuhne/corefx that referenced this pull request Feb 26, 2018
public static string Join(ReadOnlySpan<char> path1, ReadOnlySpan<char> path2)
{
if (path1.Length == 0)
return path2.Length == 0 ? string.Empty : new string(path2);
Copy link
Member

Choose a reason for hiding this comment

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

Why do we have the length == 0 check here and not for path2.Length in the second if?

The string ctor already does this check, so we shouldn't need it in either case:
https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/String.cs#L665

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, didn't know the optimization was there.

return JoinInternal(path1, path2, path3);
}

public static bool TryJoin(Span<char> destination, out int charsWritten, ReadOnlySpan<char> path1, ReadOnlySpan<char> path2)
Copy link
Member

Choose a reason for hiding this comment

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

How about something like this to avoid the extra path1/path2.Length == 0 checks?

In the worst case, there are 3 extra checks in the current impl.

        public static bool TryJoin(Span<char> destination, out int charsWritten, ReadOnlySpan<char> path1, ReadOnlySpan<char> path2)
        {
            charsWritten = 0;
            if (path1.IsEmpty)
            {
                if (path2.IsEmpty)
                    return true;
                if (destination.Length < path2.Length)
                    return false;

                path2.CopyTo(destination);
                charsWritten = path2.Length;
                return true;
            }
            else if (path2.IsEmpty)
            {
                if (destination.Length < path1.Length)
                    return false;

                path1.CopyTo(destination);
                charsWritten = path1.Length;
                return true;
            }

            Debug.Assert(!path1.IsEmpty && !path2.IsEmpty);

            int charsNeeded = path1.Length + path2.Length;
            if (!(IsDirectorySeparator(path1[path1.Length - 1]) || IsDirectorySeparator(path2[0])))
            {
                charsNeeded++;

                if (destination.Length < charsNeeded)
                    return false;
                path1.CopyTo(destination);
                destination[path1.Length] = DirectorySeparatorChar;
                path2.CopyTo(destination.Slice(path1.Length + 1));
            }
            else
            {
                if (destination.Length < charsNeeded)
                    return false;
                path1.CopyTo(destination);
                path2.CopyTo(destination.Slice(path1.Length));
            }

            charsWritten = charsNeeded;
            return true;
        }

Copy link
Member Author

Choose a reason for hiding this comment

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

Because it is a little more bulky & complicated. I can't imagine optimizing checking length being worth it.

Copy link
Member

Choose a reason for hiding this comment

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

Good point. What about the needsSeparator branching? Can we just have a single if/else (which results in some code duplication).

Copy link
Member

Choose a reason for hiding this comment

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

It does seem to make a big enough difference:
image

How large do we expect the path length to be on average? The smaller the path length, the bigger the difference.

Copy link
Member Author

Choose a reason for hiding this comment

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

There will be all sorts. The first path will typically be longer than the second. I don't want to add the complexity here unless I can see a real impact in a higher level scenario. Directory.GetFiles() over a large set, for example. I'll be switching to use these methods (as opposed to our current helpers) as soon as we drop an updated CLR and we can revisit.

@@ -10,8 +10,12 @@ internal static partial class PathInternal
/// <summary>
/// Returns true if the path ends in a directory separator.
/// </summary>
internal static bool EndsInDirectorySeparator(string path) =>
!string.IsNullOrEmpty(path) && IsDirectorySeparator(path[path.Length - 1]);
internal static bool EndsInDirectorySeparator(ReadOnlySpan<char> path) => path.Length > 0 && IsDirectorySeparator(path[path.Length - 1]);
Copy link
Member

Choose a reason for hiding this comment

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

We seem to only call these methods when path.Length is greater than 0 anyway. Can the length check be a Debug.Assert instead?

Same with StartsWithDirectorySeparator.

Copy link
Member Author

Choose a reason for hiding this comment

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

That is true now, but I expect that to change, particularly as I expect to include this file in a few CoreFX libraries.

Copy link
Member

Choose a reason for hiding this comment

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

We seem to have EndsInDirectorySeparator in already:

internal static bool EndsInDirectorySeparator(ReadOnlySpan<char> path)

But not in Path.Unix.cs (and we don't have StartsWithDirectorySeparator)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, missed that. I'll get this one. I have a fair amount of cleanup to do- I intend to do so when I include the Path sources directly in System.Runtime.Extensions again.

@JeremyKuhne JeremyKuhne removed the * NO MERGE * The PR is not ready for merge yet (see discussion for detailed reasons) label Feb 27, 2018
JeremyKuhne added a commit to JeremyKuhne/corefx that referenced this pull request Feb 27, 2018
@@ -444,6 +444,40 @@ public static bool TryJoin(Span<char> destination, out int charsWritten, ReadOnl
return true;
}

public static bool TryJoin(ReadOnlySpan<char> path1, ReadOnlySpan<char> path2, ReadOnlySpan<char> path3, Span<char> destination, out int charsWritten)
Copy link
Member

Choose a reason for hiding this comment

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

Any way this method can reuse the 2 path overload?

Join the first 2 paths, then combine the result with the 3rd?

Copy link
Member Author

Choose a reason for hiding this comment

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

I do use the other overload. Part of the contract here is that I don't write into the buffer if I can't fit the results so I can't simplify beyond what I've done.

@@ -444,6 +444,40 @@ public static bool TryJoin(Span<char> destination, out int charsWritten, ReadOnl
return true;
}

public static bool TryJoin(ReadOnlySpan<char> path1, ReadOnlySpan<char> path2, ReadOnlySpan<char> path3, Span<char> destination, out int charsWritten)
{
charsWritten = 0;
Copy link
Member

Choose a reason for hiding this comment

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

nit: Thoughts on moving this assignment only at the return sites? In this case, there are only two such places (line 451/467) since it is already set in other places.

@Anipik
Copy link

Anipik commented Feb 27, 2018

@JeremyKuhne after this #16598 gets merged we will have to change this CombineNoChecksInternal to joinInternal 8199a7e#diff-6836e42e6acde3dd212e4e6e012906ccR102

return true;
}

public static bool TryJoin(ReadOnlySpan<char> path1, ReadOnlySpan<char> path2, ReadOnlySpan<char> path3, Span<char> destination, out int charsWritten)
Copy link
Member

@ahsonkhan ahsonkhan Feb 27, 2018

Choose a reason for hiding this comment

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

I think this can be optimized similar to the 2 path overload (probably even bigger impact), but I don't think it needs to block this PR. Feel free to file an issue/optimize outside this change.

I would suggest, however, that we do the following instead of calling the methods that do the length checks:
IsDirectorySeparator(path1[path1.Length - 1])
IsDirectorySeparator(path2[0])

Copy link
Member

@ahsonkhan ahsonkhan left a comment

Choose a reason for hiding this comment

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

Other than perf, looks good to me.

@JeremyKuhne JeremyKuhne merged commit f1fee6d into dotnet:master Feb 28, 2018
JeremyKuhne added a commit to JeremyKuhne/corefx that referenced this pull request Feb 28, 2018
A-And pushed a commit to A-And/coreclr that referenced this pull request Feb 28, 2018
* Add Path.Join() methods.

See dotnet#25536.

* Address feedback and fix a couple issues now that I've got tests running correctly.

* Update per final API approval

* Fix Unix, remove redundant helper.

* Merge and tweak join methods in GetFullPath(string, string)

* Tweak again
JeremyKuhne added a commit to dotnet/corefx that referenced this pull request Feb 28, 2018
* Expose Path.Join and test. Depends on dotnet/coreclr#16561

* Update for final API review changes

* Address feedback

* Address more feedback.
dotnet-bot pushed a commit to dotnet/corert that referenced this pull request Feb 28, 2018
* Add Path.Join() methods.

See #25536.

* Address feedback and fix a couple issues now that I've got tests running correctly.

* Update per final API approval

* Fix Unix, remove redundant helper.

* Merge and tweak join methods in GetFullPath(string, string)

* Tweak again

Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>
dotnet-bot pushed a commit to dotnet/corefx that referenced this pull request Feb 28, 2018
* Add Path.Join() methods.

See #25536.

* Address feedback and fix a couple issues now that I've got tests running correctly.

* Update per final API approval

* Fix Unix, remove redundant helper.

* Merge and tweak join methods in GetFullPath(string, string)

* Tweak again

Signed-off-by: dotnet-bot-corefx-mirror <dotnet-bot@microsoft.com>
Anipik pushed a commit to dotnet/corert that referenced this pull request Feb 28, 2018
* Add Path.Join() methods.

See #25536.

* Address feedback and fix a couple issues now that I've got tests running correctly.

* Update per final API approval

* Fix Unix, remove redundant helper.

* Merge and tweak join methods in GetFullPath(string, string)

* Tweak again

Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>
ahsonkhan pushed a commit to dotnet/corefx that referenced this pull request Mar 1, 2018
* Add Path.Join() methods.

See #25536.

* Address feedback and fix a couple issues now that I've got tests running correctly.

* Update per final API approval

* Fix Unix, remove redundant helper.

* Merge and tweak join methods in GetFullPath(string, string)

* Tweak again

Signed-off-by: dotnet-bot-corefx-mirror <dotnet-bot@microsoft.com>
kbaladurin pushed a commit to kbaladurin/corert that referenced this pull request Mar 15, 2018
* Add Path.Join() methods.

See #25536.

* Address feedback and fix a couple issues now that I've got tests running correctly.

* Update per final API approval

* Fix Unix, remove redundant helper.

* Merge and tweak join methods in GetFullPath(string, string)

* Tweak again

Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
* Expose Path.Join and test. Depends on dotnet/coreclr#16561

* Update for final API review changes

* Address feedback

* Address more feedback.


Commit migrated from dotnet/corefx@0a092a2
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants