49

この署名を使用して関数を実装するように求められた就職の面接をちょうどタンクに入れました。

function justify($str_in, $desired_length)

HTML の text-align: justify が行うことを模倣する必要があります。いくつかの例を次に示します (desired_length = 48)

    Hello world there ok then = こんにちは……世界……そこ……ok……それから
    こんにちは= ...................こんにちは.................................
    わかりました = わかりました..................................................
    この文字列はほぼ確実に 48 より長いと思います = this.string.is.almost.certainly.longer.than.48.
    2 つの単語 = 2.................................................................単語
    3 つの OK ワード = 3....OK....ワード
    1 2 3 4 5 6 7 8 9 = 1....2....3.....4.....5.....6.....7..... 8.....9

(説明のためにスペースをピリオドに置き換えました)

単語間のスペースの長さの差が 1 を超えることはありません。

PHP ソリューションを作成しましたが、問題を解決するために人々がどのようなアルゴリズムを考え出すことができるかに興味があります。就職の面接で初めてのホワイトボードの質問でしたが、さまざまな要因が組み合わさって、必要以上に時間がかかってしまったのではないかと心配しています。

4

12 に答える 12

11

これが私が思いついたものです。オプションのパラメーターを追加して、$char出力内容を確認できるようにしました。もちろん、関数内でプルして、プロトタイプが要件に一致するようにすることもできます。

function justify($str_in, $desired_length, $char = '_') {

    // Some common vars and simple error checking / sanitation
    $return = '';
    $str_in = trim( $str_in);
    $desired_length = intval( $desired_length);

    // If we've got invalid input, we're done
    if( $desired_length <= 0)
        return $str_in;

    // If the input string is greater than the length, we need to truncate it WITHOUT splitting words
    if( strlen( $str_in) > $desired_length) {
        $str = wordwrap($str_in, $desired_length);
        $str = explode("\n", $str);
        $str_in = $str[0];
    }

    $words = explode( ' ', $str_in);
    $num_words = count( $words);

    // If there's only one word, it's a simple edge case
    if( $num_words == 1) {
        $length = ($desired_length - strlen( $words[0])) / 2;
        $return .= str_repeat( $char, floor( $length)) . $words[0] . str_repeat( $char, ceil( $length));
    } else {
        $word_length = strlen( implode( '', $words));

        // Calculate the number of spaces to distribute over the words
        $num_words--; // We're going to eliminate the last word
        $spaces = floor( ($desired_length - $word_length) / $num_words);
        $remainder = $desired_length - $word_length - ($num_words * $spaces);

        $last = array_pop( $words);
        foreach( $words as $word) {
            // If we didn't get an even number of spaces to distribute, just tack it on to the front
            $spaces_to_add = $spaces;
            if( $remainder > 0) {
                $spaces_to_add++;
                $remainder--;
            }

            $return .= $word . str_repeat( $char, $spaces_to_add);
        }
        $return .= $last;
    }
    return $return;
}

そしてテストケース:

$inputs = array( 
    'hello world there ok then',
    'hello',
    'ok then',
    'this string is almost certainly longer than 48 I think',
    'two words',
    'three ok words',
    '1 2 3 4 5 6 7 8 9'
);

foreach( $inputs as $x) {
    $ret = justify( $x, 48);
    echo 'Inp: ' . $x . " - strlen(" . strlen( $x) .  ")\n";
    echo 'Out: ' . $ret . " - strlen(" . strlen( $ret) .  ")\n\n";
}

そして出力:

Inp: hello world there ok then - strlen(25)
Out: hello_______world_______there_______ok______then - strlen(48)

Inp: hello - strlen(5)
Out: _____________________hello______________________ - strlen(48)

Inp: ok then - strlen(7)
Out: ok__________________________________________then - strlen(48)

Inp: this string is almost certainly longer than 48 I think - strlen(54)
Out: this_string_is_almost_certainly_longer_than_48_I - strlen(48)

Inp: two words - strlen(9)
Out: two________________________________________words - strlen(48)

Inp: three ok words - strlen(14)
Out: three__________________ok__________________words - strlen(48)

Inp: 1 2 3 4 5 6 7 8 9 - strlen(17)
Out: 1_____2_____3_____4_____5_____6_____7_____8____9 - strlen(48)

そしてデモ!

編集:コードをクリーンアップしましたが、それでも機能します:)

于 2012-06-15T22:54:13.917 に答える
10

コールバックでループ/再帰または正規表現を使用しないことを個人的な課題にしました。私はこれを達成するためにシングルexplode()とシングルを使用しimplode()ました。大成功!

コード

function justify($str, $maxlen) {
    $str = trim($str);

    $strlen = strlen($str);
    if ($strlen >= $maxlen) {
        $str = wordwrap($str, $maxlen);
        $str = explode("\n", $str);
        $str = $str[0];
        $strlen = strlen($str);
    }

    $space_count = substr_count($str, ' ');
    if ($space_count === 0) {
        return str_pad($str, $maxlen, ' ', STR_PAD_BOTH);
    }

    $extra_spaces_needed = $maxlen - $strlen;
    $total_spaces = $extra_spaces_needed + $space_count;

    $space_string_avg_length = $total_spaces / $space_count;
    $short_string_multiplier = floor($space_string_avg_length);
    $long_string_multiplier = ceil($space_string_avg_length);

    $short_fill_string = str_repeat(' ', $short_string_multiplier);
    $long_fill_string = str_repeat(' ', $long_string_multiplier);

    $limit = ($space_string_avg_length - $short_string_multiplier) * $space_count;

    $words_split_by_long = explode(' ', $str, $limit+1);
    $words_split_by_short = $words_split_by_long[$limit];
    $words_split_by_short = str_replace(' ', $short_fill_string, $words_split_by_short);
    $words_split_by_long[$limit] = $words_split_by_short;

    $result = implode($long_fill_string, $words_split_by_long);

    return $result;
}

短い(348文字)

function j($s,$m){$s=trim($s);$l=strlen($s);if($l>=$m){$s=explode("\n",wordwrap($s,$m));$s=$s[0];$l=strlen($s);}$c=substr_count($s,' ');if($c===0)return str_pad($s,$m,' ',STR_PAD_BOTH);$a=($m-$l+$c)/$c;$h=floor($a);$i=($a-$h)*$c;$w=explode(' ',$s,$i+1);$w[$i]=str_replace(' ',str_repeat(' ',$h),$w[$i]);return implode(str_repeat(' ',ceil($a)),$w);}

