読者です 読者をやめる 読者になる 読者になる

CGRectFromString の実装を調べてみる

長いです。テストの内容なんてどうでも良いから早く実装を見せろと思った方は、まとめからどうぞ。

CGRect testRect = CGRectFromString(@"{{{0, 1}}, 2, {3, 4, 5}}");
CGRect testRect = CGRectFromString(@"{{{0, 1}}, 2, {3, 4}}");
CGRect testRect = CGRectFromString(@"{{{0, {1}}, 2, {{3, 4, 5}}}");

CGRect testRect = CGRectFromString(@"0123");
CGRect testRect = CGRectFromString(@"0, 1, 2, 3");
CGRect testRect = CGRectFromString(@"{0, 1, 2, 3}");
CGRect testRect = CGRectFromString(@"{{0, 1}, {2, 3}}");
CGRect testRect = CGRectFromString(@"{{{0, 1}}, {{2, 3}}}");

こんな感じのテストを

NSLog(@"%@",NSStringFromCGRect(testRect));

で出力してみた。

{{0, 0}, {0, 0}}

でない値が返ってきたのは

CGRect testRect = CGRectFromString(@"{{{0, 1}}, 2, {3, 4}}");
CGRect testRect = CGRectFromString(@"{{0, 1}, 2, {3, 4, 5}}");
CGRect testRect = CGRectFromString(@"{{{0, 1}}, 2, {3, 4, 5}}");
CGRect testRect = CGRectFromString(@"{{0, 1}, {2}}");

CGRect testRect = CGRectFromString(@"{{0, 1}, {2, 3}}");

これらです。
出力は次の通り。

{{0, 0}, {3, 4}}
{{0,1}, {3, 4}}
{{0, 0}, {3, 4}}
{{0, 1}, {0, 0}};
{{0, 1}, {2, 3}}

ここまでは、何かきっかけをつかむためのテストです。
これ以後、これらを参考にして進んで行きます。

定義から見て行くと、

/* Points. */

struct CGPoint {
  CGFloat x;
  CGFloat y;
};
typedef struct CGPoint CGPoint;

/* Sizes. */

struct CGSize {
  CGFloat width;
  CGFloat height;
};
typedef struct CGSize CGSize;


/* Rectangles. */

struct CGRect {
  CGPoint origin;
  CGSize size;
};
typedef struct CGRect CGRect;

これですね。
構造体の中に構造体を含んでいる形になっています。

なので、

CGRect testRect = CGRectFromString(@"{{0, 1}, {2, 3}}");

{{0, 1}, {2, 3}}

こう出力される事が、最も一般的ということ。


今から異端児の処理の仕方を見て行きます。

CGRect testRect = CGRectFromString(@"0123");
CGRect testRect = CGRectFromString(@"0, 1, 2, 3");
CGRect testRect = CGRectFromString(@"{0, 1, 2, 3}");

このあたりを見ると、{}が足りない物はうまく出力されません。

逆に

CGRect testRect = CGRectFromString(@"{{{0, 1}}, {{2, 3}}}");

多くてもだめなようです。

ただ、

CGRect testRect = CGRectFromString(@"{{0, 1}, {{2, 3}}}");
CGRect testRect = CGRectFromString(@"{{{0, 1}}, {2, 3}}");
CGRect testRect = CGRectFromString(@"{{0, 1}, {2}}");

こうすると出力が次のようになります

{{0, 1}, {0, 0}}
{{0, 0}, {2, 3}}
{{0, 1}, {0, 0}};

なんとなく分かってきました。
内部でCGPointFromStringとCGSizeFromStringを呼んでいるような気がします。

という事でCGPointFromStringの実装を調べてみます。

CGPoint testPoint = CGPointFromString(@"0, 1");

CGPoint testPoint = CGPointFromString(@"{{0, 1}}");
CGPoint testPoint = CGPointFromString(@"{2}");
CGPoint testPoint = CGPointFromString(@"{0, 1, 2}");
CGPoint testPoint = CGPointFromString(@"0, {1, 2, 3}");

出力は次の通り。

{0, 0}
{0, 0}
{0, 0}
{0, 1}
{1, 2}

予想通りですね。
{}の数が多くても少なくてもうまく出力されず、
要素の数が少なければすべて0に、多ければはじめから2つ目までで、生成しています。
CGRectFromStringで出力されたものと同じパターンが返ってきている事が確認できました。
内部でCGPointFromStringとCGSizeFromStringが呼ばれていると考えて間違いないと思います。

次に気になるのは

CGRect testRect = CGRectFromString(@"{{0, 1}, 2, {3, 4, 5}}");

これですね。

{{0, 1}, {3, 4}}

が返されます。
{3, 4}となっているのは内部でCGSizeFromStringが呼ばれていると考えれば解決です。
では

CGRect testRect = CGRectFromString(@"{0, {1, 2}, {3, 4}}");
CGRect testRect = CGRectFromString(@"{{0, 1}, {2, 3}, 4}");

これはどうなるかというと

{{0, 0}, {1, 2}}
{{0, 1}, {2, 3}}

になるようです。

最後に、{と}の数が合っていない場合はすべて{{0, 0}, {0, 0}}になります。

まとめ

