Make dirname and basename ignore trailing separators and double slashes.

Separators are still preserved for other path methods.

Review URL: https://codereview.chromium.org//11638021

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/path@16397 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/lib/path.dart b/lib/path.dart
index 388c084..99eee7b 100644
--- a/lib/path.dart
+++ b/lib/path.dart
@@ -28,12 +28,20 @@
 ///
 ///     path.basename('path/to/foo.dart'); // -> 'foo.dart'
 ///     path.basename('path/to');          // -> 'to'
+///
+/// Trailing separators are ignored.
+///
+///     builder.dirname('path/to/'); // -> 'to'
 String basename(String path) => _builder.basename(path);
 
 /// Gets the part of [path] after the last separator, and without any trailing
 /// file extension.
 ///
 ///     path.basenameWithoutExtension('path/to/foo.dart'); // -> 'foo'
+///
+/// Trailing separators are ignored.
+///
+///     builder.dirname('path/to/foo.dart/'); // -> 'foo'
 String basenameWithoutExtension(String path) =>
     _builder.basenameWithoutExtension(path);
 
@@ -41,6 +49,10 @@
 ///
 ///     path.dirname('path/to/foo.dart'); // -> 'path/to'
 ///     path.dirname('path/to');          // -> 'to'
+///
+/// Trailing separators are ignored.
+///
+///     builder.dirname('path/to/'); // -> 'path'
 String dirname(String path) => _builder.dirname(path);
 
 /// Gets the file extension of [path]: the portion of [basename] from the last
@@ -190,30 +202,41 @@
   ///
   ///     builder.basename('path/to/foo.dart'); // -> 'foo.dart'
   ///     builder.basename('path/to');          // -> 'to'
+  ///
+  /// Trailing separators are ignored.
+  ///
+  ///     builder.dirname('path/to/'); // -> 'to'
   String basename(String path) => _parse(path).basename;
 
   /// Gets the part of [path] after the last separator on the builder's
   /// platform, and without any trailing file extension.
   ///
   ///     builder.basenameWithoutExtension('path/to/foo.dart'); // -> 'foo'
+  ///
+  /// Trailing separators are ignored.
+  ///
+  ///     builder.dirname('path/to/foo.dart/'); // -> 'foo'
   String basenameWithoutExtension(String path) =>
-      _parse(path).basenameWithoutExtension;
+    _parse(path).basenameWithoutExtension;
 
   /// Gets the part of [path] before the last separator.
   ///
   ///     builder.dirname('path/to/foo.dart'); // -> 'path/to'
-  ///     builder.dirname('path/to');          // -> 'to'
+  ///     builder.dirname('path/to');          // -> 'path'
+  ///
+  /// Trailing separators are ignored.
+  ///
+  ///     builder.dirname('path/to/'); // -> 'path'
   String dirname(String path) {
     var parsed = _parse(path);
+    parsed.removeTrailingSeparators();
     if (parsed.parts.isEmpty) return parsed.root == null ? '.' : parsed.root;
-    if (!parsed.hasTrailingSeparator) {
-      if (parsed.parts.length == 1) {
-        return parsed.root == null ? '.' : parsed.root;
-      }
-      parsed.parts.removeLast();
-      parsed.separators.removeLast();
+    if (parsed.parts.length == 1) {
+      return parsed.root == null ? '.' : parsed.root;
     }
-    parsed.separators[parsed.separators.length - 1] = '';
+    parsed.parts.removeLast();
+    parsed.separators.removeLast();
+    parsed.removeTrailingSeparators();
     return parsed.toString();
   }
 
@@ -440,7 +463,7 @@
 
     // Make it relative.
     pathParsed.root = '';
-    pathParsed.removeTrailingSeparator();
+    pathParsed.removeTrailingSeparators();
 
     return pathParsed.toString();
   }