アルゴリズム/コードの説明

  1. 2つの例外(最大長より長い文字列または1ワードのみ)を処理します。
  2. 各単語の間に必要な平均スペースを見つけます($space_string_avg_length)。
  3. ceil()floor()に基づいて、単語間で使用する長い塗りつぶし文字列と短い塗りつぶし文字列を$space_string_avg_lengthそれぞれ作成します。
  4. 必要な長い塗りつぶし文字列の数を調べます。($limit+1)。
  5. 必要な長い塗りつぶし文字列の数に基づいてテキストを分割します。
  6. 分割によって作成された配列の最後の部分のスペースを短い塗りつぶし文字列に置き換えます。
  7. 分割されたテキストを長い塗りつぶし文字列と一緒に結合します。

テスト

$tests = array(
    'hello world there ok then',
    'hello',
    'ok then',
    'this string is almost certainly longer than 48 I think',
    'two words',
    'three ok words',
    '1 2 3 4 5 6 7 8 9'
);

foreach ($tests as $test) {
    $len_before = strlen($test);
    $processed = str_replace(' ', '_', justify($test, 48));
    $len_after = strlen($processed);
    echo "IN($len_before): $test\n";
    echo "OUT($len_after): $processed\n";
}

結果

IN(25): hello world there ok then
OUT(48): hello_______world_______there_______ok______then
IN(5): hello
OUT(48): _____________________hello______________________
IN(7): ok then
OUT(48): ok__________________________________________then
IN(54): this string is almost certainly longer than 48 I think
OUT(48): this_string_is_almost_certainly_longer_than_48_I
IN(9): two words
OUT(48): two________________________________________words
IN(14): three ok words
OUT(48): three__________________ok__________________words
IN(17): 1 2 3 4 5 6 7 8 9
OUT(48): 1_____2_____3_____4_____5_____6_____7_____8____9

それが実行されるのを見てください!

于 2012-06-16T01:00:59.853 に答える
8

これが厄介なループのない私の解決策です

function justify( $str_in, $desired_length=48 ) {
    if ( strlen( $str_in ) > $desired_length ) {
        $str_in = current( explode( "\n", wordwrap( $str_in, $desired_length ) ) );
    }
    $string_length = strlen( $str_in );
    $spaces_count = substr_count( $str_in, ' ' );
    $needed_spaces_count = $desired_length - $string_length + $spaces_count;
    if ( $spaces_count === 0 ) {
        return str_pad( $str_in, $desired_length, ' ', STR_PAD_BOTH );
    }
    $spaces_per_space = ceil( $needed_spaces_count / $spaces_count );
    $spaced_string = preg_replace( '~\s+~', str_repeat( ' ', $spaces_per_space ), $str_in );
    return preg_replace_callback(
        sprintf( '~\s{%s}~', $spaces_per_space ),
        function ( $m ) use( $spaces_per_space ) {
            return str_repeat( ' ', $spaces_per_space-1 );
        },
        $spaced_string,
        strlen( $spaced_string ) - $desired_length
    );
}

コメントと出力...

https://gist.github.com/2939068

  1. 空きがいくつあるか調べる
  2. 必要なスペースの数を調べる
  3. 既存のスペースを、目的の行の長さを満たす、またはそれを超えるために必要なスペースの量 (均等に分散) に置き換えます。
  4. \s{spaces_inserted}preg_replace_callback を使用して、目的の行の長さを満たすために\s{spaces_inserted-1}必要な量を置き換えます
于 2012-06-15T22:39:24.570 に答える
6

どのアルゴリズムが最も効率的かを確認したかったので、いくつかのベンチマークを実行しました。7 つのテスト ケースすべてを 10 万回繰り返しました。(シングルコアの Ubuntu VM で実行)

@ppsreejith@Kristian Antonsenのコードの結果は、実行しようとしたときにコードがクラッシュしたため、省略されています。@PhpMyCoderのコードは、オブジェクトの構築後に 48 の長さにフォーマットしない限り実行されました。したがって、テスト結果は不完全です。(修理済み)

ベンチマーク結果

$ php justify.bench.php
ガレン (justify1): 5.1464750766754
ニックネーム (justify2): 3.8629620075226
パオロ・ベルガンティーノ (justify3): 4.3705048561096
user381521 (justify5): 8.5988481044769
vlzvl (justify7): 6.6795041561127
アレクサンダー (justify8): 6.7060301303864
オハール (justify9): 2.9896130561829

PhpMyCoder: 6.1514630317688 (修正済み!)

justify.bench.php

<?php
$tests = array(
    'hello world there ok then',
    'hello',
    'ok then',
    'this string is almost certainly longer than 48 I think',
    'two words',
    'three ok words',
    '1 2 3 4 5 6 7 8 9'
);
$testers = array(
    'Galen' => 'justify1',
    'nickb' => 'justify2',
    'Paolo Bergantino' => 'justify3',
//    'Kristian Antonsen' => 'justify4',
    'user381521' => 'justify5',
//    'ppsreejith' => 'justify6',
    'vlzvl' => 'justify7',
    'Alexander' => 'justify8',
    'ohaal' => 'justify9'
);
// ppsreejith and Kristian Antonsen's code crashed and burned when I tried to run it
// PhpMyCoder is a special case, but his code also crashed when doing $jus->format(48);

foreach ($testers as $tester => $func) {
    $b=microtime(true);
    for($i=0;$i<100000;$i++)
        foreach ($tests as $test)
            $func($test,48);
    $a=microtime(true);
    echo $tester.'('.$func.'): '.($a-$b)."\n";
}

echo "\n";

// Fixed!
$jus = new Justifier($tests);
$b=microtime(true);

for($i=0;$i<100000;$i++) {
    $jus->format(54);
}

$a=microtime(true);
echo 'PhpMyCoder: '.($a-$b)." (Fixed!)\n";

// ALGORITHMS BELOW

