Skip to content

Instantly share code, notes, and snippets.

@flangofas
Created October 11, 2019 09:03
Show Gist options
  • Save flangofas/407aec84124f5e1225932269adb8e578 to your computer and use it in GitHub Desktop.
Save flangofas/407aec84124f5e1225932269adb8e578 to your computer and use it in GitHub Desktop.
Write a function that provides change directory (cd) function for an abstract file system.
<?php
/**
* Write a function that provides change directory (cd) function for an abstract file system.
* Notes:
* root path is '/'.
* path separator is '/'.
* parent directory is addressable as '..'.
* directory names consist only of English alphabet letters (A-Z and a-z).
* the function will not be passed any invalid paths.
* do not use built-in path-related functions.
*
* Solution explained:
* The filesystem takes the cd request and breaks down into smaller tokens/patterns and then it processes one by one.
* The purpose of DirectoryPattern is to
* 1. Recognize the token
* 2. Execute it
*
* We can scale this solution by adding new Directory Pattern to the constructor of the FileSystem, for example, imagine we need
* implement the MoveBackDirectoryPattern which implies to "cd -".
*
* First we need to create the pattern and then assign it to the constructor. That's it! Of course, we need to create a new varialble
* oldPath in filesystem where the value of the old path can be held.
*
* Regarding, the invalid paths, a RuntimeException is being thrown when a token is not recognized.
*
* @author Antonis Flangofas
*/
declare(strict_types=1);
class FileSystem
{
private $patterns = [];
private $path;
public function __construct(string $path)
{
$this->currentPath = $path;
$this->patterns = [
new ChangeDirectoryPattern,
new MoveUpDirectoryPattern
];
}
/**
* Changes directory of the current path
*
* @param string $newDir
*/
public function cd(string $newDir) :void
{
$this->process($newDir);
}
/**
* Returns current path
*
* @return string
*/
public function path() :string
{
return $this->currentPath;
}
/**
* Take the cd request and break it down in smaller request and process one by one.
*
* When a request is ../../foobar, it will be tokenized into an array of
* ['..', '..', 'directoryName']. Then, this array is being looped to match
* each token with the applicable cd pattern.
*
* @param string $pattern
*/
private function process(string $pattern) :void
{
//Break down pattern into tokens and process one by one
$tokenized = array_filter(explode(DIRECTORY_SEPARATOR, $pattern));
//Root directory pattern
//Append the directory separator at start
$strpos = strpos($pattern, DIRECTORY_SEPARATOR);
if ($strpos === 0) {
array_unshift($tokenized, DIRECTORY_SEPARATOR);
}
//Current path change directory pattern
//Remove the dot
$strpos = strpos($pattern, './');
if ($strpos === 0) {
array_shift($tokenized);
}
foreach ($tokenized as $token) {
$this->matchTokenToPattern($token);
}
}
/**
* Use the assigned patterns to find which applies per tokeninzed cd request.
*
* @param string $token
*/
private function matchTokenToPattern(string $token) :void
{
$patternFound = false;
foreach ($this->patterns as $patternObj) {
if ($patternObj->isApplicable($token)) {
$patternFound = true;
$this->currentPath = $patternObj->execute($this->currentPath, $token);
break;
}
}
if (!$patternFound) {
throw new \RuntimeException('This pattern: ' . $token . ' cannot be recognized');
}
}
}
interface ChangeDirectoryPatternInterface
{
/**
* Check whether the given pattern is recognized.
*
* @param string $pattern
* @return bool
*/
public function isApplicable(string $pattern) :bool;
/**
* Execute and apply the defined change of the pattern.
*
* @param string $pattern
* @return bool
*/
public function execute(string $pattern, string $path) : string;
}
class MoveUpDirectoryPattern implements ChangeDirectoryPatternInterface
{
/**
* @inheritdoc
*/
public function isApplicable(string $pattern) :bool
{
return $pattern === '..';
}
/**
* @inheritdoc
*/
public function execute(string $path, string $dir) :string
{
$tokenized = explode(DIRECTORY_SEPARATOR, $path);
array_pop($tokenized);
return implode($tokenized, DIRECTORY_SEPARATOR);
}
}
class ChangeDirectoryPattern implements ChangeDirectoryPatternInterface
{
/**
* @inheritdoc
*/
public function isApplicable(string $pattern) :bool
{
//Single Word
preg_match('#^[a-z]+.*$#i', $pattern, $matches);
if (!empty($matches)) {
return true;
}
//Root directory request
if ($this->isRoot($pattern)) {
return true;
}
return false;
}
/**
* @inheritdoc
*/
public function execute(string $path, string $dir) :string
{
$result = "";
if ($this->isRoot($dir)) {
$result = $dir;
} else if ($path === DIRECTORY_SEPARATOR) {
$result = $path . $dir;
} else {
$result = $path . DIRECTORY_SEPARATOR . $dir;
}
return $result;
}
/**
* Validate if it is a root dir
*
* @param string $dir
* @return bool
*/
private function isRoot(string $dir) :bool
{
return $dir === DIRECTORY_SEPARATOR;
}
}
/**
* Testing cases
*/
$fs = new FileSystem('/a/b/c/d/e');
echo $fs->path() . PHP_EOL;
echo "cd foo-bar" . PHP_EOL;
$fs->cd('foo-bar');
echo $fs->path() . PHP_EOL;
//Prints /a/b/c/d/e/foobar
echo "cd ../../subfolder" . PHP_EOL;
$fs->cd('../../subfolder');
echo $fs->path() . PHP_EOL;
//Prints /a/b/c/d/subfolder
echo "cd ./newfolder" . PHP_EOL;
$fs->cd('./newfolder');
echo $fs->path() . PHP_EOL;
//Prints /a/b/c/d/subfolder/newfolder
echo "cd /home/username" . PHP_EOL;
$fs->cd('/home/username');
echo $fs->path() . PHP_EOL;
//Prints /home/username
echo "cd ../documents" . PHP_EOL;
$fs->cd('../documents');
echo $fs->path() . PHP_EOL;
//Prints /home/documents
echo "cd ../-/&&/" . PHP_EOL;
$fs->cd('../-/&&/"');
//Throws Exception
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment