在Rust中实现类似Node.js path.relative的相对路径功能
实现Rust版Node.js path.relative功能(非直接移植)
需求说明
要在Rust中实现类似Node.js path.relative(...)的功能,不直接移植Node.js原代码。已通过regex crate定义路径分隔符正则:
static PATH_SEPARATOR: Lazy<Regex> = lazy_regex!(r"[/\\]");
要求计算已解析所有..、.及//的绝对路径a到同类型绝对路径b的相对路径,例如调用relative_path("/a/b", "/a/c")应返回"../c"。
现有代码及问题
已实现路径解析函数resolve_single_path():
fn resolve_single_path(path: &str) -> String { let mut r = Vec::<String>::new(); for p in PATH_SEPARATOR.split(path) { if p == "." { continue; } else if p == ".." { if !r.is_empty() { r.remove(r.len() - 1); } } else if !p.is_empty() { r.push(p.to_owned()); } } r.join("/") }
但自行编写的relative_path()函数存在逻辑错误,当from_parts与to_parts长度不同时输出不符合预期:
pub fn relative_path(from_path: &str, to_path: &str) -> String { let mut r = Vec::<String>::new(); let from_parts: Vec<String> = PATH_SEPARATOR.split(resolve_single_path(from_path).as_ref()).map(|s| s.to_owned()).collect(); let to_parts: Vec<String> = PATH_SEPARATOR.split(resolve_single_path(to_path).as_ref()).map(|s| s.to_owned()).collect(); let l1 = from_parts.len(); let l2 = to_parts.len(); for i in 0..l1 { if from_parts[i] != to_parts[i] { r.push("..".to_owned()); } r.push(to_parts[i].clone()); } if l1 > l2 { for _i in l2..l1 { r.push("..".to_owned()); } } else { for i in l1..l2 { r.push(to_parts[i].clone()); } } let r = r.join("/"); let r = r.trim(); if r.is_empty() {".".to_owned() } else { r.to_owned() } }
错误分析
原函数的核心逻辑错误:
- 循环遍历
from_parts时,无论路径段是否相等,都直接将to_parts[i]加入结果,导致公共路径段被错误重复添加。 - 当
from_parts比to_parts长时,直接追加..的逻辑未结合公共前缀判断,会生成多余的上级路径。 - 未先找到两个路径的最长公共前缀,这是计算相对路径的核心前提。
正确实现逻辑
计算相对路径的正确步骤:
- 解析两个输入路径为处理后的路径段数组(已过滤
./../空段)。 - 找到两个路径段数组的最长公共前缀长度:遍历数组直到出现第一个不相等的路径段,记录此时的索引。
- 对
from_parts中公共前缀之后的每个路径段,添加一个..到结果(表示向上一级)。 - 将
to_parts中公共前缀之后的所有路径段添加到结果(表示向下进入目标路径)。 - 处理边界情况:若结果为空(两个路径完全相同),返回
".";否则用/连接结果为字符串。
修正后的代码
pub fn relative_path(from_path: &str, to_path: &str) -> String { // 解析路径为处理后的路径段数组,过滤空段 let from_parts: Vec<String> = PATH_SEPARATOR .split(resolve_single_path(from_path).as_ref()) .filter(|s| !s.is_empty()) .map(|s| s.to_owned()) .collect(); let to_parts: Vec<String> = PATH_SEPARATOR .split(resolve_single_path(to_path).as_ref()) .filter(|s| !s.is_empty()) .map(|s| s.to_owned()) .collect(); // 计算最长公共前缀的长度 let common_prefix_len = from_parts .iter() .zip(to_parts.iter()) .take_while(|(a, b)| a == b) .count(); let mut result = Vec::new(); // 从from路径向上走到公共前缀的父级 for _ in common_prefix_len..from_parts.len() { result.push("..".to_owned()); } // 从公共前缀向下走到to路径 result.extend(to_parts[common_prefix_len..].to_vec()); // 处理边界情况 if result.is_empty() { ".".to_owned() } else { result.join("/") } }
测试验证
- 测试用例1:
relative_path("/a/b", "/a/c")→ 返回"../c",符合预期。 - 测试用例2:
relative_path("/a/b/c", "/a")→ 返回"../.."。 - 测试用例3:
relative_path("/x/y", "/x/y")→ 返回"."。 - 测试用例4:
relative_path("/a/b/c", "/a/d/e/f")→ 返回"../../d/e/f"。
内容的提问来源于stack exchange,提问作者Hydroper
相关产品推荐
相关产品推荐