// Galen
function justify1( $str_in, $desired_length=48 ) {
    if ( strlen( $str_in ) > $desired_length ) {
        $str_in = current( explode( "\n", wordwrap( $str_in, $desired_length ) ) );
    }
    $string_length = strlen( $str_in );
    $spaces_count = substr_count( $str_in, ' ' );
    $needed_spaces_count = $desired_length - $string_length + $spaces_count;
    if ( $spaces_count === 0 ) {
        return str_pad( $str_in, $desired_length, ' ', STR_PAD_BOTH );
    }
    $spaces_per_space = ceil( $needed_spaces_count / $spaces_count );
    $spaced_string = preg_replace( '~\s+~', str_repeat( ' ', $spaces_per_space ), $str_in );
    return preg_replace_callback(
        sprintf( '~\s{%s}~', $spaces_per_space ),
        function ( $m ) use( $spaces_per_space ) {
            return str_repeat( ' ', $spaces_per_space-1 );
        },
        $spaced_string,
        strlen( $spaced_string ) - $desired_length
    );
}
// nickb
function justify2($str_in, $desired_length, $char = '_') {

    // Some common vars and simple error checking / sanitation
    $return = '';
    $str_in = trim( $str_in);
    $desired_length = intval( $desired_length);

    // If we've got invalid input, we're done
    if( $desired_length <= 0)
        return $str_in;

    // If the input string is greater than the length, we need to truncate it WITHOUT splitting words
    if( strlen( $str_in) > $desired_length) {
        $str = wordwrap($str_in, $desired_length);
        $str = explode("\n", $str);
        $str_in = $str[0];
    }

    $words = explode( ' ', $str_in);
    $num_words = count( $words);

    // If there's only one word, it's a simple edge case
    if( $num_words == 1) {
        $length = ($desired_length - strlen( $words[0])) / 2;
        $return .= str_repeat( $char, floor( $length)) . $words[0] . str_repeat( $char, ceil( $length));
    } else {
        $word_length = strlen( implode( '', $words));

        // Calculate the number of spaces to distribute over the words
        $num_words--; // We're going to eliminate the last word
        $spaces = floor( ($desired_length - $word_length) / $num_words);
        $remainder = $desired_length - $word_length - ($num_words * $spaces);

        $last = array_pop( $words);
        foreach( $words as $word) {
            // If we didn't get an even number of spaces to distribute, just tack it on to the front
            $spaces_to_add = $spaces;
            if( $remainder > 0) {
                $spaces_to_add++;
                $remainder--;
            }

            $return .= $word . str_repeat( $char, $spaces_to_add);
        }
        $return .= $last;
    }
    return $return;
}
// Paolo Bergantino
function justify3($str, $to_len) {
    $str = trim($str);
    $strlen = strlen($str);

    if($str == '') return '';

    if($strlen >= $to_len) {
        return substr($str, 0, $to_len);   
    }

    $words = explode(' ', $str);
    $word_count = count($words);
    $space_count = $word_count - 1;

    if($word_count == 1) {
        return str_pad($str, $to_len, ' ', STR_PAD_BOTH);
    }

    $space = $to_len - $strlen + $space_count;
    $per_space = $space/$space_count;

    if(is_int($per_space)) {
        return implode($words, str_pad('', $per_space, ' '));    
    }

    $new_str = '';
    $spacing = floor($per_space);
    $new_str .= $words[0] . str_pad('', $spacing);
    foreach($words as $x => $word) {
        if($x == $word_count - 1 || $x == 0) continue;
        if($x < $word_count - 1) {
            $diff = $to_len - strlen($new_str) - (strlen(implode('', array_slice($words, $x))));
            $new_str .= $word . str_pad('', floor($diff/($space_count - $x)), ' ');
        }
    }
    $new_str .= $words[$x];

    return $new_str;   
}
// Kristian Antonsen
function justify4($str_in, $desired_length)
{
    foreach ($str_in as &$line) {
        $words = explode(' ', $line);
        $word_count = count($words) - 1;
        $spaces_to_fill = $desired_length - strlen($line) + $word_count;
        if (count($words) == 1) {
            $line = str_repeat('_', ceil($spaces_to_fill/2)) . $line
                  . str_repeat('_', floor($spaces_to_fill/2));
            continue;
        }
        $next_space = floor($spaces_to_fill/$word_count);
        $leftover_space = $spaces_to_fill % $word_count;
        $line = array_shift($words);
        foreach($words as $word) {
            $extra_space = ($leftover_space) ? ceil($leftover_space / $word_count) : 0;
            $leftover_space -= $extra_space;
            $line .= str_repeat('_', $next_space + $extra_space) . $word;
        }
    }
    return $str_in;
}
// user381521
function justify5 ($str, $len)
{
    // split by whitespace, remove empty strings
    $words = array_diff (preg_split ('/\s+/', $str), array (""));

    // just space if no words
    if (count ($words) == 0)
        return str_repeat (" ", $len);

    // add empty strings if only one element
    if (count ($words) == 1)
        $words = array ("", $words[0], "");

    // get number of words and spaces
    $wordcount = count ($words);
    $numspaces = $wordcount - 1;

    // get number of non-space characters
    $numchars = array_sum (array_map ("strlen", $words));

    // get number of characters remaining for space
    $remaining = $len - $numchars;

    // return if too little spaces remaining
    if ($remaining <= $numspaces)
        return substr (implode (" ", $words), 0, $len);

    // get number of spaces per space
    $spaces_per_space = $remaining / $numspaces;
    $spaces_leftover = $remaining % $numspaces;

    // make array for spaces, spread out leftover spaces
    $spaces = array_fill (0, $numspaces, $spaces_per_space);
    while ($spaces_leftover--)
        $spaces[$numspaces - $spaces_leftover - 1]++;
    $spaces[] = 0; // make count ($words) == count ($spaces)

    // join it all together
    $result = array ();
    foreach ($words as $k => $v)
        array_push ($result, $v, str_repeat (" ", $spaces[$k]));
    return implode ($result);
}
// ppsreejith
function justify6($str, $to_len) {
    $str = trim($str);
    $strlen = strlen($str);

    if($str == '') return '';

    if($strlen >= $to_len) {
        return substr($str, 0, $to_len);   
    }

    $words = explode(' ', $str);
    $word_count = count($words);
    $space_count = $word_count - 1;

    if($word_count == 1) {
        return str_pad($str, $to_len, ' ', STR_PAD_BOTH);
    }

    $space = $to_len - $strlen + $space_count;
    $per_space = floor($space/$space_count);
    $spaces = str_pad('', $per_space, ' ');
    $curr_word = implode($words, $spaces);
    while(strlen($curr_word) < $to_len){
    $curr_word = substr($curr_word,0,preg_match("[! ][".$spaces."][! ]",$curr_word)." ".preg_match("[! ][".$spaces."][! ]",$curr_word));
    }
    return $curr_word;
}
// vlzvl
function justify7($str_in, $desired_length)
{
   $str_in = preg_replace("!\s+!"," ",$str_in);   // get rid of multiple spaces
   $words = explode(" ",$str_in);   // break words
   $num_words = sizeof($words);     // num words
   if ($num_words==1) {
      return str_pad($str_in,$desired_length,"_",STR_PAD_BOTH);
   }
   else {
      $num_chars = 0; $lenwords = array();
      for($x=0;$x<$num_words;$x++) { $num_chars += $lenwords[$x] = strlen($words[$x]); }
      $each_div = round(($desired_length - $num_chars) / ($num_words-1));
      for($x=0,$sum=0;$x<$num_words;$x++) { $sum += ($lenwords[$x] + ($x<$num_words-1 ? $each_div : 0)); }
      $space_to_addcut = ($desired_length - $sum);
      for($x=0;$x<$num_words-1;$x++) {
         $words[$x] .= str_repeat("_",$each_div+($each_div>1? ($space_to_addcut<0?-1:($space_to_addcut>0?1:0)) :0));
         if ($each_div>1) { $space_to_addcut += ($space_to_addcut<0 ? 1 : ($space_to_addcut>0?-1:0) ); }
      }
      return substr(implode($words),0,$desired_length);
   }
}
// Alexander
function justify8($str, $length) {
  $words   = explode(' ', $str);
  if(count($words)==1) $words = array("", $str, "");
  $spaces  = $length - array_sum(array_map("strlen", $words));
  $add     = (int)($spaces / (count($words) - 1));
  $left    = $spaces % (count($words) - 1);
  $spaced  = implode(str_repeat("_", $add + 1), array_slice($words, 0, $left + 1));
  $spaced .= str_repeat("_", max(1, $add));
  $spaced .= implode(str_repeat("_", max(1, $add)), array_slice($words, $left + 1));
  return substr($spaced, 0, $length);
}
// ohaal
function justify9($s,$m){$s=trim($s);$l=strlen($s);if($l>=$m){$s=explode("\n",wordwrap($s,$m));$s=$s[0];$l=strlen($s);}$c=substr_count($s,' ');if($c===0)return str_pad($s,$m,' ',STR_PAD_BOTH);$a=($m-$l+$c)/$c;$h=floor($a);$i=($a-$h)*$c;$w=explode(' ',$s,$i+1);$w[$i]=str_replace(' ',str_repeat(' ',$h),$w[$i]);return implode(str_repeat(' ',ceil($a)),$w);}


