整数を表す文字列を受理するオートマトン

正規表現

^[-+]?[1-9][0-9]*$

でいいんじゃないかな。これに相当する状態遷移図は

でいいんじゃないかな。S2が受理状態。S3になったら直ちにエラーとして停止していい。これを元に作ってみたC#のクラスは

interface Checker
{
  /// <summary>このオブジェクトの状態を初期状態に戻す</summary>
  void Reset();

  /// <summary>1文字読む</summary>
  /// <param name="ch">読込む文字</param>
  void Next(char ch);

  /// <summary>受理状態かどうか調べる</summary>
  /// <returns>true:受理状態, false:受理状態ではない</returns>
  bool IsAcceptable();

  /// <summary>エラー状態かどうか調べる</summary>
  /// <remarks>エラー状態になった場合、それ以降の文字を読む必要が無い</remarks>
  /// <returns>true:エラー状態, false:エラー状態ではない</returns>
  bool IsError();
}
class IntChecker : Checker
{
  enum IntParsingState
  {
    /// <summary>状態0</summary>
    State0,
    /// <summary>状態1</summary>
    State1,
    /// <summary>状態2</summary>
    State2,
    /// <summary>状態3</summary>
    State3,
  }

  IntParsingState _state;

  public IntChecker()
  {
    Reset();
  }

  public void Reset()
  {
    _state = IntParsingState.State0;
  }

  public void Next(char ch)
  {
    switch (_state)
    {
      case IntParsingState.State0:
        if (ch == '-' ||
          ch == '+')
        {
          _state = IntParsingState.State1;
        }
        else if (ch == '1' ||
          ch == '2' ||
          ch == '3' ||
          ch == '4' ||
          ch == '5' ||
          ch == '6' ||
          ch == '7' ||
          ch == '8' ||
          ch == '9')
        {
          _state = IntParsingState.State2;
        }
        else
        {
          _state = IntParsingState.State3;
        }
        break;
      case IntParsingState.State1:
        if (ch == '1' ||
          ch == '2' ||
          ch == '3' ||
          ch == '4' ||
          ch == '5' ||
          ch == '6' ||
          ch == '7' ||
          ch == '8' ||
          ch == '9')
        {
          _state = IntParsingState.State2;
        }
        else
        {
          _state = IntParsingState.State3;
        }
        break;
      case IntParsingState.State2:
        if (ch == '0' ||
          ch == '1' ||
          ch == '2' ||
          ch == '3' ||
          ch == '4' ||
          ch == '5' ||
          ch == '6' ||
          ch == '7' ||
          ch == '8' ||
          ch == '9')
        {
          _state = IntParsingState.State2;
        }
        else
        {
          _state = IntParsingState.State3;
        }
        break;
      case IntParsingState.State3:
        _state = IntParsingState.State3;
        break;
    }
  }

  /// <summary>受理状態かどうかを判断する</summary>
  /// <returns></returns>
  public bool IsAcceptable()
  {
    return _state == IntParsingState.State2;
  }


  public bool IsError()
  {
    return _state == IntParsingState.State3;
  }
}

でいいんじゃないかな。これを

Checker checker = new IntChecker();
string str = "+123";
foreach (char ch in str)
{
  checker.Next(ch);
  if (checker.IsError())
  {
    break;
  }
}

if (checker.IsAcceptable()) {
  Console.WriteLine("整数だよ");
}
else
{
  Console.WriteLine("整数じゃないよ");
}

こんな感じで使ってみたりなんかしちゃって。
実数でも同じことしたい。実数を表す文字列の正規表現

^[-+]?([0-9]+(\.[0-9]*)?|\.[0-9]+)([eE][-+]?[0-9]+)?$

でよさげだけど、今のところ状態遷移図を描けてない。ちょっと考える。

数式の結果が更新されない問題の回避方法

セルの値が正しく表示されない問題「数式の結果が更新されない - チキン煮込みチーズミックス4辛」を書いたが、回避の方法を見つけた。
数式内で、次のようにシート名まで書くと、問題は起こらないようだ。理由は不明。

=concat(Sheet1!A2:A21)


やっぱり無理だった

一回うまくいった気がしたけど。勘違いだった。もしくは、本当に上手くいったけど、再現性がない?
とにかく、まだ解決しない。

Excelで列番号を英文字に変換する方法

Excelで列番号を英文字に変換する方法 - チキン煮込みチーズミックス4辛」は間違っていた。Excelの列を表すアルファベットは、A〜Zを使った26進数表現ではない。具体的には、次のようになっている。