@@ -450,10 +473,12 @@
   ///     builder.withoutExtension('path/to/foo.dart'); // -> 'path/to/foo'
   String withoutExtension(String path) {
     var parsed = _parse(path);
-    if (parsed.hasTrailingSeparator) return parsed.toString();
 
-    if (!parsed.parts.isEmpty) {
-      parsed.parts[parsed.parts.length - 1] = parsed.basenameWithoutExtension;
+    for (var i = parsed.parts.length - 1; i >= 0; i--) {
+      if (!parsed.parts[i].isEmpty) {
+        parsed.parts[i] = parsed.basenameWithoutExtension;
+        break;
+      }
     }
 
     return parsed.toString();
@@ -555,29 +580,31 @@
   /// The file extension of the last part, or "" if it doesn't have one.
   String get extension => _splitExtension()[1];
 
-  /// `true` if the path ends with a trailing separator.
-  bool get hasTrailingSeparator {
-    if (separators.length == 0) return false;
-    return separators[separators.length - 1] != '';
-  }
-
   /// `true` if this is an absolute path.
   bool get isAbsolute => root != null;
 
   _ParsedPath(this.style, this.root, this.parts, this.separators);
 
   String get basename {
-    if (parts.length == 0) return extension;
-    if (hasTrailingSeparator) return '';
-    return parts.last;
+    var copy = this.clone();
+    copy.removeTrailingSeparators();
+    if (copy.parts.isEmpty) return root == null ? '' : root;
+    return copy.parts.last;
   }
 
-  String get basenameWithoutExtension => _splitExtension()[0];
+  String get basenameWithoutExtension {
+    var copy = this.clone();
+    copy.removeTrailingSeparators();
+    if (copy.parts.isEmpty) return root == null ? '' : root;
+    return copy._splitExtension()[0];
+  }
 
-  void removeTrailingSeparator() {
-    if (separators.length > 0) {
-      separators[separators.length - 1] = '';
+  void removeTrailingSeparators() {
+    while (!parts.isEmpty && parts.last == '') {
+      parts.removeLast();
+      separators.removeLast();
     }
+    if (separators.length > 0) separators[separators.length - 1] = '';
   }
 
   void normalize() {
@@ -617,7 +644,7 @@
     parts = newParts;
     separators = newSeparators;
 
-    removeTrailingSeparator();
+    removeTrailingSeparators();
   }
 
   String toString() {
@@ -636,7 +663,6 @@
   /// or "" if it has none.
   List<String> _splitExtension() {
     if (parts.isEmpty) return ['', ''];
-    if (hasTrailingSeparator) return ['', ''];
 
     var file = parts.last;
     if (file == '..') return ['..', ''];
@@ -649,4 +675,7 @@
 
     return [file.substring(0, lastDot), file.substring(lastDot)];
   }
+
+  _ParsedPath clone() => new _ParsedPath(
+      style, root, new List.from(parts), new List.from(separators));
 }
diff --git a/test/path_posix_test.dart b/test/path_posix_test.dart
index f929c2a..b917e93 100644
--- a/test/path_posix_test.dart
+++ b/test/path_posix_test.dart
@@ -48,14 +48,18 @@
     expect(builder.dirname('a/b'), 'a');
     expect(builder.dirname('a/b/c'), 'a/b');
     expect(builder.dirname('a/b.c'), 'a');
-    expect(builder.dirname('a/'), 'a');
+    expect(builder.dirname('a/'), '.');
     expect(builder.dirname('a/.'), 'a');
     expect(builder.dirname(r'a\b/c'), r'a\b');
     expect(builder.dirname('/a'), '/');
+    expect(builder.dirname('///a'), '/');
     expect(builder.dirname('/'), '/');
-    expect(builder.dirname('a/b/'), 'a/b');
+    expect(builder.dirname('///'), '/');
+    expect(builder.dirname('a/b/'), 'a');
     expect(builder.dirname(r'a/b\c'), 'a');
-    expect(builder.dirname('a//'), 'a/');
+    expect(builder.dirname('a//'), '.');
+    expect(builder.dirname('a/b//'), 'a');
+    expect(builder.dirname('a//b'), 'a');
   });
 
   test('basename', () {
@@ -64,15 +68,16 @@
     expect(builder.basename('a/b'), 'b');
     expect(builder.basename('a/b/c'), 'c');
     expect(builder.basename('a/b.c'), 'b.c');
-    expect(builder.basename('a/'), '');
+    expect(builder.basename('a/'), 'a');
     expect(builder.basename('a/.'), '.');
     expect(builder.basename(r'a\b/c'), 'c');
     expect(builder.basename('/a'), 'a');
-    // TODO(nweiz): this should actually return '/'
-    expect(builder.basename('/'), '');
-    expect(builder.basename('a/b/'), '');
+    expect(builder.basename('/'), '/');
+    expect(builder.basename('a/b/'), 'b');
     expect(builder.basename(r'a/b\c'), r'b\c');
-    expect(builder.basename('a//'), '');
+    expect(builder.basename('a//'), 'a');
+    expect(builder.basename('a/b//'), 'b');
+    expect(builder.basename('a//b'), 'b');
   });
 
   test('basenameWithoutExtension', () {
@@ -81,11 +86,16 @@
     expect(builder.basenameWithoutExtension('a/b'), 'b');
     expect(builder.basenameWithoutExtension('a/b/c'), 'c');
     expect(builder.basenameWithoutExtension('a/b.c'), 'b');
-    expect(builder.basenameWithoutExtension('a/'), '');
+    expect(builder.basenameWithoutExtension('a/'), 'a');
     expect(builder.basenameWithoutExtension('a/.'), '.');
     expect(builder.basenameWithoutExtension(r'a/b\c'), r'b\c');
     expect(builder.basenameWithoutExtension('a/.bashrc'), '.bashrc');
     expect(builder.basenameWithoutExtension('a/b/c.d.e'), 'c.d');
+    expect(builder.basenameWithoutExtension('a//'), 'a');
+    expect(builder.basenameWithoutExtension('a/b//'), 'b');
+    expect(builder.basenameWithoutExtension('a//b'), 'b');
+    expect(builder.basenameWithoutExtension('a/b.c/'), 'b');
+    expect(builder.basenameWithoutExtension('a/b.c//'), 'b');
   });
 
   test('isAbsolute', () {
@@ -358,5 +368,7 @@
     expect(builder.withoutExtension(r'a.b\c'), r'a');
     expect(builder.withoutExtension(r'a/b\c'), r'a/b\c');
     expect(builder.withoutExtension(r'a/b\c.d'), r'a/b\c');
+    expect(builder.withoutExtension('a/b.c/'), 'a/b/');
+    expect(builder.withoutExtension('a/b.c//'), 'a/b//');
   });
 }
diff --git a/test/path_windows_test.dart b/test/path_windows_test.dart
index a94d126..744b7ab 100644
--- a/test/path_windows_test.dart
+++ b/test/path_windows_test.dart
@@ -54,15 +54,19 @@
     expect(builder.dirname(r'a\b'), 'a');
     expect(builder.dirname(r'a\b\c'), r'a\b');
     expect(builder.dirname(r'a\b.c'), 'a');
-    expect(builder.dirname(r'a\'), 'a');
-    expect(builder.dirname('a/'), 'a');
+    expect(builder.dirname(r'a\'), '.');
+    expect(builder.dirname('a/'), '.');
     expect(builder.dirname(r'a\.'), 'a');
     expect(builder.dirname(r'a\b/c'), r'a\b');
     expect(builder.dirname(r'C:\a'), r'C:\');
-    expect(builder.dirname('C:\\'), r'C:\');
-    expect(builder.dirname(r'a\b\'), r'a\b');
+    expect(builder.dirname(r'C:\\\a'), r'C:\');
+    expect(builder.dirname(r'C:\'), r'C:\');
+    expect(builder.dirname(r'C:\\\'), r'C:\');
+    expect(builder.dirname(r'a\b\'), r'a');
     expect(builder.dirname(r'a/b\c'), 'a/b');
-    expect(builder.dirname(r'a\\'), r'a\');
+    expect(builder.dirname(r'a\\'), r'.');
+    expect(builder.dirname(r'a\b\\'), 'a');
+    expect(builder.dirname(r'a\\b'), 'a');
   });
 
   test('basename', () {
@@ -71,16 +75,17 @@
     expect(builder.basename(r'a\b'), 'b');
     expect(builder.basename(r'a\b\c'), 'c');
     expect(builder.basename(r'a\b.c'), 'b.c');
-    expect(builder.basename(r'a\'), '');
-    expect(builder.basename(r'a/'), '');
+    expect(builder.basename(r'a\'), 'a');
+    expect(builder.basename(r'a/'), 'a');
     expect(builder.basename(r'a\.'), '.');
     expect(builder.basename(r'a\b/c'), r'c');
     expect(builder.basename(r'C:\a'), 'a');
-    // TODO(nweiz): this should actually return 'C:\'
-    expect(builder.basename(r'C:\'), '');
-    expect(builder.basename(r'a\b\'), '');
+    expect(builder.basename(r'C:\'), r'C:\');
+    expect(builder.basename(r'a\b\'), 'b');
     expect(builder.basename(r'a/b\c'), 'c');
-    expect(builder.basename(r'a\\'), '');
+    expect(builder.basename(r'a\\'), 'a');
+    expect(builder.basename(r'a\b\\'), 'b');
+    expect(builder.basename(r'a\\b'), 'b');
   });
 
   test('basenameWithoutExtension', () {
@@ -89,11 +94,16 @@
     expect(builder.basenameWithoutExtension(r'a\b'), 'b');
     expect(builder.basenameWithoutExtension(r'a\b\c'), 'c');
     expect(builder.basenameWithoutExtension(r'a\b.c'), 'b');
-    expect(builder.basenameWithoutExtension(r'a\'), '');
+    expect(builder.basenameWithoutExtension(r'a\'), 'a');
     expect(builder.basenameWithoutExtension(r'a\.'), '.');
     expect(builder.basenameWithoutExtension(r'a\b/c'), r'c');
     expect(builder.basenameWithoutExtension(r'a\.bashrc'), '.bashrc');
     expect(builder.basenameWithoutExtension(r'a\b\c.d.e'), 'c.d');
+    expect(builder.basenameWithoutExtension(r'a\\'), 'a');
+    expect(builder.basenameWithoutExtension(r'a\b\\'), 'b');
+    expect(builder.basenameWithoutExtension(r'a\\b'), 'b');
+    expect(builder.basenameWithoutExtension(r'a\b.c\'), 'b');
+    expect(builder.basenameWithoutExtension(r'a\b.c\\'), 'b');
   });
 
   test('isAbsolute', () {
@@ -392,5 +402,6 @@
     expect(builder.withoutExtension(r'a\b/c'), r'a\b/c');
     expect(builder.withoutExtension(r'a\b/c.d'), r'a\b/c');
     expect(builder.withoutExtension(r'a.b/c'), r'a.b/c');
+    expect(builder.withoutExtension(r'a\b.c\'), r'a\b\');
   });
 }