// PhpMyCoder
class Justifier {
    private $text;

    public function __construct($text) {
        if(!is_string($text) && !is_array($text)) {
            throw new InvalidArgumentException('Expected a string or an array of strings, instead received type: ' . gettype($text));
        }

        if(is_array($text)) {
            // String arrays must be converted to JustifierLine arrays
            $this->text = array_map(function($line) {
                return JustifierLine::fromText($line);
            }, $text);
        } else {
            // Single line of text input
            $this->text = $text;
        }
    }

    public function format($width = NULL) {
        // Strings have to be broken into an array and then jusitifed
        if(is_string($this->text)) {
            if($width == null) {
                throw new InvalidArgumentException('A width must be provided for separation when an un-split string is provided');
            }

            if($width <= 0) {
                throw new InvalidArgumentException('Expected a positive, non-zero width, instead received width of ' . $width);
            }

            // Break up a JustifierLine of all text until each piece is smaller or equal to $width
            $lines = array(JustifierLine::fromText($this->text));
            $count = 0;
            $newLine = $lines[0]->breakAtColumn($width);

            while($newLine !== null) {
                $lines[] = $newLine;
                $newLine = $lines[++$count]->breakAtColumn($width);
            }
        } else {
            $lines = $this->text;

            // Allow for fluid width (uses longest line with single space)
            if($width == NULL) {
                $width = -1;

                foreach($lines as $line) {
                    // Width of line = Sum of the lengths of the words and the spaces (number of words - 1)
                    $newWidth = $line->calculateWordsLength() + $line->countWords() - 1;

                    if($newWidth > $width) { // Looking for the longest line
                        $width = $newWidth;
                    }
                }
            }
        }

        // Justify each element of array
        //$output = array_map(function($line) use ($width) {
        //    return $this->justify($line, $width);
        //}, $lines);

        $output = array();
        foreach($lines as $line) {
            $output[] = $this->justify($line, $width);
        }            

        // If a single-line is passed in, a single line is returned
        if(count($output)) {
            return $output[0];
        }

        return $output;
    }

    private function justify(JustifierLine $line, $width) {
        // Retrieve already calculated line information
        $words     = $line->extractWords();
        $spaces    = $line->countWords() - 1;
        $wordLens  = $line->findWordLengths();
        $wordsLen  = $line->calculateWordsLength();
        $minWidth  = $wordsLen + $spaces;
        $output    = '';

        if($minWidth > $width) {
            throw new LengthException('A minimum width of ' . $minWidth . ' was required, but a width of ' . $width . ' was given instead');
        }

        // No spaces means only one word (center align)
        if($spaces == 0) {
            return str_pad($words[0], $width, ' ', STR_PAD_BOTH);
        }

        for(;$spaces > 0; $spaces--) {
            // Add next word to output and subtract its length from counters
            $output   .= array_shift($words);
            $length    = array_shift($wordLens);
            $wordsLen -= $length;
            $width    -= $length;

            if($spaces == 1) { // Last Iteration
                return $output . str_repeat(' ', $width - $wordsLen) . $words[0];
            }

            // Magic padding is really just simple math
            $padding  = floor(($width - $wordsLen) / $spaces);
            $output  .= str_repeat(' ', $padding);
            $width   -= $padding;
        }
    }
}

class JustifierLine {
    private $words;
    private $numWords;
    private $wordLengths;
    private $wordsLength;

    public static function fromText($text) {
        // Split words into an array
        preg_match_all('/[^ ]+/', $text, $matches, PREG_PATTERN_ORDER);
        $words       = $matches[0];

        // Count words
        $numWords    = count($words);

        // Find the length of each word
        $wordLengths = array_map('strlen', $words);

        //And Finally, calculate the total length of all words
        $wordsLength = array_reduce($wordLengths, function($result, $length) {
            return $result + $length;
        }, 0);

        return new JustifierLine($words, $numWords, $wordLengths, $wordsLength);
    }

    private function __construct($words, $numWords, $wordLengths, $wordsLength) {
        $this->words       = $words;
        $this->numWords    = $numWords;
        $this->wordLengths = $wordLengths;
        $this->wordsLength = $wordsLength;
    }

    public function extractWords() { return $this->words; }
    public function countWords() { return $this->numWords; }
    public function findWordLengths() { return $this->wordLengths; }
    public function calculateWordsLength() { return $this->wordsLength; }

    public function breakAtColumn($column) {
        // Avoid extraneous processing if we can determine no breaking can be done
        if($column >= ($this->wordsLength + $this->numWords - 1)) {
            return null;
        }

        $width       = 0;
        $wordsLength = 0;

        for($i = 0; $i < $this->numWords; $i++) {
            // Add width of next word
            $width += $this->wordLengths[$i];

            // If the line is overflowing past required $width
            if($width > $column) {
                // Remove overflow at end & create a new object with the overflow
                $words             = array_splice($this->words, $i);
                $numWords          = $this->numWords - $i;
                $this->numWords    = $i;
                $wordLengths       = array_splice($this->wordLengths, $i);
                $tempWordsLength   = $wordsLength;
                $wordsLength       = $this->wordsLength - $wordsLength;
                $this->wordsLength = $tempWordsLength;

                return new JustifierLine($words, $numWords, $wordLengths, $wordsLength);
            }

            $width++; // Assuming smallest spacing to fit

            // We also have to keep track of the total $wordsLength
            $wordsLength += $this->wordLengths[$i];
        }

        return null;
    }
}
于 2012-06-16T14:14:56.283 に答える
4

これが私の解決策です。厄介な正規表現はありません:)

function justify($str, $length) {
  $words   = explode(' ', $str);
  if(count($words)==1) $words = array("", $str, "");
  $spaces  = $length - array_sum(array_map("strlen", $words));
  $add     = (int)($spaces / (count($words) - 1));
  $left    = $spaces % (count($words) - 1);
  $spaced  = implode(str_repeat("_", $add + 1), array_slice($words, 0, $left + 1));
  $spaced .= str_repeat("_", max(1, $add));
  $spaced .= implode(str_repeat("_", max(1, $add)), array_slice($words, $left + 1));
  return substr($spaced, 0, $length);
}

これは、PHP配列関数を利用しています。

これが実際の例です。

于 2012-06-16T08:45:47.097 に答える
3

Pythonでのリスト内包表記が恋しい...

<?php
function justify ($str, $len)
{
    // split by whitespace, remove empty strings
    $words = array_diff (preg_split ('/\s+/', $str), array (""));

    // just space if no words
    if (count ($words) == 0)
        return str_repeat (" ", $len);

    // add empty strings if only one element
    if (count ($words) == 1)
        $words = array ("", $words[0], "");

    // get number of words and spaces
    $wordcount = count ($words);
    $numspaces = $wordcount - 1;

    // get number of non-space characters
    $numchars = array_sum (array_map ("strlen", $words));

    // get number of characters remaining for space
    $remaining = $len - $numchars;

    // return if too little spaces remaining
    if ($remaining <= $numspaces)
        return substr (implode (" ", $words), 0, $len);

    // get number of spaces per space
    $spaces_per_space = $remaining / $numspaces;
    $spaces_leftover = $remaining % $numspaces;

    // make array for spaces, spread out leftover spaces
    $spaces = array_fill (0, $numspaces, $spaces_per_space);
    while ($spaces_leftover--)
        $spaces[$numspaces - $spaces_leftover - 1]++;
    $spaces[] = 0; // make count ($words) == count ($spaces)

    // join it all together
    $result = array ();
    foreach ($words as $k => $v)
        array_push ($result, $v, str_repeat (" ", $spaces[$k]));
    return implode ($result);
}
?>
于 2012-06-15T23:11:58.693 に答える
3

私が彼らに宿題をさせようとしていると誰も思わないように、これが私の(うまくいっていると思う)解決策です。

オンデマンドでこれほど多くのコードをホワイトボードに書くことを期待できたかどうかはわかりませんが、他の人が私のコードを見ずにどのように取り組むかを知りたいと思っています。(いわば「時間」と呼ばれる前に、インタビューでforeachの周りにたどり着きました)

function justify($str, $to_len) {
    $str = trim($str);
    $strlen = strlen($str);

    if($str == '') return '';

    if($strlen >= $to_len) {
        return substr($str, 0, $to_len);   
    }

    $words = explode(' ', $str);
    $word_count = count($words);
    $space_count = $word_count - 1;

    if($word_count == 1) {
        return str_pad($str, $to_len, ' ', STR_PAD_BOTH);
    }

    $space = $to_len - $strlen + $space_count;
    $per_space = $space/$space_count;

    if(is_int($per_space)) {
        return implode($words, str_pad('', $per_space, ' '));    
    }

    $new_str = '';
    $spacing = floor($per_space);
    $new_str .= $words[0] . str_pad('', $spacing);
    foreach($words as $x => $word) {
        if($x == $word_count - 1 || $x == 0) continue;
        if($x < $word_count - 1) {
            $diff = $to_len - strlen($new_str) - (strlen(implode('', array_slice($words, $x))));
            $new_str .= $word . str_pad('', floor($diff/($space_count - $x)), ' ');
        }
    }
    $new_str .= $words[$x];

    return $new_str;   
}

$tests = array(' hello world there ok then ', 'hello', 'ok then', 'this string is almost certainly longer than 48 I think', 'two words', 'three ok words', '1 2 3 4 5 6 7 8 9');

foreach($tests as $word) {
    print $word . ' = ' . str_replace(' ', '_', justify($word, 48)) . '<br>';
}
于 2012-06-15T21:34:48.373 に答える
2

