課題15: 再利用可能ライブラリの作成

課題概要

この課題では、実用的な文字列処理とバリデーションのライブラリを作成します。適切なパッケージ構造、可視性ルール、ドキュメント、テストを含む、プロダクションレベルのGoライブラリを実装します。

マンダトリー要件

要件1: プロジェクト構造の作成(15点)

標準的なプロジェクト構造を作成してください。

go mod init github.com/yourusername/textkit

ディレクトリ構造:

textkit/
├── go.mod
├── README.md
├── cmd/
│   └── textkit/
│       └── main.go          # デモアプリケーション
├── pkg/
│   ├── stringutil/
│   │   ├── stringutil.go    # 文字列ユーティリティ
│   │   └── stringutil_test.go
│   └── validator/
│       ├── validator.go     # バリデーション
│       └── validator_test.go
└── internal/
    └── helper/
        └── helper.go        # 内部ヘルパー関数

要件2: 文字列ユーティリティパッケージ(25点)

ファイル: pkg/stringutil/stringutil.go

// Package stringutil provides utility functions for string manipulation.
package stringutil

import (
    "strings"
    "unicode"
)

// Reverse reverses the given string.
// It correctly handles multi-byte UTF-8 characters.
//
// Example:
//   Reverse("hello") // returns "olleh"
//   Reverse("こんにちは") // returns "はちにんこ"
func Reverse(s string) string {
    runes := []rune(s)
    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
        runes[i], runes[j] = runes[j], runes[i]
    }
    return string(runes)
}

// IsPalindrome checks if the given string is a palindrome.
// The check is case-insensitive and ignores spaces.
//
// Example:
//   IsPalindrome("racecar") // returns true
//   IsPalindrome("A man a plan a canal Panama") // returns true
func IsPalindrome(s string) bool {
    // スペースを除去し、小文字に変換
    cleaned := removeSpaces(strings.ToLower(s))
    return cleaned == Reverse(cleaned)
}

// Title converts the first letter of each word to uppercase.
//
// Example:
//   Title("hello world") // returns "Hello World"
func Title(s string) string {
    return strings.Title(strings.ToLower(s))
}

// CamelCase converts a string to camelCase.
//
// Example:
//   CamelCase("hello world") // returns "helloWorld"
//   CamelCase("user_name") // returns "userName"
func CamelCase(s string) string {
    // スペースとアンダースコアで分割
    words := strings.FieldsFunc(s, func(r rune) bool {
        return r == ' ' || r == '_' || r == '-'
    })

    if len(words) == 0 {
        return ""
    }

    result := strings.ToLower(words[0])
    for i := 1; i < len(words); i++ {
        result += strings.Title(strings.ToLower(words[i]))
    }

    return result
}

// SnakeCase converts a string to snake_case.
//
// Example:
//   SnakeCase("HelloWorld") // returns "hello_world"
//   SnakeCase("userName") // returns "user_name"
func SnakeCase(s string) string {
    var result []rune

    for i, r := range s {
        if unicode.IsUpper(r) {
            if i > 0 {
                result = append(result, '_')
            }
            result = append(result, unicode.ToLower(r))
        } else {
            result = append(result, r)
        }
    }

    return string(result)
}

// Truncate truncates the string to the specified length.
// If the string is longer than maxLen, it adds "..." at the end.
//
// Example:
//   Truncate("Hello, World!", 8) // returns "Hello..."
func Truncate(s string, maxLen int) string {
    if len(s) <= maxLen {
        return s
    }

    if maxLen <= 3 {
        return s[:maxLen]
    }

    return s[:maxLen-3] + "..."
}

// WordCount returns the number of words in the string.
//
// Example:
//   WordCount("Hello World") // returns 2
func WordCount(s string) int {
    return len(strings.Fields(s))
}

// Contains checks if the string contains the substring (case-insensitive).
//
// Example:
//   Contains("Hello World", "world") // returns true
func Contains(s, substr string) bool {
    return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
}

// removeSpaces removes all spaces from the string.
func removeSpaces(s string) string {
    return strings.ReplaceAll(s, " ", "")
}

テストファイル: pkg/stringutil/stringutil_test.go

package stringutil

import "testing"

