Class: Pubid::Ieee::Identifiers::Base

Inherits:
Pubid::Identifier
  • Object
show all
Defined in:
lib/pubid/ieee/identifiers/base.rb

Overview

Base class for all IEEE identifiers

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Pubid::Identifier

#base_identifier, #eql?, #exclude, #hash, #mr_number, #mr_number_with_part, #mr_part, #mr_publisher, #mr_type, #mr_year, #new_edition_of?, polymorphic_name, #render, #resolve_urn_generator, #root, #to_mr_string, #to_supplement_s, #to_urn, #urn_supplement_type, #urn_type_code

Constructor Details

#initialize(**args) ⇒ Base

Returns a new instance of Base.



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/pubid/ieee/identifiers/base.rb', line 58

def initialize(**args)
  super()

  # Handle typed_stage if provided
  if args[:typed_stage]
    self.typed_stage = args[:typed_stage]
  end

  # Handle code as component object
  if args[:code].is_a?(String)
    self.code_obj = Components::Code.parse(args[:code])
    self.code = args[:code]
  elsif args[:code]
    self.code_obj = args[:code]
    self.code = args[:code].to_s
  end

  # Handle draft as component object
  if args[:draft_obj]
    self.draft_obj = args[:draft_obj]
    self.draft = args[:draft_obj].to_s
  elsif args[:draft]
    # If draft is passed as string, try to create Draft object
    if args[:draft].is_a?(String)
      # Try to parse the string to extract version/revision
      if args[:draft] =~ /^D(\d+)(?:\.(\d+))?/
        version = $1
        revision = $2
        self.draft_obj = Components::Draft.new(version: version,
                                               revision: revision)
      else
        # Simple case - treat as version
        self.draft_obj = Components::Draft.new(version: args[:draft])
      end
      self.draft = draft_obj.to_s
    else
      self.draft_obj = args[:draft]
      self.draft = args[:draft].to_s
    end
  end

  # Set other attributes
  attrs = self.class.attributes
  args.each do |key, value|
    next if %i[code draft draft_obj typed_stage].include?(key)

    setter = :"#{key}="
    public_send(setter, value) if attrs.key?(key)
  end
end

Instance Attribute Details

#code_objObject

Store actual component objects



56
57
58
# File 'lib/pubid/ieee/identifiers/base.rb', line 56

def code_obj
  @code_obj
end

#draft_objObject

Store actual component objects



56
57
58
# File 'lib/pubid/ieee/identifiers/base.rb', line 56

def draft_obj
  @draft_obj
end

Class Method Details

.parse(input) ⇒ Object

Parse IEEE identifier string



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
# File 'lib/pubid/ieee/identifiers/base.rb', line 126