(セミロング)ソリューション

完成するまでにはしばらく時間がかかりましたが(おそらく、インタビュアーが許可したよりもはるかに長い時間でした)、この問題に対するエレガントな162行のOOPソリューションを考え出しました。単一の文字列、文字列の配列(すでに行に分割されている)、または最初に最大幅の行に分割する必要がある長い文字列を揃えることができる機能を含めました。デモはコードブロックに従います。

重要な注意:このクラスはPHP5.4でのみ機能します。XDebugでプロファイリング統計を取得するために自分のサーバーPHP(5.3.6)でバージョンを実行しているときに、これに気づきました。$thisPHP 5.3は、無名関数での使用について文句を言います。匿名関数に関するドキュメントを簡単にチェックすると、$this5.4まで匿名関数のコンテキストで使用できなかったことがわかります。誰かがこれに対するクリーンな回避策を見つけることができるなら、コメントにそれをドロップしてください。 PHP 5.3のサポートが追加されました!

<?php
class Justifier {
    private $text;

    public function __construct($text) {
        if(!is_string($text) && !is_array($text)) {
            throw new InvalidArgumentException('Expected a string or an array of strings, instead received type: ' . gettype($text));
        }

        if(is_array($text)) {
            // String arrays must be converted to JustifierLine arrays
            $this->text = array_map(function($line) {
                return JustifierLine::fromText($line);
            }, $text);
        } else {
            // Single line of text input
            $this->text = $text;
        }
    }

    public function format($width = null) {
        // Strings have to be broken into an array and then jusitifed
        if(is_string($this->text)) {
            if($width == null) {
                throw new InvalidArgumentException('A width must be provided for separation when an un-split string is provided');
            }

            if($width <= 0) {
                throw new InvalidArgumentException('Expected a positive, non-zero width, instead received width of ' . $width);
            }

            // Break up a JustifierLine of all text until each piece is smaller or equal to $width
            $lines = array(JustifierLine::fromText($this->text));
            $count = 0;
            $newLine = $lines[0]->breakAtColumn($width);

            while($newLine !== null) {
                $lines[] = $newLine;
                $newLine = $lines[++$count]->breakAtColumn($width);
            }
        } else {
            $lines = $this->text;

            // Allow for fluid width (uses longest line with single space)
            if($width == NULL) {
                $width = -1;

                foreach($lines as $line) {
                    // Width of line = Sum of the lengths of the words and the spaces (number of words - 1)
                    $newWidth = $line->calculateWordsLength() + $line->countWords() - 1;

                    if($newWidth > $width) { // Looking for the longest line
                        $width = $newWidth;
                    }
                }
            }
        }

        // Justify each element of array (PHP 5.4 ONLY)
        //$output = array_map(function($line) use ($width) {
        //  return $this->justify($line, $width);
        //}, $lines);

                    // Support for PHP 5.3
                    $output = array();
                    foreach($lines as $line) {
                        $output = $this->justify($line, $width);
                    }

        // If a single-line is passed in, a single line is returned
        if(count($output)) {
            return $output[0];
        }

        return $output;
    }

    private function justify(JustifierLine $line, $width) {
        // Retrieve already calculated line information
        $words     = $line->extractWords();
        $spaces    = $line->countWords() - 1;
        $wordLens  = $line->findWordLengths();
        $wordsLen  = $line->calculateWordsLength();
        $minWidth  = $wordsLen + $spaces;
        $output    = '';

        if($minWidth > $width) {
            throw new LengthException('A minimum width of ' . $minWidth . ' was required, but a width of ' . $width . ' was given instead');
        }

        // No spaces means only one word (center align)
        if($spaces == 0) {
            return str_pad($words[0], $width, ' ', STR_PAD_BOTH);
        }

        for(;$spaces > 0; $spaces--) {
            // Add next word to output and subtract its length from counters
            $output   .= array_shift($words);
            $length    = array_shift($wordLens);
            $wordsLen -= $length;
            $width    -= $length;

            if($spaces == 1) { // Last Iteration
                return $output . str_repeat(' ', $width - $wordsLen) . $words[0];
            }

            // Magic padding is really just simple math
            $padding  = floor(($width - $wordsLen) / $spaces);
            $output  .= str_repeat(' ', $padding);
            $width   -= $padding;
        }
    }
}

class JustifierLine {
    private $words;
    private $numWords;
    private $wordLengths;
    private $wordsLength;

    public static function fromText($text) {
        // Split words into an array
        preg_match_all('/[^ ]+/', $text, $matches, PREG_PATTERN_ORDER);
        $words       = $matches[0];

        // Count words
        $numWords    = count($words);

        // Find the length of each word
        $wordLengths = array_map('strlen', $words);

        //And Finally, calculate the total length of all words
        $wordsLength = array_reduce($wordLengths, function($result, $length) {
            return $result + $length;
        }, 0);

        return new JustifierLine($words, $numWords, $wordLengths, $wordsLength);
    }

    private function __construct($words, $numWords, $wordLengths, $wordsLength) {
        $this->words       = $words;
        $this->numWords    = $numWords;
        $this->wordLengths = $wordLengths;
        $this->wordsLength = $wordsLength;
    }

    public function extractWords() { return $this->words; }
    public function countWords() { return $this->numWords; }
    public function findWordLengths() { return $this->wordLengths; }
    public function calculateWordsLength() { return $this->wordsLength; }

    public function breakAtColumn($column) {
        // Avoid extraneous processing if we can determine no breaking can be done
        if($column >= ($this->wordsLength + $this->numWords - 1)) {
            return null;
        }

        $width       = 0;
        $wordsLength = 0;

        for($i = 0; $i < $this->numWords; $i++) {
            // Add width of next word
            $width += $this->wordLengths[$i];

            // If the line is overflowing past required $width
            if($width > $column) {
                // Remove overflow at end & create a new object with the overflow
                $words             = array_splice($this->words, $i);
                $numWords          = $this->numWords - $i;
                $this->numWords    = $i;
                $wordLengths       = array_splice($this->wordLengths, $i);
                $tempWordsLength   = $wordsLength;
                $wordsLength       = $this->wordsLength - $wordsLength;
                $this->wordsLength = $tempWordsLength;

                return new JustifierLine($words, $numWords, $wordLengths, $wordsLength);
            }

            $width++; // Assuming smallest spacing to fit

            // We also have to keep track of the total $wordsLength
            $wordsLength += $this->wordLengths[$i];
        }

        return null;
    }
}