func TestReverse(t *testing.T) {
    tests := []struct {
        input    string
        expected string
    }{
        {"hello", "olleh"},
        {"Go", "oG"},
        {"こんにちは", "はちにんこ"},
        {"", ""},
    }

    for _, tt := range tests {
        result := Reverse(tt.input)
        if result != tt.expected {
            t.Errorf("Reverse(%q) = %q; want %q", tt.input, result, tt.expected)
        }
    }
}

func TestIsPalindrome(t *testing.T) {
    tests := []struct {
        input    string
        expected bool
    }{
        {"racecar", true},
        {"hello", false},
        {"A man a plan a canal Panama", true},
        {"", true},
    }

    for _, tt := range tests {
        result := IsPalindrome(tt.input)
        if result != tt.expected {
            t.Errorf("IsPalindrome(%q) = %v; want %v", tt.input, result, tt.expected)
        }
    }
}

func TestCamelCase(t *testing.T) {
    tests := []struct {
        input    string
        expected string
    }{
        {"hello world", "helloWorld"},
        {"user_name", "userName"},
        {"hello-world", "helloWorld"},
        {"", ""},
    }

    for _, tt := range tests {
        result := CamelCase(tt.input)
        if result != tt.expected {
            t.Errorf("CamelCase(%q) = %q; want %q", tt.input, result, tt.expected)
        }
    }
}

func TestSnakeCase(t *testing.T) {
    tests := []struct {
        input    string
        expected string
    }{
        {"HelloWorld", "hello_world"},
        {"userName", "user_name"},
        {"HTTPServer", "h_t_t_p_server"},
        {"", ""},
    }

    for _, tt := range tests {
        result := SnakeCase(tt.input)
        if result != tt.expected {
            t.Errorf("SnakeCase(%q) = %q; want %q", tt.input, result, tt.expected)
        }
    }
}

func TestTruncate(t *testing.T) {
    tests := []struct {
        input    string
        maxLen   int
        expected string
    }{
        {"Hello, World!", 8, "Hello..."},
        {"Hi", 10, "Hi"},
        {"Hello", 3, "Hel"},
        {"", 5, ""},
    }

    for _, tt := range tests {
        result := Truncate(tt.input, tt.maxLen)
        if result != tt.expected {
            t.Errorf("Truncate(%q, %d) = %q; want %q", tt.input, tt.maxLen, result, tt.expected)
        }
    }
}

実装すべき内容

  • Reverse: 文字列の反転(UTF-8対応)
  • IsPalindrome: 回文判定
  • Title: タイトルケース変換
  • CamelCase: キャメルケース変換
  • SnakeCase: スネークケース変換
  • Truncate: 文字列の切り詰め
  • WordCount: 単語数カウント
  • Contains: 大文字小文字を区別しない検索
  • 包括的なテスト

要件3: バリデーションパッケージ(30点)

ファイル: pkg/validator/validator.go

// Package validator provides validation functions for common data types.
package validator

import (
    "errors"
    "regexp"
    "strings"
    "unicode"
)

var (
    // ErrInvalidEmail is returned when an email is invalid.
    ErrInvalidEmail = errors.New("無効なメールアドレスです")

    // ErrInvalidURL is returned when a URL is invalid.
    ErrInvalidURL = errors.New("無効なURLです")

    // ErrPasswordTooShort is returned when a password is too short.
    ErrPasswordTooShort = errors.New("パスワードが短すぎます")

    // ErrPasswordTooWeak is returned when a password is too weak.
    ErrPasswordTooWeak = errors.New("パスワードが弱すぎます")
)

// emailRegex はメールアドレスの正規表現
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}

課題15: 再利用可能ライブラリの作成

課題概要

この課題では、実用的な文字列処理とバリデーションのライブラリを作成します。適切なパッケージ構造、可視性ルール、ドキュメント、テストを含む、プロダクションレベルのGoライブラリを実装します。

マンダトリー要件

要件1: プロジェクト構造の作成(15点)

標準的なプロジェクト構造を作成してください。

go mod init github.com/yourusername/textkit

ディレクトリ構造:

textkit/
├── go.mod
├── README.md
├── cmd/
│   └── textkit/
│       └── main.go          # デモアプリケーション
├── pkg/
│   ├── stringutil/
│   │   ├── stringutil.go    # 文字列ユーティリティ
│   │   └── stringutil_test.go
│   └── validator/
│       ├── validator.go     # バリデーション
│       └── validator_test.go
└── internal/
    └── helper/
        └── helper.go        # 内部ヘルパー関数