def self.parse(input)
  # Preprocessing: Convert comma-separated dual standards to "and" format
  # This must happen BEFORE the "and" check below
  # Pattern: "IEEE Std 960-1989, Std 1177-1989" -> "IEEE Std 960-1989 and IEEE Std 1177-1989"
  input = input.gsub(/(\d{4}),\s+Std\s/, '\1 and IEEE Std ')

  # Check for AIEE identifiers (but NOT those with parentheses which have relationships/adoptions)
  # AIEE has its own parser/builder for simple identifiers
  # AIEE inputs with parentheses are handled by IEEE parser for relationship/adoption parsing
  if input.start_with?("AIEE ") && !input.include?("(")
    return Aiee::Identifier.parse(input)
  end

  # Check for IEC/IEEE copublished patterns first (before other checks)
  if input.start_with?("IEC/IEEE ")
    return parse_single(input)
  end

  # Check for semicolon-separated dual identifiers (IEC-first patterns)
  # Pattern: "IEC 61523-3 First edition 2004-09; IEEE 1497"
  if input.include?("; ")
    parts = input.split("; ")
    if parts.length == 2
      # Try to parse both parts
      begin
        first = parse_single(parts[0].strip)
        second = parse_single(parts[1].strip)

        return Identifiers::DualPublished.new(
          first_identifier: first,
          second_identifier: second,
        )
      rescue Parslet::ParseFailed
        # If parsing fails, continue with normal flow
      end
    end
  end

  # PREPROCESS: Handle (R####) (Revision of...) pattern
  # Convert to single parenthetical: (Reaffirmed ####, Revision of...)
  # This allows parser to capture both in one parenthetical
  if input =~ /\(R(\d{4})\)\s*\(Revision of ([^)]+)\)/
    year = $1
    $2
    # Replace the two parentheticals with reaffirmed info extracted
    # Parse normally but capture year first
    input_modified = input.sub(
      /\(R(\d{4})\)\s*\(Revision of ([^)]+)\)/, "(Revision of \\2)"
    )

    # Parse the modified input
    result = parse_single(input_modified)
    # Add reaffirmed attribute
    if result.class.attributes.key?(:reaffirmed)
      result.reaffirmed = year
    end
    return result
  end

  # PREPROCESS: Handle (Reaffirmed ####) (Revision of...) pattern (full word format)
  # Pattern: "ANSI/IEEE Std 101-1987 (Reaffirmed 2010) (Revision of IEEE Std 101-1972)"
  if input =~ /\(Reaffirmed\s+(\d{4})\)\s*\(Revision of ([^)]+)\)/
    year = $1
    $2
    # Replace the two parentheticals
    input_modified = input.sub(
      /\(Reaffirmed\s+(\d{4})\)\s*\(Revision of ([^)]+)\)/, "(Revision of \\2)"
    )

    # Parse the modified input
    result = parse_single(input_modified)
    # Add reaffirmed attribute
    if result.class.attributes.key?(:reaffirmed)
      result.reaffirmed = year
    end
    return result
  end

  # NEW Session 174: Check for IRE dual published pattern
  # Pattern after preprocessing: "IEEE Std 218-1956 (R1980) (56 IRE 28.S2)"
  # First parenthetical: (Rxxx) reaffirmed
  # Second parenthetical: IRE identifier
  if /\(R\d{4}\)\s*\((\d+\s+IRE[^)]+)\)/.match?(input)
    main_part = input.split(" (R").first.strip # Get "IEEE Std 218-1956"
    reaffirmed_year = input.match(/\(R(\d{4})\)/)[1]
    ire_part = input.match(/\((\d+\s+IRE[^)]+)\)/)[1]

    begin
      # Parse main identifier
      ieee_id = parse_single(main_part)
      # Add reaffirmed year
      ieee_id.reaffirmed = reaffirmed_year if ieee_id.class.attributes.key?(:reaffirmed)

      # Parse IRE identifier
      ire_id = parse_single(ire_part)

      return Identifiers::DualPublished.new(
        first_identifier: ieee_id,
        second_identifier: ire_id,
      )
    rescue Parslet::ParseFailed
      # If parsing fails, fall through to regular processing
    end
  end

  # Check for space-separated dual identifiers (e.g., "IEC 62014-5 IEEE Std 1734-2011")
  # This must be checked before " and " pattern
  # Look for pattern where a second publisher appears after the first complete identifier
  # Publishers: IEEE, AIEE, ANSI, ASA, IEC, ISO, ASTM, NACE, NSF, ASHRAE, NCTA, AESC
  publishers = %w[IEEE AIEE ANSI ASA IEC ISO ASTM NACE NSF ASHRAE NCTA
                  AESC]

  # Find all positions where publishers appear (but NOT inside parentheses)
  # Publishers inside parentheses are part of relationship clauses, not dual published patterns
  publisher_positions = []
  publishers.each do |pub|
    # Look for publisher at word boundaries (preceded by space or start of string)
    regex = /(?:^|\s)(#{Regexp.escape(pub)})(?:\s|\/)/
    input.scan(regex) do
      match_pos = Regexp.last_match.begin(1)

      # Check if this publisher is inside parentheses (relationship clause)
      # Count parens before this position
      before_match = input[0...match_pos]
      paren_count = before_match.count("(") - before_match.count(")")

      # Skip publishers inside parentheses - they're part of relationship clauses
      next if paren_count.positive?

      publisher_positions << { pos: match_pos, publisher: pub }
    end
  end

  # If we have 2 or more publishers at distinct positions (not co-publishers with /)
  if publisher_positions.length >= 2
    # Sort by position
    publisher_positions.sort_by! { |p| p[:pos] }

    # Check if they're not part of a co-published pattern (Publisher1/Publisher2)
    # by ensuring there's no slash between them
    first_pub = publisher_positions[0]
    second_pub = publisher_positions[1]

    # Get the substring between the two publishers
    between = input[first_pub[:pos]..(second_pub[:pos] - 1)]

    # If there's no slash and no " and ", this might be space-separated dual
    if !between.include?("/") && !between.include?(" and ")
      # Try to split at the second publisher position
      # Back up to find the space before the second publisher
      split_pos = second_pub[:pos]
      while split_pos.positive? && input[split_pos - 1] == " "
        split_pos -= 1
      end

      first_part = input[0...split_pos].strip
      second_part = input[split_pos..].strip

      # Try to parse both parts
      begin
        first = parse_single(first_part)
        second = parse_single(second_part)

        # Only treat as dual if both parse successfully
        return Identifiers::DualPublished.new(
          first_identifier: first,
          second_identifier: second,
        )
      rescue Parslet::ParseFailed
        # If parsing fails, continue with normal flow
      end
    end
  end

  # Check for dual published patterns with " and "
  if input.include?(" and ")
    # DON'T split if " and " is inside parentheses (likely a relationship clause)
    # Check if parentheses are balanced and " and " is inside them
    paren_count = 0
    and_outside_parens = false
    and_position = nil

    input.each_char.with_index do |char, i|
      paren_count += 1 if char == "("
      paren_count -= 1 if char == ")"

      # Check if " and " starts at this position and we're outside parens
      if paren_count.zero? && input[i..(i + 4)] == " and "
        and_outside_parens = true
        and_position = i
        break
      end
    end

    # Only split if " and " is outside parentheses
    if and_outside_parens && and_position
      # Split at the found position only (not at all " and " occurrences)
      first_part = input[0...and_position].strip
      second_part = input[(and_position + 5)..].strip

      # Parse each part separately
      first = parse_single(first_part)
      second = parse_single(second_part)

      return Identifiers::DualPublished.new(
        first_identifier: first,
        second_identifier: second,
      )
    end
  end

  # NEW Session 171: Check for dual published patterns with " & " (ampersand)
  if input.include?(" & ")
    # DON'T split if " & " is inside parentheses
    paren_count = 0
    ampersand_outside_parens = false

    input.each_char.with_index do |char, i|
      paren_count += 1 if char == "("
      paren_count -= 1 if char == ")"

      # Check if " & " starts at this position and we're outside parens
      if paren_count.zero? && input[i..(i + 2)] == " & "
        ampersand_outside_parens = true
        break
      end
    end

    # Only split if " & " is outside parentheses
    if ampersand_outside_parens
      parts = input.split(" & ")
      if parts.length == 2
        # Parse each part separately
        first = parse_single(parts[0].strip)
        second = parse_single(parts[1].strip)

        return Identifiers::DualPublished.new(
          first_identifier: first,
          second_identifier: second,
        )
      end
    end
  end

  # Special case: AIEE identifiers with ASA parenthetical references
  # Pattern: "AIEE No 18-1934 (ASA C55 1934)"
  if input.match?(/^AIEE\s+/) && input.include?("(") && input.include?("ASA")
    main_part = input.split("(").first.strip
    adoption_match = input.match(/\((ASA[^)]+)\)/)

    if adoption_match
      adoption_part = adoption_match.captures.first
      # Parse the main AIEE identifier
      begin
        aiee_id = parse_single(main_part)
        # Parse the ASA identifier
        asa_id = parse_single(adoption_part)

        return Identifiers::AdoptedStandard.new(
          ieee_identifier: aiee_id,
          adopted_identifier: asa_id,
        )
      rescue Parslet::ParseFailed
        # If parsing fails, fall through to regular processing
      end
    end
  end

  # Check for adopted standards (parenthetical adoptions)
  # Only consider it an adoption if the parenthetical content looks like an identifier
  if input.include?("(") && input.include?(")") && !input.start_with?("IEC/IEEE ")
    # Extract the part before parentheses and the adoption part
    main_part = input.split("(").first.strip
    adoption_match = input.match(/\(([^)]+)\)/)
    adoption_part = adoption_match&.captures&.first

    # Check if adoption_part looks like an identifier (contains publisher or type keywords)
    # BUT exclude revision/amendment/supersedes notes
    # AND exclude Pattern 4 relationship types
    if main_part && adoption_part &&
        !adoption_part.match?(/^\s*(Revision|Revison|Amendment|Corrigendum|Corrigenda|incorporates|Incorporating|Incorporates|Adoption|Supplement|Draft Amendment|DRAFT Amendment|Draft Revision|Reaffirmation|Redesignation|redesignated as|Supersedes|Supercedes|Includes|Previously designated as|Notebooks|Standard Newspaper)/i) &&
        (adoption_part.match?(/\b(ANSI|ISO|IEC|IEEE|AIEE|IRE|ASA|ASTM|CSA|ASME|NACE|NSF|ASHRAE|NCTA|AESC)\s/) ||
         adoption_part.match?(/^\s*(ANSI|ISO|IEC|IEEE|AIEE|IRE|ASA|ASTM|CSA|ASME|NACE|NSF|ASHRAE|NCTA|AESC)\b/) ||
         adoption_part.match?(/\bStd\s+\d+/))
      # Parse the main IEEE identifier
      ieee_id = parse_single(main_part)

      # Parse comma-separated adopted identifiers
      adopted_parts = adoption_part.split(",").map(&:strip)
      adopted_ids = adopted_parts.map do |part|
        if part.strip.start_with?("IEC")
          # Use IEC parser for IEC adoptions
          # Preprocess to convert "Edition X.Y" to IEC format
          # Pattern: "IEC 60255-24 Edition 2.0 2013-04" → "IEC 60255-24:2013-04 ED2.0"
          iec_part = part.dup
          # Replace " Edition X.Y YYYY-MM" (or similar) with ":YYYY-MM EDX.Y"
          iec_part.gsub!(/\s+Edition\s+([0-9.]+)\s+([0-9-]+)/,
                         ':\2 ED\1')
          # Replace " Edition X.Y" at end (no date)
          iec_part.gsub!(/\s+Edition\s+([0-9.]+)\s*$/, ' ED\1')
          Pubid::Iec.parse(iec_part)
        elsif part.strip.start_with?("ANSI")
          # Use ANSI parser for ANSI adoptions
          Pubid::Ansi.parse(part)
        else
          # Use IEEE parser for other adoptions
          parse_single(part)
        end
      end

      return Identifiers::AdoptedStandard.new(
        ieee_identifier: ieee_id,
        adopted_identifiers: adopted_ids,
      )
    end
    # If it doesn't look like an identifier, let the parser handle it as additional_parameters
  end

  # Fall back to single identifier parsing
  parse_single(input)