デモ

元の質問(テキストの行を幅= 48に揃える)

多くの文字列の配列または1つの文字列のみをに渡すことができますJustifier。呼び出しJustifier::format($desired_length)ますいつも正当化された行の配列を返します*セグメンテーションを必要とする文字列または文字列の配列がコンストラクターに渡された場合。それ以外の場合は、文字列が返されます。コードパッドデモ

$jus = new Justifier(array(
    'hello world there ok then',
    'hello',
    'ok then',
    'two words',
    'three ok words',
    '1 2 3 4 5 6 7 8 9'
));

print_r( $jus->format(48) );

出力

Array
(
    [0] => hello      world       there       ok       then
    [1] =>                      hello                      
    [2] => ok                                          then
    [3] => two                                        words
    [4] => three                  ok                  words
    [5] => 1    2     3     4     5     6     7     8     9
)

OPのテストラインの1つを省略したことに気付くかもしれません。これは、54文字であり、に$desired_length渡された値を超えるためJustifier::format()です。IllegalArgumentExceptionこの関数は、正ではない幅、最小幅以上のゼロ以外の数値をスローします。最小幅は、(コンストラクターに渡されるすべての行の中で)最も長い行を単一の間隔で見つけることによって計算されます。

文字列の配列で位置合わせする流体幅

幅を省略するJustifierと、シングルスペースの場合に(コンストラクターに渡されたものの)最長の行の幅が使用されます。これは、前のデモで最小幅を見つけるのと同じ計算です。(コードパッドデモ

$jus = new Justifier(array(
    'hello world there ok then',
    'hello',
    'ok then',
    'this string is almost certainly longer than 48 I think',
    'two words',
    'three ok words',
    '1 2 3 4 5 6 7 8 9'
));

print_r( $jus->format() );

出力

Array
(
    [0] => hello        world        there        ok         then
    [1] =>                         hello                         
    [2] => ok                                                then
    [3] => this string is almost certainly longer than 48 I think
    [4] => two                                              words
    [5] => three                     ok                     words
    [6] => 1     2     3     4      5      6      7      8      9
)

テキストの単一の文字列を正当化する(幅= 48)

また、クラスには、壊れていない単一の文字列をコンストラクターに渡すことができる機能が含まれています。この文字列は任意の長さにすることができます。呼び出すとJustifier::format($desired_length)、文字列は行に分割され、各行が可能な限り多くのテキストで埋められ、新しい行を開始する前に両端揃えになります。InvalidArgumentException文字列を分割できる幅を指定する必要があるため、クラスはに文句を言います。誰かが賢明なデフォルトまたは文字列のデフォルトをプログラムで決定する方法を考えることができれば、私は提案を完全に受け入れます。(コードパッドデモ

$jus = new Justifier(
    'hello world there ok then hello ok then this string is almost certainly longer than 48 I think two words three ok words 1 2 3 4 5 6 7 8 9'
);

print_r( $jus->format(48) );

出力

Array
(
    [0] => hello world there ok then  hello  ok  then  this
    [1] => string is almost  certainly  longer  than  48  I
    [2] => think two words three ok words 1 2 3 4 5 6 7 8 9
)
于 2012-06-16T02:40:33.493 に答える
2

これが私の試みです。

function justify($str_in, $desired_length)
{
    foreach ($str_in as &$line) {
        $words = explode(' ', $line);
        $word_count = count($words) - 1;
        $spaces_to_fill = $desired_length - strlen($line) + $word_count;
        if (count($words) == 1) {
            $line = str_repeat('_', ceil($spaces_to_fill/2)) . $line
                  . str_repeat('_', floor($spaces_to_fill/2));
            continue;
        }
        $next_space = floor($spaces_to_fill/$word_count);
        $leftover_space = $spaces_to_fill % $word_count;
        $line = array_shift($words);
        foreach($words as $word) {
            $extra_space = ($leftover_space) ? ceil($leftover_space / $word_count) : 0;
            $leftover_space -= $extra_space;
            $line .= str_repeat('_', $next_space + $extra_space) . $word;
        }
    }
    return $str_in;
}

比較的簡潔にしようとしたため、読みやすさに影響がありました。しかし、これがどのように機能するかです:

エントリごとに、単語を配列に分割します$words。単語の前後にスペースが必要な場合があるため、配列の先頭と末尾に空の文字列も追加します。

スペースの残りの量$leftover_space(つまり、どこかに挿入する必要があるスペース)を計算し$word_count、それを単語数で割ります。これにより、各単語の間に挿入するスペースの平均数がわかります。

単語を追加するたびに、$extra_space残っている数に応じて、いくつかのスペースも追加します。その後、追加された金額を から削除し$leftover_spaceます。

サンプル出力

$data = justify($data, 48);
print_r($data);

Array
(
    [0] => 123456789012345678901234567890123456789012345678
    [1] => hello_______world_______there_______ok______then
    [2] => ______________________hello_____________________
    [3] => ok__________________________________________then
    [4] => this__string__is_almost_certainly_longer_than_48
    [5] => two________________________________________words
    [6] => three__________________ok__________________words
    [7] => 1_____2_____3_____4_____5_____6_____7_____8____9
)
于 2012-06-15T22:44:01.807 に答える
2

これは完全に機能していると思います:(「_」はスペースを表示したままにするだけです)

function justify($str_in, $desired_length)
{
   $str_in = preg_replace("!\s+!"," ",$str_in);   // get rid of multiple spaces
   $words = explode(" ",$str_in);   // break words
   $num_words = sizeof($words);     // num words   
   if ($num_words==1) {   
      return str_pad($str_in,$desired_length,"_",STR_PAD_BOTH);   
   }
   else {
      $num_chars = 0; $lenwords = array();
      for($x=0;$x<$num_words;$x++) { $num_chars += $lenwords[$x] = strlen($words[$x]); }
      $each_div = round(($desired_length - $num_chars) / ($num_words-1));
      for($x=0,$sum=0;$x<$num_words;$x++) { $sum += ($lenwords[$x] + ($x<$num_words-1 ? $each_div : 0)); }
      $space_to_addcut = ($desired_length - $sum);
      for($x=0;$x<$num_words-1;$x++) {
         $words[$x] .= str_repeat("_",$each_div+($each_div>1? ($space_to_addcut<0?-1:($space_to_addcut>0?1:0)) :0));
         if ($each_div>1) { $space_to_addcut += ($space_to_addcut<0 ? 1 : ($space_to_addcut>0?-1:0) ); } 
      }
      return substr(implode($words),0,$desired_length);
   }
}

編集:

関数は、単語間の複数のスペースも取り除くようになりました。仕組み(要するに):

  • 単語間の連続スペースを削除します
  • 単語を数えるので、1 つ ( 「hello」の例) の場合は両方をパディングしてエコーします。
  • ..それ以外の場合は、使用された単語の文字数を数えます
  • 追加するグローバルおよび部分スペースを計算します (例の「_」 )。
  • 追加する余分なスペースを計算し (文字列 len < 希望) または削除 (文字列 len > 希望) し、パディングに適用します。
  • final、最終的な文字列を目的の長さに減らします。

テスト:

$tests = array(
   'hello world there ok then',
   'hello',
   'ok then',
   'this string is almost certainly longer than 48 I think',
   'three ok words',
   '1 2 3 4 5 6 7 8 9',
   'Lorem Ipsum is simply dummy text'
);

$arr = array();
foreach($tests as $key=>$val) {
   $arr[$key] = justify($val,50);
   $arr[$key] .= " - (chars: ".strlen($arr[$key]).")";
}
echo "<pre>".print_r($arr,TRUE)."</pre>";

そして結果:

Array
(
    [0] => hello________world_______there_______ok_______then - (chars: 50)
    [1] => ______________________hello_______________________ - (chars: 50)
    [2] => ok____________________________________________then - (chars: 50)
    [3] => this_string_is_almost_certainly_longer_than_48_I_t - (chars: 50)
    [4] => three___________________ok___________________words - (chars: 50)
    [5] => 1______2_____3_____4_____5_____6_____7_____8_____9 - (chars: 50)
    [6] => Lorem____Ipsum____is_____simply_____dummy_____text - (chars: 50)
)

それは大変でした:)

編集2:

そのベンチマークが私に触れたので、機能は現在約20%高速です:)