要件2: 文字列ユーティリティパッケージ(25点)

ファイル: pkg/stringutil/stringutil.go

// Package stringutil provides utility functions for string manipulation.
package stringutil

import (
    "strings"
    "unicode"
)

// Reverse reverses the given string.
// It correctly handles multi-byte UTF-8 characters.
//
// Example:
//   Reverse("hello") // returns "olleh"
//   Reverse("こんにちは") // returns "はちにんこ"
func Reverse(s string) string {
    runes := []rune(s)
    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
        runes[i], runes[j] = runes[j], runes[i]
    }
    return string(runes)
}

// IsPalindrome checks if the given string is a palindrome.
// The check is case-insensitive and ignores spaces.
//
// Example:
//   IsPalindrome("racecar") // returns true
//   IsPalindrome("A man a plan a canal Panama") // returns true
func IsPalindrome(s string) bool {
    // スペースを除去し、小文字に変換
    cleaned := removeSpaces(strings.ToLower(s))
    return cleaned == Reverse(cleaned)
}

// Title converts the first letter of each word to uppercase.
//
// Example:
//   Title("hello world") // returns "Hello World"
func Title(s string) string {
    return strings.Title(strings.ToLower(s))
}

// CamelCase converts a string to camelCase.
//
// Example:
//   CamelCase("hello world") // returns "helloWorld"
//   CamelCase("user_name") // returns "userName"
func CamelCase(s string) string {
    // スペースとアンダースコアで分割
    words := strings.FieldsFunc(s, func(r rune) bool {
        return r == ' ' || r == '_' || r == '-'
    })

    if len(words) == 0 {
        return ""
    }

    result := strings.ToLower(words[0])
    for i := 1; i < len(words); i++ {
        result += strings.Title(strings.ToLower(words[i]))
    }

    return result
}

// SnakeCase converts a string to snake_case.
//
// Example:
//   SnakeCase("HelloWorld") // returns "hello_world"
//   SnakeCase("userName") // returns "user_name"
func SnakeCase(s string) string {
    var result []rune

    for i, r := range s {
        if unicode.IsUpper(r) {
            if i > 0 {
                result = append(result, '_')
            }
            result = append(result, unicode.ToLower(r))
        } else {
            result = append(result, r)
        }
    }

    return string(result)
}

// Truncate truncates the string to the specified length.
// If the string is longer than maxLen, it adds "..." at the end.
//
// Example:
//   Truncate("Hello, World!", 8) // returns "Hello..."
func Truncate(s string, maxLen int) string {
    if len(s) <= maxLen {
        return s
    }

    if maxLen <= 3 {
        return s[:maxLen]
    }

    return s[:maxLen-3] + "..."
}

// WordCount returns the number of words in the string.
//
// Example:
//   WordCount("Hello World") // returns 2
func WordCount(s string) int {
    return len(strings.Fields(s))
}

// Contains checks if the string contains the substring (case-insensitive).
//
// Example:
//   Contains("Hello World", "world") // returns true
func Contains(s, substr string) bool {
    return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
}

// removeSpaces removes all spaces from the string.
func removeSpaces(s string) string {
    return strings.ReplaceAll(s, " ", "")
}

テストファイル: pkg/stringutil/stringutil_test.go

package stringutil

import "testing"

func TestReverse(t *testing.T) {
    tests := []struct {
        input    string
        expected string
    }{
        {"hello", "olleh"},
        {"Go", "oG"},
        {"こんにちは", "はちにんこ"},
        {"", ""},
    }

    for _, tt := range tests {
        result := Reverse(tt.input)
        if result != tt.expected {
            t.Errorf("Reverse(%q) = %q; want %q", tt.input, result, tt.expected)
        }
    }
}

func TestIsPalindrome(t *testing.T) {
    tests := []struct {
        input    string
        expected bool
    }{
        {"racecar", true},
        {"hello", false},
        {"A man a plan a canal Panama", true},
        {"", true},
    }

    for _, tt := range tests {
        result := IsPalindrome(tt.input)
        if result != tt.expected {
            t.Errorf("IsPalindrome(%q) = %v; want %v", tt.input, result, tt.expected)
        }
    }
}

