class ZXLib::Basic::Variable
Represents a ZX Spectrum's Basic
variable with various methods to create new variables, inspect their content or save as TAP files.
Attributes
The variable's data in its original format.
The dimension sizes as an array of integers. Only for array variables.
The variable's original name.
The type of a variable; one of VariableTypes
.
Public Class Methods
Creates a Basic::Variable
from a ZX Spectrum's VARS raw data.
Provide data
as a binary string.
# File lib/zxlib/basic.rb, line 1747 def from_data(data) raise ArgumentError unless String === data head, rest = data.unpack('Ca*') raise VariableParseError if head.nil? || rest.nil? || rest.empty? name = ((head & 0b00011111) | 0b01100000).chr type = head >> 5 case type when VAR_STRING len, = data.unpack('xv') raise VariableParseError if len.nil? || data.bytesize < len + 3 Variable.new type, name << '$', data.byteslice(0, len + 3) when VAR_NUMBER raise VariableParseError if data.bytesize < 6 Variable.new type, name, data.byteslice(0, 6) when VAR_NUMBER_EX data.byteslice(1..-1).each_byte do |c| name << (c & 0b01111111).chr break unless (c & 0b10000000).zero? end bsize = name.length + 5 raise VariableParseError if data.bytesize < bsize Variable.new type, name, data.byteslice(0, bsize) when VAR_NUMBER_ARRAY, VAR_CHAR_ARRAY bsize, ndims, rest = data.unpack('xvCa*') raise VariableParseError if bsize.nil? || ndims.nil? || rest.nil? || ndims.zero? || bsize <= ndims * 7 + 1 raise VariableParseError, "not enough array data" if rest.bytesize + 1 < bsize dims = rest.unpack("v#{ndims}") raise VariableParseError, "invalid array dimensions" if dims.any?(&:zero?) datasize = dims.inject(:*) if type == VAR_NUMBER_ARRAY datasize *= 5 else name << '$' end raise VariableParseError, "invalid array data" if bsize != datasize + ndims * 2 + 1 Variable.new type, name, data.byteslice(0, bsize + 3), dims.freeze when VAR_FOR_LOOP raise VariableParseError if data.bytesize < 19 Variable.new type, name, data.byteslice(0, 19) else raise VariableParseError end end
Creates a character array Basic::Variable
.
The strings
are parsed by Vars.program_text_to_string
only if encoded as UTF-8
.
dims
must be an array of dimension sizes provided as positive integers. values
if provided should be a nested array of strings of exact same dimensions as dims
.
# File lib/zxlib/basic.rb, line 1700 def new_char_array(name, dims, values=nil) raise TypeError unless String === name name.downcase! unless Tokenizer::Patterns::VARSTRNAME_MATCH_EXACT === name raise ArgumentError, "name must be a single alphabetic character followed by a $ sign" end raise ArgumentError, "`dims` must be an array of dimension sizes" unless dims.is_a?(Array) raise ArgumentError, "`dims` must not be empty" if dims.empty? raise ArgumentError, "number of dimensions must be less than or equal to 255" if dims.length > 255 unless dims.all? { |v| (1..65535) === v } raise ArgumentError, "dimension size must be at least 1" end head = (VAR_CHAR_ARRAY << 5)|(name.ord & 0b00011111) ndims = dims.length vsize = dims.inject(:*) bsize = vsize + ndims * 2 + 1 vpacked = if values.nil? ?\x20 * vsize else strsize = dims[-1] enumerate_deep_values(dims[0...-1], values).inject('') do |buf, str| str = Vars.program_text_to_string(str) if str.encoding.name == "UTF-8".freeze buf << str.byteslice(0, strsize).ljust(strsize) end end Variable.new VAR_CHAR_ARRAY, name, [head, bsize, ndims, *dims, vpacked].pack("CvCv#{ndims}a*"), dims.freeze end
Creates a FOR loop Basic::Variable
.
# File lib/zxlib/basic.rb, line 1652 def new_for_loop(name, value, limit, step, line, statement) raise TypeError unless String === name name.downcase! unless Tokenizer::Patterns::ALPHA_MATCH_EXACT === name raise ArgumentError, "name must be a single alphabetic character" end value, limit, step = [value, limit, step].map { |num| ZXLib::Math.pack_number num } head = (VAR_FOR_LOOP << 5)|(name.ord & 0b00011111) Variable.new VAR_FOR_LOOP, name, [head, value, limit, step, line, statement].pack('Ca5a5a5vC') end
Creates a numeric Basic::Variable
.
# File lib/zxlib/basic.rb, line 1617 def new_number(name, num, simplified_int=true) raise TypeError unless String === name name.downcase! unless Tokenizer::Patterns::VARNAME_MATCH_EXACT === name raise ArgumentError, "name must be composed of a single alphabetic character followed by alphabetic or numeric ones" end value = ZXLib::Math.pack_number num, simplified_int if name.length == 1 head = (VAR_NUMBER << 5)|(name.ord & 0b00011111) Variable.new VAR_NUMBER, name, [head, value].pack('Ca5') else head = ((VAR_NUMBER_EX << 5)|(name.ord & 0b00011111)).chr name[1...-1].each_byte do |code| head << (code & 0b01111111).chr end head << (name[-1].ord | 0b10000000).chr Variable.new VAR_NUMBER_EX, name, [head, value].pack('a*a5') end end
Creates a numeric array Basic::Variable
.
dims
must be an array of dimension sizes provided as positive integers. values
if provided should be a nested array of numbers of exact same dimensions as dims
.
# File lib/zxlib/basic.rb, line 1668 def new_number_array(name, dims, values=nil) raise TypeError unless String === name name.downcase! unless Tokenizer::Patterns::ALPHA_MATCH_EXACT === name raise ArgumentError, "name must be a single alphabetic character" end raise ArgumentError, "`dims` must be an array of dimension sizes" unless dims.is_a?(Array) raise ArgumentError, "`dims` must not be empty" if dims.empty? raise ArgumentError, "number of dimensions must be less than or equal to 255" if dims.length > 255 unless dims.all? { |v| (1..65535) === v } raise ArgumentError, "dimension size must be at least 1" end head = (VAR_NUMBER_ARRAY << 5)|(name.ord & 0b00011111) ndims = dims.length vsize = dims.inject(:*) bsize = vsize * 5 + ndims * 2 + 1 vpacked = if values.nil? ?\0 * (vsize * 5) else enumerate_deep_values(dims, values).inject('') do |buf, num| buf << ZXLib::Math.pack_number(num) end end Variable.new VAR_NUMBER_ARRAY, name, [head, bsize, ndims, *dims, vpacked].pack("CvCv#{ndims}a*"), dims.freeze end
Creates a string Basic::Variable
.
The string
is parsed by Vars.program_text_to_string
only if encoded as UTF-8
.
# File lib/zxlib/basic.rb, line 1640 def new_string(name, string) raise TypeError unless String === string && String === name name.downcase! unless Tokenizer::Patterns::VARSTRNAME_MATCH_EXACT === name raise ArgumentError, "name must be a single alphabetic character followed by a $ sign" end string = Vars.program_text_to_string(string) if string.encoding.name == "UTF-8".freeze head = (VAR_STRING << 5)|(name.ord & 0b00011111) Variable.new VAR_STRING, name, [head, string.bytesize, string].pack('Cva*') end
Public Instance Methods
Returns a selected portion of an array variable according to the provided dimension indices.
The indices start from 1 (not 0). Indices may be negative to indicate counting from the end. The number of indices should be equal or less than the number of dimensions. The last dimension index may be provided as a Range.
# File lib/zxlib/basic.rb, line 1506 def [](*at) if dims && at.length < dims.length last_at = 1..dims[-1] fun = ->(*args) { self.[](*args, last_at) } return dims[(at.length)...-1].reverse_each.inject(fun) { |fun, n| ->(*args) do 1.upto(n).map do |i| fun.call(*args, i) end end }.call(*at) end data = byteslice(*at) case type when VAR_NUMBER_ARRAY if Range === at.last 0.step(data.bytesize - 1, 5).map do |offs| ZXLib::Math.unpack_number data.byteslice(offs, 5), false end else ZXLib::Math.unpack_number data end when VAR_CHAR_ARRAY, VAR_STRING Vars.string_to_program_text data else raise "Variable is a scalar" end end
true
if variable is a number or character array
# File lib/zxlib/basic.rb, line 1426 def array? !dims.nil? end
Returns original size of this variable in bytes.
# File lib/zxlib/basic.rb, line 1453 def bytesize data.bytesize end
Returns a selected portion of an array variable according to provided dimension indices as raw bytes.
The indices start from 1 (not 0). Indices may be negative to indicate counting from the end. All dimension indices must be provided. The last dimension index may be passed as a Range.
# File lib/zxlib/basic.rb, line 1539 def byteslice(*at) if array? raise "Subscript wrong" if at.length != dims.length start = 4 + at.length * 2 offset = 0 size = 1 last_at = at.last if Range === last_at dlen = dims[-1] last_at = Range.new( *[last_at.begin, last_at.end].map { |x| x < 0 ? x + dlen + 1 : x }, last_at.exclude_end? ) size = last_at.size raise "Subscript wrong" unless (1..dlen) === last_at.begin + size - 1 at[-1] = last_at.begin end at.zip(dims).reverse_each.inject(1) do |dsiz, (x, dlen)| x += dlen + 1 if x < 0 raise "Subscript wrong" unless (1..dlen) === x offset += (x - 1) * dsiz dsiz * dlen end if type == VAR_NUMBER_ARRAY offset *= 5 data.byteslice(start + offset, 5*size) else data.byteslice(start + offset, size) end elsif string? return code if at.empty? raise "Subscript wrong" if at.length != 1 offs, = at len = length normalize = ->(x) do if x < 0 x + len else x - 1 end end if Range === offs offsets = [offs.begin, offs.end].map(&normalize) raise "Subscript wrong" unless offsets.all? { |x| (0...len) === x } offs = Range.new *offsets, offs.exclude_end? else offs = normalize.call(offs) raise "Subscript wrong" unless (0...len) === offs end code.byteslice(offs) end end
true
if variable is a character array
# File lib/zxlib/basic.rb, line 1430 def char_array? type == VAR_CHAR_ARRAY end
Returns a portion of data after the header.
# File lib/zxlib/basic.rb, line 1391 def code case type when VAR_NUMBER, VAR_NUMBER_EX data.byteslice(-5..-1) when VAR_FOR_LOOP data.byteslice(1, 18) else data.byteslice(3..-1) end end
true
if variable is a FOR loop variable
# File lib/zxlib/basic.rb, line 1438 def for_loop? type == VAR_FOR_LOOP end
Returns a header byte.
# File lib/zxlib/basic.rb, line 1386 def head data.ord end
For strings returns the original string length, for arrays a number of dimensions.
# File lib/zxlib/basic.rb, line 1443 def length case type when VAR_STRING bytesize - 3 when VAR_NUMBER_ARRAY, VAR_CHAR_ARRAY dims.length end end
Returns the FOR loop limit value.
# File lib/zxlib/basic.rb, line 1474 def limit if for_loop? ZXLib::Math.unpack_number data.byteslice(6, 5) end end
Returns the FOR loop line number.
# File lib/zxlib/basic.rb, line 1488 def line if for_loop? data.unpack('@16v')[0] end end
true
if variable is a number variable
# File lib/zxlib/basic.rb, line 1422 def number? type == VAR_NUMBER || type == VAR_NUMBER_EX end
true
if variable is a number array
# File lib/zxlib/basic.rb, line 1434 def number_array? type == VAR_NUMBER_ARRAY end
Returns the FOR loop execute statement number.
# File lib/zxlib/basic.rb, line 1495 def statement if for_loop? data.unpack('@18C')[0] end end
Returns the FOR loop step value.
# File lib/zxlib/basic.rb, line 1481 def step if for_loop? ZXLib::Math.unpack_number data.byteslice(11, 5) end end
true
if variable is a string variable
# File lib/zxlib/basic.rb, line 1418 def string? type == VAR_STRING end
Returns this variable in a BASIC-like text format.
# File lib/zxlib/basic.rb, line 1591 def to_s case type when VAR_STRING "LET #{name}=\"#{value}\"" when VAR_STRING, VAR_NUMBER, VAR_NUMBER_EX "LET #{name}=#{value}" when VAR_NUMBER_ARRAY, VAR_CHAR_ARRAY "DIM #{name}(#{dims.join ','})" when VAR_FOR_LOOP "FOR #{name}=#{value} TO #{limit} STEP #{step}, #{line}:#{statement}" else raise "unknown variable type" end end
Creates a Z80::TAP::HeaderBody
instance from Basic::Variable
.
This method is provided for the included Z80::TAP#to_tap
and Z80::TAP#save_tap
methods.
# File lib/zxlib/basic.rb, line 1377 def to_tap_chunk(name, org:nil) if array? Z80::TAP::HeaderBody.new_var_array(name, code, head) else Z80::TAP::HeaderBody.new_code(name, code, org || 0x5B00) end end
Returns a value of a variable.
-
A
Float
or an Integer for numbers (including FOR loops). -
A (possibly nested) array of values for array variables.
-
A string, suitable to be inserted as a program literal, for string variables.
# File lib/zxlib/basic.rb, line 1462 def value case type when VAR_STRING Vars.string_to_program_text code when VAR_NUMBER, VAR_NUMBER_EX, VAR_FOR_LOOP ZXLib::Math.unpack_number code else self.[]() end end