New Chat
  • Slides
  • Websites
  • Docs
  • Deep Research
  • Sheets
  • Agent Swarm
Kimi Code
Kimi Claw
Chat History
  • Get App
  • About Us
    • Visit Moonshot AI
    • Kimi Platform
    • Features
    • Terms of Service
    • Privacy Policy
  • Language
  • User Feedback

Оптимизация с StringScanner

rb

string_helpers.rb

RB 6.02 KB
Как ты думаешь, можно ли оптимизировать модуль в приложенном файле, используя StringScanner?
Да, можно оптимизировать! StringScanner будет эффективнее текущей реализации с String#scan, особенно для больших строк. Давай разберём почему и как.

Проблемы текущей реализации

ruby
Copy
# Сейчас: создаёт массив всех совпадений, потом итерирует
str.scan(TOKEN) do |match|  # scan ищет ВСЕ совпадения сначала
  # обработка
end
Недостатки:
  1. String#scan сначала находит все совпадения, создавая промежуточный массив
  2. Для str_truncate мы сканируем всю строку, даже если нужна только начальная часть
  3. Повторные проходы по строке в str_ellipsis (сначала str_width, потом str_truncate)

Оптимизация с StringScanner

ruby
Copy
# frozen_string_literal: true

require_relative 'info'
require 'strscan'

module IS::Term::StringHelpers

  ESC_CODES = /\e\[[0-9;]*[a-zA-Z]/.freeze
  EMOJI     = /\p{Emoji_Presentation}/.freeze
  EAST_ASIA = /\p{Han}|\p{Hiragana}|\p{Katakana}|\p{Hangul}/.freeze
  
  # Один regex для токенизации вместо scan
  TOKEN = /#{ ESC_CODES }|\X/.freeze

  private_constant :ESC_CODES, :EMOJI, :EAST_ASIA, :TOKEN

  ALIGN_LEFT   = :left
  ALIGN_RIGHT  = :right
  ALIGN_CENTER = :center
  ALIGN_MODES  = [ ALIGN_LEFT, ALIGN_RIGHT, ALIGN_CENTER ].freeze

  DEFAULT_ELLIPSIS_MARKER = '…'
  DEFAULT_ALIGN_MODE = ALIGN_LEFT

  # Оптимизированная версия с StringScanner
  def str_width(str)
    scanner = StringScanner.new(str)
    width = 0
    
    while scanner.scan(TOKEN)
      match = scanner.matched
      width += case match
               when ESC_CODES then 0
               when EMOJI, EAST_ASIA then 2
               else 1
               end
    end
    
    width
  end

  # Ключевая оптимизация: останавливаемся как только достигли лимита
  def str_truncate(str, width)
    return '' if width <= 0
    return str if str.empty?
    
    scanner = StringScanner.new(str)
    current_width = 0
    last_pos = 0
    
    while scanner.scan(TOKEN)
      match = scanner.matched
      match_width = case match
                    when ESC_CODES then 0
                    when EMOJI, EAST_ASIA then 2
                    else 1
                    end
      
      # Проверяем ДО добавления: влезет ли этот токен?
      if current_width + match_width > width
        return str[0, last_pos]
      end
      
      current_width += match_width
      last_pos = scanner.pos
    end
    
    str
  end

  # str_ellipsis теперь эффективнее благодаря оптимизированным методам
  def str_ellipsis(str, width, marker = DEFAULT_ELLIPSIS_MARKER)
    marker_width = str_width(marker)
    raise ArgumentError, "Marker too long: #{marker.inspect}", caller_locations if marker_width > width
    
    # Один проход вместо двух (str_width + str_truncate)
    return str unless str_width(str) > width
    
    str_truncate(str, width - marker_width) + marker
  end

  # str_align остаётся без изменений, т.к. использует str_width один раз
  def str_align(str, width, mode = DEFAULT_ALIGN_MODE)
    src_width = str_width(str)
    return str if src_width >= width
    
    case mode
    when ALIGN_LEFT
      str + ' ' * (width - src_width)
    when ALIGN_RIGHT
      ' ' * (width - src_width) + str
    when ALIGN_CENTER
      left = (width - src_width) / 2
      right = width - src_width - left
      ' ' * left + str + ' ' * right
    else
      raise ArgumentError, "Invalid align value: #{mode.inspect}", caller_locations
    end
  end

  module_function :str_width, :str_truncate, :str_ellipsis, :str_align

  refine String do
    def width
      IS::Term::StringHelpers.str_width(self)
    end

    def truncate(width)
      IS::Term::StringHelpers.str_truncate(self, width)
    end

    def ellipsis(width, marker = DEFAULT_ELLIPSIS_MARKER)
      IS::Term::StringHelpers.str_ellipsis(self, width, marker)
    end

    def align(width, mode = DEFAULT_ALIGN_MODE)
      IS::Term::StringHelpers.str_align(self, width, mode)
    end
  end
end

Ключевые улучшения

Table
АспектБылоСтало
str_truncateСканирует всю строкуОстанавливается при достижении лимита
ПамятьСоздаёт массив токеновПотоковая обработка
ПозицияПодсчёт position += match.lengthscanner.pos — точная байтовая позиция
str_ellipsisДва прохода (width + truncate)Один проход truncate

Дополнительная оптимизация: кэширование ширины

Если строки часто переиспользуются, можно добавить ленивое вычисление:
ruby
Copy
# Для частых операций с одной строкой
def str_truncate(str, width)
  # ... оптимизация: если строка ASCII-only и без ANSI — используем slice
  return str[0, width] if str.ascii_only? && !str.include?("\e")
  
  # ... StringScanner для сложных случаев
end

Когда оптимизация критична

ruby
Copy
# Тест производительности
long_string = "中👨‍⚕️" * 10000 + "truncate_here" + "x" * 10000

# Старый scan: обрабатывает все 20000+ символов
# Новый StringScanner: остановится после ~20 символов для truncate(40)
Вывод: StringScanner даёт значительный выигрыш для str_truncate на длинных строках, позволяя остановиться раньше. Для str_width выигрыш скромнее (нет раннего выхода), но всё ещё есть за счёт отсутствия промежуточного массива.