func TestCamelCase(t *testing.T) {
    tests := []struct {
        input    string
        expected string
    }{
        {"hello world", "helloWorld"},
        {"user_name", "userName"},
        {"hello-world", "helloWorld"},
        {"", ""},
    }

    for _, tt := range tests {
        result := CamelCase(tt.input)
        if result != tt.expected {
            t.Errorf("CamelCase(%q) = %q; want %q", tt.input, result, tt.expected)
        }
    }
}

func TestSnakeCase(t *testing.T) {
    tests := []struct {
        input    string
        expected string
    }{
        {"HelloWorld", "hello_world"},
        {"userName", "user_name"},
        {"HTTPServer", "h_t_t_p_server"},
        {"", ""},
    }

    for _, tt := range tests {
        result := SnakeCase(tt.input)
        if result != tt.expected {
            t.Errorf("SnakeCase(%q) = %q; want %q", tt.input, result, tt.expected)
        }
    }
}

func TestTruncate(t *testing.T) {
    tests := []struct {
        input    string
        maxLen   int
        expected string
    }{
        {"Hello, World!", 8, "Hello..."},
        {"Hi", 10, "Hi"},
        {"Hello", 3, "Hel"},
        {"", 5, ""},
    }

    for _, tt := range tests {
        result := Truncate(tt.input, tt.maxLen)
        if result != tt.expected {
            t.Errorf("Truncate(%q, %d) = %q; want %q", tt.input, tt.maxLen, result, tt.expected)
        }
    }
}

実装すべき内容

  • Reverse: 文字列の反転(UTF-8対応)
  • IsPalindrome: 回文判定
  • Title: タイトルケース変換
  • CamelCase: キャメルケース変換
  • SnakeCase: スネークケース変換
  • Truncate: 文字列の切り詰め
  • WordCount: 単語数カウント
  • Contains: 大文字小文字を区別しない検索
  • 包括的なテスト

要件3: バリデーションパッケージ(30点)

ファイル: pkg/validator/validator.go

) // urlRegex はURLの正規表現 var urlRegex = regexp.MustCompile(`^https?://[a-zA-Z0-9.-]+(:[0-9]+)?(/.*)?

課題15: 再利用可能ライブラリの作成

課題概要

この課題では、実用的な文字列処理とバリデーションのライブラリを作成します。適切なパッケージ構造、可視性ルール、ドキュメント、テストを含む、プロダクションレベルのGoライブラリを実装します。

マンダトリー要件

要件1: プロジェクト構造の作成(15点)

標準的なプロジェクト構造を作成してください。

go mod init github.com/yourusername/textkit

ディレクトリ構造:

textkit/
├── go.mod
├── README.md
├── cmd/
│   └── textkit/
│       └── main.go          # デモアプリケーション
├── pkg/
│   ├── stringutil/
│   │   ├── stringutil.go    # 文字列ユーティリティ
│   │   └── stringutil_test.go
│   └── validator/
│       ├── validator.go     # バリデーション
│       └── validator_test.go
└── internal/
    └── helper/
        └── helper.go        # 内部ヘルパー関数

要件2: 文字列ユーティリティパッケージ(25点)

ファイル: pkg/stringutil/stringutil.go

// Package stringutil provides utility functions for string manipulation.
package stringutil

import (
    "strings"
    "unicode"
)

// Reverse reverses the given string.
// It correctly handles multi-byte UTF-8 characters.
//
// Example:
//   Reverse("hello") // returns "olleh"
//   Reverse("こんにちは") // returns "はちにんこ"
func Reverse(s string) string {
    runes := []rune(s)
    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
        runes[i], runes[j] = runes[j], runes[i]
    }
    return string(runes)
}

// IsPalindrome checks if the given string is a palindrome.
// The check is case-insensitive and ignores spaces.
//
// Example:
//   IsPalindrome("racecar") // returns true
//   IsPalindrome("A man a plan a canal Panama") // returns true
func IsPalindrome(s string) bool {
    // スペースを除去し、小文字に変換
    cleaned := removeSpaces(strings.ToLower(s))
    return cleaned == Reverse(cleaned)
}

