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

data[R]

The variable's data in its original format.

dims[R]

The dimension sizes as an array of integers. Only for array variables.

name[R]

The variable's original name.

type[R]

The type of a variable; one of VariableTypes.

Public Class Methods

from_data(data) click to toggle source

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
new_char_array(name, dims, values=nil) click to toggle source

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
new_for_loop(name, value, limit, step, line, statement) click to toggle source

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
new_number(name, num, simplified_int=true) click to toggle source

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
new_number_array(name, dims, values=nil) click to toggle source

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
new_string(name, string) click to toggle source

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

[](*at) click to toggle source

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
array?() click to toggle source

true if variable is a number or character array

# File lib/zxlib/basic.rb, line 1426
def array?
        !dims.nil?
end
bytesize() click to toggle source

Returns original size of this variable in bytes.

# File lib/zxlib/basic.rb, line 1453
def bytesize
        data.bytesize
end
byteslice(*at) click to toggle source

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
char_array?() click to toggle source

true if variable is a character array

# File lib/zxlib/basic.rb, line 1430
def char_array?
        type == VAR_CHAR_ARRAY
end
code() click to toggle source

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
for_loop?() click to toggle source

true if variable is a FOR loop variable

# File lib/zxlib/basic.rb, line 1438
def for_loop?
        type == VAR_FOR_LOOP
end
head() click to toggle source

Returns a header byte.

# File lib/zxlib/basic.rb, line 1386
def head
        data.ord
end
length() click to toggle source

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
limit() click to toggle source

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
line() click to toggle source

Returns the FOR loop line number.

# File lib/zxlib/basic.rb, line 1488
def line
        if for_loop?
                data.unpack('@16v')[0]
        end
end
number?() click to toggle source

true if variable is a number variable

# File lib/zxlib/basic.rb, line 1422
def number?
        type == VAR_NUMBER || type == VAR_NUMBER_EX
end
number_array?() click to toggle source

true if variable is a number array

# File lib/zxlib/basic.rb, line 1434
def number_array?
        type == VAR_NUMBER_ARRAY
end
statement() click to toggle source

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
step() click to toggle source

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
string?() click to toggle source

true if variable is a string variable

# File lib/zxlib/basic.rb, line 1418
def string?
        type == VAR_STRING
end
to_s() click to toggle source

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
to_tap_chunk(name, org:nil) click to toggle source

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
value() click to toggle source

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