end

.parse_single(input) ⇒ Object

Parse a single IEEE identifier



449
450
451
452
453
454
455
456
457
# File 'lib/pubid/ieee/identifiers/base.rb', line 449

def self.parse_single(input)
  # Apply legacy update_codes normalization first, before Parser's extensive preprocessing
  normalized = Core::UpdateCodes.apply(input, :ieee)
  parsed = Parser.parse(normalized) # Use class method for preprocessing
  builder = Builder.new(Base)
  # Pass the original input string to builder for context
  builder.original_input = input
  builder.build(parsed)
end

Instance Method Details

#codeObject

Override accessors to return component objects



110
111
112
# File 'lib/pubid/ieee/identifiers/base.rb', line 110

def code
  code_obj
end

#draftObject



114
115
116
# File 'lib/pubid/ieee/identifiers/base.rb', line 114

def draft
  draft_obj
end

#draft_monthObject

Expose numeric month from draft if available



119
120
121
122
123
# File 'lib/pubid/ieee/identifiers/base.rb', line 119

def draft_month
  return nil unless draft_obj.is_a?(Components::Draft)

  draft_obj.numeric_month
end

#publisherString

Generate URN for this identifier

Returns:

  • (String)

    URN representation



16
# File 'lib/pubid/ieee/identifiers/base.rb', line 16