// Title converts the first letter of each word to uppercase.
//
// Example:
//   Title("hello world") // returns "Hello World"
func Title(s string) string {
    return strings.Title(strings.ToLower(s))
}

// CamelCase converts a string to camelCase.
//
// Example:
//   CamelCase("hello world") // returns "helloWorld"
//   CamelCase("user_name") // returns "userName"
func CamelCase(s string) string {
    // スペースとアンダースコアで分割
    words := strings.FieldsFunc(s, func(r rune) bool {
        return r == ' ' || r == '_' || r == '-'
    })

    if len(words) == 0 {
        return ""
    }

    result := strings.ToLower(words[0])
    for i := 1; i < len(words); i++ {
        result += strings.Title(strings.ToLower(words[i]))
    }

    return result
}

// SnakeCase converts a string to snake_case.
//
// Example:
//   SnakeCase("HelloWorld") // returns "hello_world"
//   SnakeCase("userName") // returns "user_name"
func SnakeCase(s string) string {
    var result []rune

    for i, r := range s {
        if unicode.IsUpper(r) {
            if i > 0 {
                result = append(result, '_')
            }
            result = append(result, unicode.ToLower(r))
        } else {
            result = append(result, r)
        }
    }

    return string(result)
}

// Truncate truncates the string to the specified length.
// If the string is longer than maxLen, it adds "..." at the end.
//
// Example:
//   Truncate("Hello, World!", 8) // returns "Hello..."
func Truncate(s string, maxLen int) string {
    if len(s) <= maxLen {
        return s
    }

    if maxLen <= 3 {
        return s[:maxLen]
    }

    return s[:maxLen-3] + "..."
}

// WordCount returns the number of words in the string.
//
// Example:
//   WordCount("Hello World") // returns 2
func WordCount(s string) int {
    return len(strings.Fields(s))
}

// Contains checks if the string contains the substring (case-insensitive).
//
// Example:
//   Contains("Hello World", "world") // returns true
func Contains(s, substr string) bool {
    return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
}

// removeSpaces removes all spaces from the string.
func removeSpaces(s string) string {
    return strings.ReplaceAll(s, " ", "")
}

テストファイル: pkg/stringutil/stringutil_test.go

package stringutil

import "testing"

func TestReverse(t *testing.T) {
    tests := []struct {
        input    string
        expected string
    }{
        {"hello", "olleh"},
        {"Go", "oG"},
        {"こんにちは", "はちにんこ"},
        {"", ""},
    }

    for _, tt := range tests {
        result := Reverse(tt.input)
        if result != tt.expected {
            t.Errorf("Reverse(%q) = %q; want %q", tt.input, result, tt.expected)
        }
    }
}

func TestIsPalindrome(t *testing.T) {
    tests := []struct {
        input    string
        expected bool
    }{
        {"racecar", true},
        {"hello", false},
        {"A man a plan a canal Panama", true},
        {"", true},
    }

    for _, tt := range tests {
        result := IsPalindrome(tt.input)
        if result != tt.expected {
            t.Errorf("IsPalindrome(%q) = %v; want %v", tt.input, result, tt.expected)
        }
    }
}

func TestCamelCase(t *testing.T) {
    tests := []struct {
        input    string
        expected string
    }{
        {"hello world", "helloWorld"},
        {"user_name", "userName"},
        {"hello-world", "helloWorld"},
        {"", ""},
    }

    for _, tt := range tests {
        result := CamelCase(tt.input)
        if result != tt.expected {
            t.Errorf("CamelCase(%q) = %q; want %q", tt.input, result, tt.expected)
        }
    }
}

func TestSnakeCase(t *testing.T) {
    tests := []struct {
        input    string
        expected string
    }{
        {"HelloWorld", "hello_world"},
        {"userName", "user_name"},
        {"HTTPServer", "h_t_t_p_server"},
        {"", ""},
    }

    for _, tt := range tests {
        result := SnakeCase(tt.input)
        if result != tt.expected {
            t.Errorf("SnakeCase(%q) = %q; want %q", tt.input, result, tt.expected)
        }
    }
}