桁数 列番号(0ベース*1 アルファベット*2 場合の数
1 0〜25 A〜Z 26^1通り
2 26〜701 AA〜ZZ 26^2通り
3 702〜18278 AAA〜ZZZ 26^3通り

また、上記のURLで紹介したアルゴリズムは、アルファベットが2桁までならば正しく計算できるようだ。アルファベットが3桁になるような列番号を入力すると、間違った値が得られる。ConvertToLetter(702)は"ZZ"で、ConvertToLetter(703)は"[A"となってしまう。

ということで、列番号を英文字に変換するVBA関数ConvertToLetter2を書いてみた。

Option Explicit

Function ConvertToLetter2(iCol As Integer) As String
    'Excelの列番号は1始まりなので、-1して0始まりにする
    ConvertToLetter2 = ConvertToLetter2_detail(iCol - 1)
End Function

Function ConvertToLetter2_detail(iCol As Integer) As String
    Dim nCases As Long
    Dim nCasesPrev As Long
    Dim i As Integer
    
    '何桁になるか調べる
    i = 1
    nCases = 0
    nCasesPrev = 0
    Do While True
        nCasesPrev = nCases
        nCases = SumOfPower(1, i, 26)
        If iCol < nCases Then
            Exit Do
        End If
        i = i + 1
    Loop

    'i桁のアルファベットであることが確定
    'i-1桁の最大値以下ではnCasesPrev通りの場合の数を表現できる
    Dim rest As Long
    rest = iCol - nCasesPrev
    
    Dim result As String
    result = ConvertRadix(rest)
    Dim act_length As Integer
    act_length = Len(result)
    
    'i桁にする
    Dim shortage As Integer
    shortage = i - act_length
    For i = 0 To shortage - 1
        result = "A" & result
    Next
        
    ConvertToLetter2_detail = result
End Function

Function SumOfPower(lowerLim As Integer, upperLim As Integer, base As Integer) As Long
    Dim result As Long
    result = 0
    Dim i As Integer
    For i = lowerLim To upperLim
        Dim tmp As Long
        tmp = base ^ i
        result = result + tmp
    Next
    SumOfPower = result
End Function

Function ConvertRadix(n10 As Long) As String
    Dim result As String
    Dim codeA As Integer
    Dim radix As Integer
    Dim quotient As Long
    
    codeA = Asc("A")
    radix = 26
    Dim r, q As Long
    quotient = n10
    Do
        q = quotient \ radix
        r = quotient - (q * radix)
        quotient = q

        If q > 0 Then
            result = Chr(codeA + r) & result
        End If
    Loop While q > 0
    result = Chr(codeA + r) & result

    ConvertRadix = result
End Function

Excel VBAの記事続いてるな...

*1:つまり、COLUMN関数で得られる値よりも1だけ小さい。

*2:ただし、自分が使用しているExcel2007ではXFD列までしか利用できない。

数式の結果が更新されない

次のような引数で指定された範囲をカンマ区切りで連結して返す関数concatを準備しといて、この関数を複数シートで使って、ブックをxlsm形式で保存した。
concatを使うセルはA1でconcatの引数はA2:A21。各シートで位置は揃えている。

Option Explicit
Function concat(rng As Range) As String
  Dim startX, startY, endX, endY As Long, strResult As String
  startX = rng.Columns.Column
  startY = rng.Rows.Row
  endX = startX + rng.Columns.Count - 1
  endY = startY + rng.Rows.Count - 1

  Dim currX As Integer
  For currX = startX To endX
    Dim currY As Integer
    For currY = startY To endY
      If Len(Cells(currY, currX)) > 0 Then
        If Len(strResult) < 1 Then
          strResult = Cells(currY, currX)
        Else
          strResult = strResult & "," & Cells(currY, currX)
        End If
      End If
    Next currY
  Next currX
  concat = strResult
End Function

つまり、保存する内容はこんな感じ。左からSheet1、Sheet2、Sheet3。

で、次にこのファイル開いたとき、

が出るけど、こいつでマクロを許可してやると、マクロを許可したときに表示していたページのconcatの結果が、その他の全ページconcatしているセルの値に表示されている。さっきのSheet2を選んでマクロ許可してやると、こんな感じになる。

この状態から各シートのA1セルを再計算(いったんセルを編集状態にして確定するなど)すると、ちゃんと正しい値になる。
なんなのこの挙動。最初から保存した内容で表示するにはどうしたら良いのだろう。ちなみに、計算方法は「自動」にしてる。
誰かおせーて。

追記2016/05/28

2重ループは重い処理だと勝手にExcelが判定するから再計算しないのかと考え、どうせ1列しか見ないので、ループをネストさせないようにしてみた。結果は同じだった。
64ビットのマシンだからかと考え、32ビットの別PCでも試してみたが、やはりこれもダメだった。
現象だけ見ると「エクセルで数式の結果が反映されない(自動計算になっているのに) -す- Excel(エクセル) | 教えて!goo」と似ているように思われるけど、この人も解決できていないっぽい。
かなり困るんだけどな...

デシリアライズではまった

次のサンプルコードのMyMapクラスのようなクラスをファイルにシリアライズしてデシリアライズすると、デシリアライズ用のコンストラクタの中で_entriesの要素が全部(null,null)になってしまう。entry.Item1とentry.Item2を使って処理をしていたので、実行時エラーになっていた。そしてその理由が分からず数週間悩んだ。
Testメソッドで、デシリアライズ済みのオブジェクトを操作するときは、_entriesには正しい値が入っているので、ちゃんとTryGetValueを実行できている。entry.Item1やentry.Item2に依存した何らかの初期化処理をしたい場合、どうしたら良いのかな?

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

namespace ConsoleApplication1
{
  [Serializable]
  class MyString : ISerializable
  {
    string _value;

    public MyString(string str)
    {
      _value = str;
    }

    protected MyString(SerializationInfo info, StreamingContext context)
    {
      _value = info.GetString("Value");
    }

    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
      info.AddValue("Value", _value);
    }

    public string Value
    {
      get { return _value; }
      set { _value = value; }
    }

    public override string ToString()
    {
      return Value;
    }
  }

  [Serializable]
  class MyMap : ISerializable
  {
    int _n;

    IList<Tuple<MyString, MyString>> _entries;

    public MyMap()
    {
      _n = 0;
      _entries = new List<Tuple<MyString, MyString>>();
    }

    protected MyMap(SerializationInfo info, StreamingContext context)
    {
      _n = info.GetInt32("N");
      Console.WriteLine(_n);

      _entries = (IList<Tuple<MyString, MyString>>)info.GetValue("Entries", typeof(IList<Tuple<MyString, MyString>>));
      foreach (var entry in _entries)
      {
        // なぜかすべてのTupleが(null, null)になっている。
        Console.WriteLine("{0} -> {1}", entry.Item1, entry.Item2);
      }
    }

    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
      info.AddValue("N", _n);
      info.AddValue("Entries", _entries);
    }

    public void Add(MyString val1, MyString val2)
    {
      Tuple<MyString, MyString> entry = Tuple.Create(val1, val2);
      _entries.Add(entry);
    }

    public bool TryGetValue(MyString key, out MyString result)
    {
      foreach (Tuple<MyString, MyString> entry in _entries)
      {
        if (entry.Item1.Value == key.Value)
        {
          result = entry.Item2;
          return true;
        }
      }
      result = null;
      return false;
    }

    public int N
    {
      get { return _n; }
      set { _n = value; }
    }
  }

  class Program
  {
    static MyString Key = new MyString("bar");

    // 【要修正】ファイル出力先
    const string FilePath = @"C:\Users\foobar\Desktop\a.out";

    static void Main(string[] args)
    {
      MyMap map = new MyMap();
      map.Add(new MyString("foo"), new MyString("hoge"));
      map.Add(new MyString("bar"), new MyString("fuga"));
      map.Add(new MyString("baz"), new MyString("piyo"));
      map.N = 42;

      // シリアライズ → デシリアライズ
      Test(map);
    }

    static void Test(MyMap obj)
    {
      MyString result1 = null;
      obj.TryGetValue(Key, out result1);
      System.Console.WriteLine(result1);

      Stream s = Serialize(obj);
      MyMap tmp = Deserialize();

      MyString result2 = null;
      tmp.TryGetValue(Key, out result2);
      System.Console.WriteLine(result2);
    }

    static Stream Serialize(MyMap obj)
    {
      Stream s = null;
      try
      {
        s = new FileStream(FilePath, FileMode.Create);
        BinaryFormatter bf = new BinaryFormatter();
        bf.Serialize(s, obj);
      }
      catch (Exception)
      {
        throw;
      }
      finally
      {
        if (s != null)
        {
          s.Close();
        }
      }

      return s;
    }

    static MyMap Deserialize()
    {
      MyMap ret = null;
      Stream s = null;
      try
      {
        s = new FileStream(FilePath, FileMode.Open);
        s.Position = 0;
        BinaryFormatter bf = new BinaryFormatter();
        ret = (MyMap)bf.Deserialize(s);
      }
      catch (Exception)
      {
        throw;
      }
      finally
      {
        if (s != null)
        {
          s.Close();
        }
      }

      return ret;
    }
  }
}