attribute :publisher, :string, default: -> { "IEEE" }

#to_sObject



459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
# File 'lib/pubid/ieee/identifiers/base.rb', line 459

def to_s
  parts = []

  # Publisher(s) - handle copublisher array properly
  if copublisher && !copublisher.empty?
    # Copublisher is an array, join all publishers with single slash
    parts << [publisher, *copublisher].join("/")
  else
    parts << publisher
  end

  # Draft status
  parts << draft_status if draft_status

  # Type - only render for IEEE/AIEE publishers, and only for non-projects
  # Must come BEFORE code
  should_render_type = publisher&.match?(/^(IEEE|AIEE)/)

  if should_render_type && !typed_stage&.project_status && type && !type.to_s.strip.empty? && type != "P"
    # Non-project with explicit type (Std, No, etc.)
    type_str = type.dup
    # Remove P prefix if somehow present
    type_str = type_str.sub(/^P/, "") if type_str.start_with?("P")
    parts << type_str unless type_str.strip.empty?
  end

  # Code - with P prefix for projects (concatenated, not separated)
  if code_obj
    result = code_obj.to_s

    # Prepend P if this is a project AND code doesn't already have P
    if typed_stage&.project_status && should_render_type && !result.start_with?("P")
      result = "P#{result}"
    end

    # Only attach year to code if there's no edition, no month, and no draft
    result += "-#{year}" if year && !draft_obj && !edition && !month

    # Append draft to code - with or without space based on original format
    if draft_obj
      result += space_before_draft ? " #{draft_obj}" : draft_obj.to_s
    end

    # Append interpretation notation (/INT)
    result += "/INT" if interpretation

    # Append conformance notation (/ConformanceNN-YYYY)
    if conf_number
      result += "/Conformance#{conf_number}"
      result += "-#{conf_year}" if conf_year
    end

    # Append ASHRAE joint publication (/ASHRAE Guideline NN-YYYY)
    if ashrae_number
      result += "/ASHRAE Guideline #{ashrae_number}"
      result += "-#{ashrae_year}" if ashrae_year
    end

    # Append IEEE cross-reference (/C62.22.1-1996)
    result += crossref if crossref

    parts << result
  elsif should_render_type && typed_stage&.project_status
    # No code but is a project - add standalone P
    parts << "P"
  end

  # Edition - with year if present (IEC style)
  if edition
    edition_str = "Edition #{edition}"
    if year
      edition_str += " #{year}"
      edition_str += "-#{edition_month}" if edition_month
    end
    parts << edition_str
  end

  # Build the main identifier (without month yet)
  result = parts.join(" ")

  # Month/Day - append directly to avoid extra space before comma
  if month
    # Format: ", Month Day, Year" or ", Month, Year"
    result += ", #{month}"
    result += " #{day}" if day
    if year && !edition
      # Add comma after month if year follows
      result += ", #{year}"
    end
  end

  # Add parenthetical content if present
  # Handle multiple parenthetical clauses (reaffirmed + relationships/revision)
  parentheticals = []

  reaff = reaffirmed
  if reaff && !reaff.to_s.strip.empty?
    parentheticals << "(R#{reaff})"
  end

  # Then add relationships/revision/amendment as second parenthetical
  if parenthetical_content
    parentheticals << "(#{parenthetical_content})"
  elsif relationships && !relationships.empty?
    # Render relationships (multiple separated by /)
    relationship_str = relationships.join(" / ")
    parentheticals << "(#{relationship_str})"
  elsif revision_of
    parentheticals << "(Revision of IEEE Std #{revision_of})"
  elsif amendment_to
    parentheticals << "(Amendment to IEEE Std #{amendment_to})"
  elsif adoption
    parentheticals << "(Adoption of #{adoption})"
  elsif note && !note.to_s.strip.empty?
    # Only add note if it doesn't duplicate other content
    parentheticals << "(#{note})"
  end

  # Append all parentheticals with space separation
  result += " #{parentheticals.join(' ')}" unless parentheticals.empty?

  # Book nickname - outside parentheses in square brackets
  result += " [#{nickname}]" if nickname && !nickname.to_s.strip.empty?

  # Redline suffix
  result += " - Redline" if redline

  result
end