func TestTruncate(t *testing.T) {
    tests := []struct {
        input    string
        maxLen   int
        expected string
    }{
        {"Hello, World!", 8, "Hello..."},
        {"Hi", 10, "Hi"},
        {"Hello", 3, "Hel"},
        {"", 5, ""},
    }

    for _, tt := range tests {
        result := Truncate(tt.input, tt.maxLen)
        if result != tt.expected {
            t.Errorf("Truncate(%q, %d) = %q; want %q", tt.input, tt.maxLen, result, tt.expected)
        }
    }
}

実装すべき内容

  • Reverse: 文字列の反転(UTF-8対応)
  • IsPalindrome: 回文判定
  • Title: タイトルケース変換
  • CamelCase: キャメルケース変換
  • SnakeCase: スネークケース変換
  • Truncate: 文字列の切り詰め
  • WordCount: 単語数カウント
  • Contains: 大文字小文字を区別しない検索
  • 包括的なテスト

要件3: バリデーションパッケージ(30点)

ファイル: pkg/validator/validator.go

) // ValidateEmail validates an email address. // // Returns ErrInvalidEmail if the email is invalid. // // Example: // ValidateEmail("user@example.com") // returns nil // ValidateEmail("invalid") // returns ErrInvalidEmail func ValidateEmail(email string) error { if !emailRegex.MatchString(email) { return ErrInvalidEmail } return nil } // ValidateURL validates a URL. // // Returns ErrInvalidURL if the URL is invalid. // // Example: // ValidateURL("https://example.com") // returns nil // ValidateURL("invalid") // returns ErrInvalidURL func ValidateURL(url string) error { if !urlRegex.MatchString(url) { return ErrInvalidURL } return nil } // PasswordStrength represents the strength of a password. type PasswordStrength int const ( // Weak password Weak PasswordStrength = iota // Medium password Medium // Strong password Strong ) // ValidatePassword validates a password. // // Requirements: // - Minimum 8 characters // - At least one uppercase letter // - At least one lowercase letter // - At least one digit // // Returns ErrPasswordTooShort or ErrPasswordTooWeak if invalid. func ValidatePassword(password string) error { if len(password) < 8 { return ErrPasswordTooShort } var ( hasUpper bool hasLower bool hasDigit bool hasSpecial bool ) for _, r := range password { switch { case unicode.IsUpper(r): hasUpper = true case unicode.IsLower(r): hasLower = true case unicode.IsDigit(r): hasDigit = true case unicode.IsPunct(r) || unicode.IsSymbol(r): hasSpecial = true } } if !hasUpper || !hasLower || !hasDigit { return ErrPasswordTooWeak } return nil } // CheckPasswordStrength returns the strength of a password. func CheckPasswordStrength(password string) PasswordStrength { if len(password) < 8 { return Weak } var ( hasUpper bool hasLower bool hasDigit bool hasSpecial bool ) for _, r := range password { switch { case unicode.IsUpper(r): hasUpper = true case unicode.IsLower(r): hasLower = true case unicode.IsDigit(r): hasDigit = true case unicode.IsPunct(r) || unicode.IsSymbol(r): hasSpecial = true } } count := 0 if hasUpper { count++ } if hasLower { count++ } if hasDigit { count++ } if hasSpecial { count++ } if count >= 4 && len(password) >= 12 { return Strong } else if count >= 3 { return Medium } return Weak } // ValidateUsername validates a username. // // Requirements: // - 3-20 characters // - Only alphanumeric and underscores // - Must start with a letter func ValidateUsername(username string) error { if len(username) < 3 || len(username) > 20 { return errors.New("ユーザー名は3-20文字である必要があります") } if !unicode.IsLetter(rune(username[0])) { return errors.New("ユーザー名は文字で始まる必要があります") } for _, r := range username { if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '_' { return errors.New("ユーザー名は英数字とアンダースコアのみ使用できます") } } return nil } // ValidatePhone validates a phone number. // // Accepts formats: 090-1234-5678, 09012345678 func ValidatePhone(phone string) error { // ハイフンを除去 cleaned := strings.ReplaceAll(phone, "-", "") if len(cleaned) != 10 && len(cleaned) != 11 { return errors.New("無効な電話番号です") } for _, r := range cleaned { if !unicode.IsDigit(r) { return errors.New("電話番号は数字のみ使用できます") } } return nil }

テストファイル: pkg/validator/validator_test.go

package validator

import (
    "testing"
)