1.{の存在を確認した場所から処理を始める
2.{と}が対応してなければすべて0
3.{}の数が無駄に多い場合すべて0
4.要素が少なければ0
5.要素が多ければ、始めから取って行く


ここからが本題です。
実装をしていきたいと思います。

スタックを使うとややこしくなりそうなので、別の方法で。
まずはCGPointFromStringの実装から。


1."{"と"}"の個数を取る。
2.両方とも1つではなかったらCGPointZeroを返す。
3."{"と"}"の位置を取る。(1番外側に決まっていると思うかもしれませんが、"0, {1, 2}" というようなパターンもあります)
4.その間の文字列が、 ”値, 値”の形式であると認識(そう思い込む)し、","で文字列を分ける。","がなければ0を返す。
5.分けた文字列を NSString の -(float)floatValue メソッドで数値に変換する。
6.CGPointの構造体に、6で分けたものの始めから2つを数値として代入。


流れはこんな感じかと。

4の分岐の「","がなければ0を返す」というところが微妙で、","で分けた後の配列の個数が2未満だったら。かもしれない。

まず個数を取得する関数。

NSUInteger countOfString(NSString *str, NSString *searchStr)
{
    NSUInteger count = 0;
    NSString *_str = str;
    
    while ([_str rangeOfString:searchStr].location != NSNotFound) {
        count++;
        _str = [_str substringFromIndex:[_str rangeOfString:searchStr].location + 1];
    }
    
    NSLog(@"count = %d",count);
    return count;
}

実装。

CGPoint CGPointFromString(NSString *str)
{
    NSUInteger leftBracketCount = countOfString(str, @"{");
    NSUInteger rightBracketCount = countOfString(str, @"}");

    if( !((leftBracketCount == 1) && (rightBracketCount == 1)) )  
   {
        return CGPointZero;
    }


    NSRange firstLeftBracket = [str rangeOfString:@"{"];
    NSRange mostRightBracket = [str rangeOfString:@"}"options:NSBackwardsSearch];


    NSRange subStringRange = NSMakeRange(firstLeftBracket.location + 1,
                                        mostRightBracket.location - firstLeftBracket.location - 1);
    str = [str substringWithRange:subStringRange];
    NSArray *valueArray = [str componentsSeparatedByString:@","];


    if(valueArray.count < 2)
    {
        return CGPointZero;
    }
    return CGPointMake([(NSString*)[valueArray objectAtIndex:0] floatValue] ,
                                   [(NSString*)[valueArray objectAtIndex:1] floatValue]);
}

できた。


はいやっとCGRectFromStringを実装していきます!

数値をどうやって分けて、どのタイミングでCGPointFromString等を使うかが問題。

テストしたものを眺めていた結果、

{{0, 1}, 2, {3, 4, 5}}
{0, {1, 2}, {3, 4}}
{{0, 1}, {2, 3}, 4}

",{"で分けるのが良さそう。

そして、CGRectの定義から、",{"は2つ以上あるべきではない。
なので、2つ目を見つけたら、それ以降の文字列は消去してしまおう。

(0.文字列から半角スペースを削除。@", {"と@",{"では違う文字列なので、面倒にならないよう先に削除しておく。)

1."{"と"}"の個数を取る。
2.どちらかが1個以下、または同じ個数存在しなかったらCGRectZeroを返す。
3.1番外側の"{"と"}"の位置を取る。
4.その間の文字列が、 ”CGPoint, CGSize”の形式であると認識(そう思い込む)する。
5.",{"を探し、それ以前をsubStringとして、切り出す。 ex) {0, 1}, {2, 3} → "{0,1}"と", {2, 3}"に分ける。
6.",{"を再び探し、もしなければ、5で切り取った残りを保存。あれば、それ以前を5と同様に保存。
7.5,6でCGPointとCGSizeそれぞれを形成するNSStringを取り出せた。
8.あとはCGPointFromStringとCGSizeFromStringを使うだけ。

CGRect CGRectFromString(NSString *str)
{
    NSString *cgPointString;
    NSString *cgSizeString;
    NSRange commaBracket;

    NSUInteger leftBracketCount = countOfString(str, @"{");
    NSUInteger rightBracketCount = countOfString(str, @"}");
    if((leftBracketCount <= 1) || (rightBracketCount <= 1) || (leftBracketCount != rightBracketCount))
    {
        return CGRectZero;
    }

    NSRange firstLeftBracket = [str rangeOfString:@"{"];
    NSRange mostRightBracket = [str rangeOfString:@"}"options:NSBackwardsSearch];
    
    NSRange subStringRange = NSMakeRange(firstLeftBracket.location + 1,
                                         mostRightBracket.location - firstLeftBracket.location - 1);
    
    str = [str substringWithRange:subStringRange];

    str = [str stringByReplacingOccurrencesOfString:@" " withString:@""];
    commaBracket = [str rangeOfString:@",{"];
    if(commaBracket.location == NSNotFound)
    {
            return CGRectZero;
    }
    else
    {
        cgPointString = [str substringToIndex:commaBracket.location];
    }

    str = [str substringFromIndex:commaBracket.location + 1];
    commaBracket = [str rangeOfString:@",{"];
    if(commaBracket.location == NSNotFound)
    {
        cgSizeString = str;
    }
    else
    {
        cgSizeString = [str substringToIndex:commaBracket.location];
    }
    
    CGPoint point = CGPointFromString(cgPointString);
    CGSize size = CGSizeFromString(cgSizeString);

    return CGRectMake(point.x, point.y, size.width, size.height);
}

こんな感じですか………


もう少し詰められそうですが、力つきました。
ただ、動きはこれで確かなはず(はず)です。



なぜこんな事をしていたかというと、plistからUIColorを読み込むときに、CGPointとかと同じように読み込めたら便利だと思いまして、UIColorFromStringメソッドを実装するためです。
実装したものは後で公開します。

まあCGPointの実装が出来た時点で、目的は達成できたのですが、個人的に定義していた構造体にfloatとCGPointを持つものがありました。それも同じようにplistから読み込みたいと思ったら、CGRectFromStringの実装をまねるのが良いかと思ったので。