于 2012-06-15T22:53:51.367 に答える
1

これが私の解決策です。それだけの価値はありますが、正当化機能とその受け入れテストの両方を作成するのに約 20 分かかりました。正当化機能のデバッグに 5 分間。また、インタビュー環境をある程度シミュレートするために、より堅牢な IDE の代わりに notpad++ を使用しました。

面接担当者が疑似コードで書くことを許可し、ボードに何を載せているかというあなたの思考プロセスにもっと関心がない限り、これはホワイトボード面接の質問には大きすぎる問題かもしれないと思います.

<?php


function justify($str_in, $desired_length) {
    $words = preg_split("/ +/",$str_in);
    // handle special cases
    if(count($words)==0) { return str_repeat(" ",$desired_length); }

    // turn single word case into a normal case
    if(count($words)==1) { $words = array("",$words[0],""); }

    $numwords = count($words);
    $wordlength = strlen(join("",$words));
    // handles cases where words are longer than the desired_length
    if($wordlength>($desired_length-$numwords)) { 
        return substr(join(" ",$words),0,$desired_length);
    }

    $minspace = floor(($desired_length-$wordlength)/($numwords-1));
    $extraspace = $desired_length - $wordlength - ($minspace * ($numwords-1));
    $result = $words[0];
    for($i=1;$i<$numwords;$i++) {
        if($extraspace>0) {
            $result.=" ";
            $extraspace--;
        }
        $result.=str_repeat(" ",$minspace);
        $result.=$words[$i];
    }
    return $result;
}

function acceptance_justify($orig_str, $just_str, $expected_length) {
    // should be the correct length
    if(strlen($just_str)!=$expected_length) { return false; }

    // should contain most of the words in the original string, in the right order
    if(preg_replace("/ +/","",substr($orig_str,0,$expected_length)) != preg_replace("/ +/","",substr($just_str,0,$expected_length))) { return false; }

    //spacing should be uniform (+/- 1 space)
    if(!preg_match("/( +)/",$just_str,$spaces)) { return false; }

    $space_length=strlen($spaces[0]);
    $smin=$space_length;
    $smax=$space_length;
    for($i=1;$i<count(@spaces);$i++) {
        $smin=min($smin,strlen($spaces));
        $smax=max($smax,strlen($spaces));
    }
    if(($smax-$smin)>1) { return false; }
    return true;
}

function run_test($str,$len) {
    print "<pre>";
    print "$str  ==> \n";
    $result = justify($str,$len);
    print preg_replace("/ /",".",$result) . "\n";
    print acceptance_justify($str,$result,$len)?"passed":"FAILED";
    print "\n\n</pre>";
}


run_test("hello world there ok then",48);
run_test("hello",48);
run_test("this string is almost certainly longer than 48 I think",48);
run_test("two words",48);
run_test("three ok words",48);
run_test("1 2 3 4 5 6 7 8 9",48);
于 2012-06-16T23:11:13.760 に答える
1

最後に、少し異なる実装を次に示します。

<?php
function justify($str, $to_len) {
    $str = trim($str);
    $strlen = strlen($str);

    if($str == '') return '';

    if($strlen >= $to_len) {
        return substr($str, 0, $to_len);   
    }

    $words = explode(' ', $str);
    $word_count = count($words);
    $space_count = $word_count - 1;

    if($word_count == 1) {
        return str_pad($str, $to_len, ' ', STR_PAD_BOTH);
    }

    $space = $to_len - $strlen + $space_count;
    $per_space = floor($space/$space_count);
    $spaces = str_pad('', $per_space, ' ');
    $curr_word = implode($words, $spaces);
    while(strlen($curr_word) < $to_len){
    $curr_word = substr($curr_word,0,preg_match("[! ][".$spaces."][! ]",$curr_word))." ".preg_match("[! ][".$spaces."][! ]",$curr_word));
    }
    return $curr_word;

?>

についてはよくわかりません。次のスペースではありませんregexp$spaces

于 2012-06-15T22:10:12.523 に答える