func TestValidateEmail(t *testing.T) {
    tests := []struct {
        input   string
        isValid bool
    }{
        {"user@example.com", true},
        {"test.user@example.co.jp", true},
        {"invalid", false},
        {"@example.com", false},
        {"user@", false},
    }

    for _, tt := range tests {
        err := ValidateEmail(tt.input)
        if (err == nil) != tt.isValid {
            t.Errorf("ValidateEmail(%q) error = %v; want valid = %v", tt.input, err, tt.isValid)
        }
    }
}

func TestValidatePassword(t *testing.T) {
    tests := []struct {
        input   string
        isValid bool
    }{
        {"Password123", true},
        {"Pass1", false},        // too short
        {"password123", false},  // no uppercase
        {"PASSWORD123", false},  // no lowercase
        {"Password", false},     // no digit
    }

    for _, tt := range tests {
        err := ValidatePassword(tt.input)
        if (err == nil) != tt.isValid {
            t.Errorf("ValidatePassword(%q) error = %v; want valid = %v", tt.input, err, tt.isValid)
        }
    }
}

func TestCheckPasswordStrength(t *testing.T) {
    tests := []struct {
        input    string
        expected PasswordStrength
    }{
        {"Pass1", Weak},
        {"Password123", Medium},
        {"P@ssw0rd123!", Strong},
    }

    for _, tt := range tests {
        result := CheckPasswordStrength(tt.input)
        if result != tt.expected {
            t.Errorf("CheckPasswordStrength(%q) = %v; want %v", tt.input, result, tt.expected)
        }
    }
}

func TestValidateUsername(t *testing.T) {
    tests := []struct {
        input   string
        isValid bool
    }{
        {"user123", true},
        {"ab", false},           // too short
        {"123user", false},      // starts with digit
        {"user@name", false},    // invalid character
        {"valid_user", true},
    }

    for _, tt := range tests {
        err := ValidateUsername(tt.input)
        if (err == nil) != tt.isValid {
            t.Errorf("ValidateUsername(%q) error = %v; want valid = %v", tt.input, err, tt.isValid)
        }
    }
}

実装すべき内容

  • ValidateEmail: メールアドレス検証
  • ValidateURL: URL検証
  • ValidatePassword: パスワード検証
  • CheckPasswordStrength: パスワード強度チェック
  • ValidateUsername: ユーザー名検証
  • ValidatePhone: 電話番号検証
  • 包括的なテスト

要件4: 内部ヘルパーパッケージ(10点)

ファイル: internal/helper/helper.go

// Package helper provides internal utility functions.
// This package is not accessible from outside the module.
package helper

import "unicode"

// IsAlphanumeric checks if all characters are alphanumeric.
func IsAlphanumeric(s string) bool {
    for _, r := range s {
        if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
            return false
        }
    }
    return true
}

// CountChars counts the number of characters by type.
func CountChars(s string) (letters, digits, others int) {
    for _, r := range s {
        switch {
        case unicode.IsLetter(r):
            letters++
        case unicode.IsDigit(r):
            digits++
        default:
            others++
        }
    }
    return
}

要件5: デモアプリケーション(20点)

ファイル: cmd/textkit/main.go

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"

    "github.com/yourusername/textkit/pkg/stringutil"
    "github.com/yourusername/textkit/pkg/validator"
)

func main() {
    fmt.Println("=== TextKit Demo ===")
    fmt.Println()

    demoStringUtil()
    fmt.Println()
    demoValidator()
}

func demoStringUtil() {
    fmt.Println("--- String Utilities ---")

    text := "hello world"
    fmt.Printf("Original: %s\n", text)
    fmt.Printf("Reversed: %s\n", stringutil.Reverse(text))
    fmt.Printf("Title: %s\n", stringutil.Title(text))
    fmt.Printf("CamelCase: %s\n", stringutil.CamelCase(text))
    fmt.Printf("Word Count: %d\n", stringutil.WordCount(text))

    palindrome := "racecar"
    fmt.Printf("\nIs '%s' a palindrome? %v\n", palindrome, stringutil.IsPalindrome(palindrome))

    long := "This is a very long string that needs truncation"
    fmt.Printf("\nTruncated: %s\n", stringutil.Truncate(long, 20))
}

