lib/fit4ruby/FitDataRecord.rb in fit4ruby-3.7.0 vs lib/fit4ruby/FitDataRecord.rb in fit4ruby-3.8.0

- old
+ new

@@ -1,41 +1,44 @@ #!/usr/bin/env ruby -w # encoding: UTF-8 # # = FitDataRecord.rb -- Fit4Ruby - FIT file processing library for Ruby # -# Copyright (c) 2014, 2015 by Chris Schlaeger <cs@taskjuggler.org> +# Copyright (c) 2014, 2015, 2020 by Chris Schlaeger <cs@taskjuggler.org> # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # require 'fit4ruby/FitMessageIdMapper' require 'fit4ruby/GlobalFitMessages.rb' +require 'fit4ruby/BDFieldNameTranslator' module Fit4Ruby class FitDataRecord include Converters + include BDFieldNameTranslator RecordOrder = [ 'user_data', 'user_profile', 'device_info', 'data_sources', 'event', 'record', 'lap', 'length', 'session', 'heart_rate_zones', 'personal_records' ] - attr_reader :message + attr_reader :message, :timestamp def initialize(record_id) @message = GlobalFitMessages.find_by_name(record_id) # Create instance variables that correspond to every field of the # corresponding FIT data record. @message.fields_by_name.each do |name, field| create_instance_variable(name) end + # Meta fields are additional fields that are not part of the FIT # specification but are convenient to have. These are typcially # aggregated or converted values of regular fields. @meta_field_units = {} @timestamp = Time.now @@ -56,10 +59,11 @@ end instance_variable_set(ivar_name, value) end def get(name) + # This is a request for a native FIT field. ivar_name = '@' + name return nil unless instance_variable_defined?(ivar_name) instance_variable_get(ivar_name) end @@ -71,26 +75,26 @@ return value if to_unit.nil? || to_unit.empty? if @meta_field_units.include?(name) unit = @meta_field_units[name] else - field = @message.fields_by_name[name] - unless (unit = field.opts[:unit]) + unless (unit = get_unit_by_name(name)) Log.fatal "Field #{name} has no unit" end end value * conversion_factor(unit, to_unit) end def ==(fdr) - @message.fields_by_name.each do |name, field| - ivar_name = '@' + name - v1 = field.fit_to_native(field.native_to_fit( - instance_variable_get(ivar_name))) - v2 = field.fit_to_native(field.native_to_fit( - fdr.instance_variable_get(ivar_name))) + @message.each_field(field_values_as_hash) do |number, field| + ivar_name = '@' + field.name + # Comparison of values is done in the fit file format as the accuracy + # of native formats is better and could lead to wrong results if a + # value hasn't been read back from a fit file yet. + v1 = field.native_to_fit(instance_variable_get(ivar_name)) + v2 = field.native_to_fit(fdr.instance_variable_get(ivar_name)) return false unless v1 == v2 end true @@ -102,17 +106,12 @@ RecordOrder.index(fdr.message.name) : @timestamp <=> fdr.timestamp end def write(io, id_mapper) - # Construct a GlobalFitMessage object that matches exactly the provided - # set of fields. It does not contain any AltField objects. - fields = {} - @message.fields_by_name.each_key do |name| - fields[name] = instance_variable_get('@' + name) - end - global_fit_message = @message.construct(fields) + global_fit_message = @message.construct( + field_values = field_values_as_hash) # Map the global message number to the current local message number. unless (local_message_number = id_mapper.get_local(global_fit_message)) # If the current dictionary does not contain the global message # number, we need to create a new entry for it. The index in the @@ -129,44 +128,98 @@ header.local_message_type = local_message_number header.write(io) # Create a BinData::Struct object to store the data record. fields = [] - global_fit_message.fields_by_number.each do |field_number, field| - bin_data_type = FitDefinitionFieldBase.fit_type_to_bin_data(field.type) - fields << [ bin_data_type, field.name ] - end - bd = BinData::Struct.new(:endian => :little, :fields => fields) + values = {} # Fill the BinData::Struct object with the values from the corresponding # instance variables. - global_fit_message.fields_by_number.each do |field_number, field| + global_fit_message.each_field(field_values) do |number, field| + bin_data_type = FitDefinitionFieldBase.fit_type_to_bin_data(field.type) + field_name = to_bd_field_name(field.name) + field_def = [ bin_data_type, field_name ] + iv = "@#{field.name}" if instance_variable_defined?(iv) && !(iv_value = instance_variable_get(iv)).nil? - value = field.native_to_fit(iv_value) + values[field.name] = field.native_to_fit(iv_value) else # If we don't have a corresponding variable or the variable is nil # we write the 'undefined' value instead. value = FitDefinitionFieldBase.undefined_value(field.type) + values[field.name] = field.opts[:array] ? [ value ] : + field.type == 'string' ? '' : value end - bd[field.name] = value + + # Some field types need special handling. + if field.type == 'string' + # Zero terminate the string. + values[field.name] += "\0" + elsif field.opts[:array] + # For Arrays we use a BinData::Array to write them. + field_def = [ :array, field_name, + { :type => bin_data_type, + :initial_length => values[field.name].size } ] + end + fields << field_def end + bd = BinData::Struct.new(:endian => :little, :fields => fields) + # Fill the BinData::Struct object with the values from the corresponding + # instance variables. + global_fit_message.each_field(field_values) do |number, field| + bd[to_bd_field_name(field.name)] = values[field.name] + end + # Write the data record to the file. bd.write(io) end - def inspect - fields = {} - @message.fields_by_name.each do |name, field| + def export + message = { + 'message' => @message.name, + 'number' => @message.number, + 'fields' => {} + } + + @message.each_field(field_values_as_hash) do |number, field| ivar_name = '@' + field.name - fields[field.name] = instance_variable_get(ivar_name) + fit_value = field.native_to_fit(instance_variable_get(ivar_name)) + unless field.is_undefined?(fit_value) + fld = { + 'number' => number, + 'value' => field.fit_to_native(fit_value), + #'human' => field.to_human(fit_value), + 'type' => field.type + } + fld['unit'] = field.opts[:unit] if field.opts[:unit] + fld['scale'] = field.opts[:scale] if field.opts[:scale] + + message['fields'][field.name] = fld + end end - fields.inspect + + message end + def get_unit_by_name(name) + field = @message.fields_by_name[name] + field.opts[:unit] + end + private + + def field_values_as_hash + # Construct a GlobalFitMessage object that matches exactly the provided + # set of fields. It does not contain any AltField objects. + field_values = {} + @message.fields_by_name.each_key do |name| + field_values[name] = instance_variable_get('@' + name) + end + + field_values + end def create_instance_variable(name) # Create a new instance variable for 'name'. We initialize it with a # provided default value or nil. instance_variable_set('@' + name, nil)