func demoValidator() {
    fmt.Println("--- Validators ---")

    reader := bufio.NewReader(os.Stdin)

    // Email validation
    fmt.Print("\nメールアドレスを入力してください: ")
    email, _ := reader.ReadString('\n')
    email = strings.TrimSpace(email)

    if err := validator.ValidateEmail(email); err != nil {
        fmt.Printf("❌ %v\n", err)
    } else {
        fmt.Println("✅ 有効なメールアドレスです")
    }

    // Password validation
    fmt.Print("\nパスワードを入力してください: ")
    password, _ := reader.ReadString('\n')
    password = strings.TrimSpace(password)

    if err := validator.ValidatePassword(password); err != nil {
        fmt.Printf("❌ %v\n", err)
    } else {
        strength := validator.CheckPasswordStrength(password)
        strengthText := map[validator.PasswordStrength]string{
            validator.Weak:   "弱い",
            validator.Medium: "普通",
            validator.Strong: "強い",
        }
        fmt.Printf("✅ 有効なパスワードです (強度: %s)\n", strengthText[strength])
    }
}

実装すべき内容

  • ライブラリの機能をデモ
  • インタラクティブな入力
  • 結果の表示
  • 期待される出力

    $ go run cmd/textkit/main.go
    
    === TextKit Demo ===
    
    --- String Utilities ---
    Original: hello world
    Reversed: dlrow olleh
    Title: Hello World
    CamelCase: helloWorld
    Word Count: 2
    
    Is 'racecar' a palindrome? true
    
    Truncated: This is a very lo...
    
    --- Validators ---
    
    メールアドレスを入力してください: user@example.com
    ✅ 有効なメールアドレスです
    
    パスワードを入力してください: Password123!
    ✅ 有効なパスワードです (強度: 強い)
    

    ボーナス課題

    > ボーナス: これらはオプションです。マンダトリー部分が完了してから取り組んでください。

    ボーナス1: READMEとドキュメント(10点)

    包括的なREADME.mdを作成してください。

    # TextKit
    
    A comprehensive Go library for string manipulation and validation.
    
    ## Installation
    
    go get github.com/yourusername/textkit
    
    ## Usage
    
    ### String Utilities
    
    // ... examples
    
    ### Validators
    
    // ... examples
    
    ## API Documentation
    
    Visit [pkg.go.dev](https://pkg.go.dev/github.com/yourusername/textkit)
    
    ## Testing
    
    go test ./...
    
    ## License
    
    MIT
    

    ボーナス2: ベンチマーク(5点)

    パフォーマンステストを追加してください。

    func BenchmarkReverse(b *testing.B) {
        s := "hello world"
        for i := 0; i < b.N; i++ {
            Reverse(s)
        }
    }
    

    ボーナス3: CLI ツール(5点)

    コマンドラインツールを実装してください。

    $ textkit reverse "hello"
    olleh
    
    $ textkit validate-email user@example.com
    ✅ Valid email
    

    評価基準

    項目 配点 詳細
    プロジェクト構造 15点 適切なディレクトリ構成
    文字列ユーティリティ 25点 全機能が正しく動作する
    バリデーション 30点 包括的な検証機能
    内部ヘルパー 10点 internal/が適切に使われている
    デモアプリケーション 20点 ライブラリの使い方を示している
    **ボーナス1** 10点 詳細なドキュメント
    **ボーナス2** 5点 ベンチマークテスト
    **ボーナス3** 5点 CLIツール

    提出方法

    以下の構造で提出してください:

    textkit/
    ├── go.mod
    ├── go.sum (if any)
    ├── README.md
    ├── cmd/
    │   └── textkit/
    │       └── main.go
    ├── pkg/
    │   ├── stringutil/
    │   │   ├── stringutil.go
    │   │   └── stringutil_test.go
    │   └── validator/
    │       ├── validator.go
    │       └── validator_test.go
    └── internal/
        └── helper/
            └── helper.go
    

    ヒント

  • godoc: コメントは文で始め、関数名から始める
  • テスト: go test -v ./... で全テスト実行
  • カバレッジ: go test -cover ./... で確認
  • モジュール: go mod tidy で依存関係を整理
  • internal/: 他のプロジェクトから使えないことを確認
  • 学習リソース

  • Effective Go
  • Go Modules Reference
  • Package Names
  • Organizing Go Code