Jump To …

bcv_passage.coffee

This class takes the output from the grammar and turns it into simpler objects for additional processing or for output.

class bcv_passage
	books: []
	indices: {}

bcv_parser sets these two.

	options: {}
	translations: {}

Public

Loop through the parsed passages.

	handle_array: (passages, accum=[], context={}) ->

passages is an array of passage objects.

		for passage in passages

Each passage consists of passage objects and, possibly, strings.

			[accum, context] = @handle_obj passage, accum, context
		[accum, context]

Handle a typical passage object with an index, type, and array in value.

	handle_obj: (passage, accum, context) ->
		if passage.type? and @[passage.type]?
			@[passage.type] passage, accum, context
		else [accum, context]

Types Returned from the Grammar

These functions correspond to type attributes returned from the grammar. They're designed to be called multiple times if necessary.

Handle a book on its own.

	b: (passage, accum, context) ->
		passage.start_context = @shallow_clone context
		passage.passages = []
		alternates = []
		for b in @books[passage.value].parsed
			valid = @validate_ref passage.start_context.translations, {b: b}
			obj = start: {b: b}, end: {b: b}, valid: valid

Use the first valid book.

			if passage.passages.length is 0 and valid.valid
				passage.passages.push obj
			else
				alternates.push obj

If none are valid, use the first one.

		passage.passages.push alternates.shift() if passage.passages.length is 0
		passage.passages[0].alternates = alternates if alternates.length > 0
		passage.passages[0].translations = passage.start_context.translations if passage.start_context.translations?
		passage.absolute_indices ?= @get_absolute_indices passage.indices
		accum.push passage
		context = b: passage.passages[0].start.b
		context.translations = passage.start_context.translations if passage.start_context.translations?
		[accum, context]

Handle book-only ranges.

	b_range: (passage, accum, context) ->
		@range passage, accum, context

Handle book-only ranges like 1-2 Samuel. It doesn't support multiple ambiguous ranges (like 1-2C), which it probably shouldn't, anyway

	b_range_pre: (passage, accum, context) ->
		passage.start_context = @shallow_clone context
		passage.passages = []
		alternates = []
		book = @pluck "b", passage.value
		[[end], context] = @b book, [], context
		passage.absolute_indices ?= @get_absolute_indices passage.indices
		start_obj = b: passage.value[0].value + end.passages[0].start.b.substr(1), type: "b"
		passage.passages = [start: start_obj, end: end.passages[0].end, valid: end.passages[0].valid]
		passage.passages[0].translations = passage.start_context.translations if passage.start_context.translations?
		accum.push passage
		[accum, context]

The base (root) object in the grammar and controls the base indices.

	base: (passage, accum, context) ->
		@indices = @calculate_indices passage.match, passage.start_index
		@handle_array passage.value, accum, context

Handle book-chapter.

	bc: (passage, accum, context) ->
		passage.start_context = @shallow_clone context
		passage.passages = []
		for type in ["b", "c", "v"]
			delete context[type]
		c = @pluck("c", passage.value).value
		alternates = []
		for b in @books[@pluck("b", passage.value).value].parsed
			context_key = "c"
			valid = @validate_ref passage.start_context.translations, {b: b, c: c}
			obj = start: {b: b}, end: {b: b}, valid: valid

Is it really a bv object?

			if valid.messages.start_chapter_not_exist_in_single_chapter_book
				obj.valid = @validate_ref passage.start_context.translations, {b: b, v: c}
				obj.start.c = 1
				obj.end.c = 1
				context_key = "v"
			obj.start[context_key] = c

If it's zero, fix it before assigning the end.

			[obj.start.c, obj.start.v] = @fix_start_zeroes obj.valid, obj.start.c, obj.start.v
			obj.end[context_key] = obj.start[context_key]
			if passage.passages.length is 0 and obj.valid.valid
				passage.passages.push obj
			else
				alternates.push obj
		passage.passages.push alternates.shift() if passage.passages.length is 0
		passage.passages[0].alternates = alternates if alternates.length > 0
		passage.passages[0].translations = passage.start_context.translations if passage.start_context.translations?
		passage.absolute_indices ?= @get_absolute_indices passage.indices
		for type in ["b", "c", "v"]
			context[type] = passage.passages[0].start[type] if passage.passages[0].start[type]?
		accum.push passage
		[accum, context]

Handle "Ps 3 title"

	bc_title: (passage, accum, context) ->
		passage.start_context = @shallow_clone context

First, check to see whether we're dealing with Psalms. If not, treat it as a straight bc.

		[[bc], context] = @bc @pluck("bc", passage.value), [], context
		if bc.passages[0].start.b isnt "Ps" and bc.passages[0].alternates?
			for i in [0...bc.passages[0].alternates.length]
				continue unless bc.passages[0].alternates[i].start.b is "Ps"

If Psalms is one of the alternates, promote it to the primary passage and discard the others--we know it's right.

				bc.passages[0] = bc.passages[0].alternates[i]
				break
		if bc.passages[0].start.b isnt "Ps"
			accum.push bc
			return [accum, context]

Overwrite all the other book possibilities; the presence of "title" indicates a Psalm

		@books[@pluck("b", bc.value).value].parsed = ["Ps"]

Set the indices of the new v object to the indices of the title. We won't actually use these indices anywhere.

		title = @pluck "title", passage.value
		passage.value[1] = {type: "v", value: [{type: "integer", value: 1, indices: title.indices}], indices: title.indices}

Not for reparsing but in case we're curious later.

		passage.original_type = "bc_title"
		passage.type = "bcv"

Treat it as a standard bcv.

		@bcv passage, accum, passage.start_context

Handle book chapter:verse.

	bcv: (passage, accum, context) ->
		passage.start_context = @shallow_clone context
		passage.passages = []
		for type in ["b", "c", "v"]
			delete context[type]
		bc = @pluck "bc", passage.value
		c = @pluck("c", bc.value).value
		v = @pluck("v", passage.value).value
		alternates = []
		for b in @books[@pluck("b", bc.value).value].parsed
			valid = @validate_ref passage.start_context.translations, {b: b, c: c, v: v}
			[c, v] = @fix_start_zeroes valid, c, v
			obj = start: {b: b, c: c, v: v}, end: {b: b, c: c, v: v}, valid: valid

Use the first valid option.

			if passage.passages.length is 0 and valid.valid
				passage.passages.push obj
			else
				alternates.push obj

If there are no valid options, use the first one.

		passage.passages.push alternates.shift() if passage.passages.length is 0
		passage.passages[0].alternates = alternates if alternates.length > 0
		passage.passages[0].translations = passage.start_context.translations if passage.start_context.translations?
		passage.absolute_indices ?= @get_absolute_indices passage.indices

Set all the available context keys.

		for type in ["b", "c", "v"]
			context[type] = passage.passages[0].start[type] if passage.passages[0].start[type]?
		accum.push passage
		[accum, context]

Handle "Philemon verse 6." This is unusual.

	bv: (passage, accum, context) ->
		passage.start_context = @shallow_clone context
		[b, v] = passage.value

Construct a virtual BCV object with a chapter of 1.

		bcv =
			indices: passage.indices
			value: [
				{type: "bc", value: [b, {type: "c", value: [{type: "integer", value: 1}]}]}
				v
			]
		[[bcv], context] = @bcv bcv, [], context
		passage.passages = bcv.passages
		passage.passages[0].translations = passage.start_context.translations if passage.start_context.translations?
		passage.absolute_indices ?= @get_absolute_indices passage.indices
		accum.push passage
		[accum, context]

Handle a chapter.

	c: (passage, accum, context) ->
		passage.start_context = @shallow_clone context

If it's an actual chapter object, the value we want is in the integer object inside it.

		c = if passage.type is "integer" then passage.value else @pluck("integer", passage.value).value
		valid = @validate_ref passage.start_context.translations, {b: context.b, c: c}

If it's a single-chapter book, then treat it as a verse even if it looks like a chapter (unless its value is 1).

		if not valid.valid and valid.messages.start_chapter_not_exist_in_single_chapter_book
			return @v passage, accum, context
		[c] = @fix_start_zeroes valid, c
		passage.passages = [start: {b: context.b, c: c}, end: {b: context.b, c: c}, valid: valid]
		passage.passages[0].translations = passage.start_context.translations if passage.start_context.translations?
		accum.push passage
		context.c = c
		delete context.v
		passage.absolute_indices ?= @get_absolute_indices passage.indices
		[accum, context]

Handle "23rd Psalm" by recasting it as a bc.

	c_psalm: (passage, accum, context) ->
		passage.original_type = passage.type
		passage.original_value = passage.value
		passage.type = "bc"

This string always starts with the chapter number, followed by other letters.

		c = @books[passage.value].value.match(/^\d+/)[0]
		passage.value = [
			{type: "b", value: passage.original_value, indices: passage.indices}
			{type: "c", value: [{type: "integer", value: c, indices: passage.indices}], indices: passage.indices}
		]
		@bc passage, accum, context

Handle "Ps 3, ch 4:title"

	c_title: (passage, accum, context) ->
		passage.start_context = @shallow_clone context

If it's not a Psalm, treat it as a regular chapter

		if context.b isnt "Ps"
			return @c passage.value[0], accum, context

Add a v object and treat it as a refular cv.

		title = @pluck "title", passage.value
		passage.value[1] = {type: "v", value: [{type: "integer", value: 1, indices: title.indices}], indices: title.indices}

Not for reparsing but in case we're curious later.

		passage.original_type = "c_title"
		passage.type = "cv"
		@cv passage, accum, passage.start_context

Handle a chapter:verse.

	cv: (passage, accum, context) ->
		passage.start_context = @shallow_clone context
		c = @pluck("c", passage.value).value
		v = @pluck("v", passage.value).value
		valid = @validate_ref passage.start_context.translations, {b: context.b, c: c, v: v}
		[c, v] = @fix_start_zeroes valid, c, v
		passage.passages = [start: {b: context.b, c: c, v: v}, end: {b: context.b, c: c, v: v}, valid: valid]
		passage.passages[0].translations = passage.start_context.translations if passage.start_context.translations?
		accum.push passage
		context.c = c
		context.v = v
		passage.absolute_indices ?= @get_absolute_indices passage.indices
		[accum, context]

Handle "Chapters 1-2 from Daniel".

	cb_range: (passage, accum, context) ->
		passage.original_type = passage.type
		passage.type = "range"
		[b, start_c, end_c] = passage.value
		passage.original_value = [b, start_c, end_c]
		passage.value = [{type: "bc", value:[b, start_c], indices: passage.indices}, end_c]
		end_c.indices[1] = passage.indices[1]
		@range passage, accum, context

Handle "23rd Psalm verse 1" by recasting it as a bcv.

	cv_psalm: (passage, accum, context) ->
		passage.start_context = @shallow_clone context
		passage.original_type = passage.type
		passage.original_value = passage.value
		[c_psalm, v] = passage.value
		passage.type = "bcv"
		[[bc]] = @c_psalm c_psalm, [], passage.start_context
		passage.value = [bc, v]
		@bcv passage, accum, context

Handle "and following" (e.g., "Matt 1:1ff") by assuming it means to continue to the end of the current context (end of chapter if a verse is given, end of book if a chapter is given).

	ff: (passage, accum, context) ->
		passage.start_context = @shallow_clone context

Create a virtual end to pass to @range.

		passage.value.push type: "integer", indices: passage.indices, value: 999
		[[passage], context] = @range passage, [], passage.start_context

And then get rid of the virtual end so it doesn't stick around if we need to reparse it later.

		passage.value.pop()

Ignore any warnings that the end chapter / verse doesn't exist.

		delete passage.passages[0].valid.end_verse_not_exist if passage.passages[0].valid.end_verse_not_exist?
		delete passage.passages[0].valid.end_chapter_not_exist if passage.passages[0].valid.end_chapter_not_exist?
		delete passage.passages[0].end.original_c if passage.passages[0].end.original_c?

translations was handled in @range.

		accum.push passage
		passage.absolute_indices ?= @get_absolute_indices passage.indices
		[accum, context]

Handle "Ps 3-4:title" or "Acts 2:22-27. Title"

	integer_title: (passage, accum, context) ->
		passage.start_context = @shallow_clone context

If it's not Psalms, treat it as a straight integer, ignoring the "title".

		if context.b isnt "Ps"
			return @integer passage.value[0], accum, context
		passage.value[0] = type: "c", value: [passage.value[0]], indices: [passage.value[0].indices[0], passage.value[0].indices[1]]

Add a v object.

		v_indices = [passage.indices[1] - 5, passage.indices[1]]
		passage.value[1] = {type: "v", value: [{type: "integer", value: 1, indices: v_indices}], indices: v_indices}

Not for reparsing but in case we're curious later.

		passage.original_type = "integer_title"
		passage.type = "cv"
		@cv passage, accum, passage.start_context

Pass the integer off to whichever handler is relevant.

	integer: (passage, accum, context) ->
		return @v passage, accum, context if context.v?
		return @c passage, accum, context

Handle a sequence of references. This is the only function that can return more than one object in the passage.passages array.

	sequence: (passage, accum, context) ->
		passage.start_context = @shallow_clone context
		passage.passages = []
		for obj in passage.value
			[[psg], context] = @handle_array obj, [], context

There's only more than one sub_psg if there was a range error.

			for sub_psg in psg.passages
				sub_psg.type ?= psg.type

Add the indices so we can possibly retrieve them later, depending on our sequence_combination_strategy.

				sub_psg.absolute_indices ?= psg.absolute_indices
				sub_psg.translations = psg.start_context.translations if psg.start_context.translations?
				passage.passages.push sub_psg
		unless passage.absolute_indices?
			if passage.passages.length > 0
				passage.absolute_indices = [passage.passages[0].absolute_indices[0], passage.passages[passage.passages.length - 1].absolute_indices[1]]
			else
				passage.absolute_indices =  @get_absolute_indices  passage.indices
		accum.push passage
		[accum, context]

Handle a verse, either as part of a sequence or because someone explicitly wrote "verse".

	v: (passage, accum, context) ->
		v = if passage.type is "integer" then passage.value else @pluck("integer", passage.value).value
		passage.start_context = @shallow_clone context

The chapter context might not be set if it follows a book in a sequence

		c = if context.c? then context.c else 1
		valid = @validate_ref passage.start_context.translations, {b: context.b, c: c, v: v}
		[no_c, v] = @fix_start_zeroes valid, 0, v
		passage.passages = [start: {b: context.b, c: c, v: v}, end: {b: context.b, c: c, v: v}, valid: valid]
		passage.passages[0].translations = passage.start_context.translations if passage.start_context.translations?
		passage.absolute_indices ?= @get_absolute_indices passage.indices
		accum.push passage
		context.v = v
		[accum, context]

Ranges

Handle any type of start and end range.

	range: (passage, accum, context) ->
		passage.start_context = @shallow_clone context
		[start, end] = passage.value

Matt 5-verse 6 = Matt.5.6

		if end.type is "v" and (start.type is "bc" or start.type is "c") and @options.end_range_digits_strategy is "verse"
			return @range_change_integer_end passage, accum

These always return exactly one object that we're interested in.

		[[start], context] = @handle_obj start, [], context
		[[end], context] = @handle_obj end, [], context

If we had to change the start or end types, make sure that's reflected in the value.

		passage.value = [start, end]

Similarly, if we had to adjust the indices, make sure they're reflected in the indices for the range.

		passage.indices = [start.indices[0], end.indices[1]]

We'll also need to recalculate these if they exist.

		delete passage.absolute_indices

Create the prospective start and end objects that will end up in passage.passages

		start_obj = b: start.passages[0].start.b, c: start.passages[0].start.c, v: start.passages[0].start.v, type: start.type
		end_obj = b: end.passages[0].end.b, c: end.passages[0].end.c, v: end.passages[0].end.v, type: end.type
		end_obj.c = 0 if end.passages[0].valid.messages.start_chapter_is_zero
		end_obj.v = 0 if end.passages[0].valid.messages.start_verse_is_zero
		valid = @validate_ref passage.start_context.translations, start_obj, end_obj
		if valid.valid

If Heb 13-15, treat it as Heb 13:15. This may be too clever for its own good.

			if valid.messages.end_chapter_not_exist and @options.end_range_digits_strategy is "verse" and not start_obj.v? and (end.type is "integer" or end.type is "v")
				temp_value = if end.type is "v" then @pluck "integer", end.value else end.value
				temp_valid = @validate_ref passage.start_context.translations, {b: start_obj.b, c: start_obj.c, v: temp_value}
				return @range_change_integer_end passage, accum if temp_valid.valid

If "John 10:22-42 vs 27", we're possibly misreading the "42 vs 27" as a cv.

			if valid.messages.end_chapter_not_exist and @options.end_range_digits_strategy is "verse" and start_obj.v? and end.type is "cv"

Make sure that what we're changing it to actually exists (that the chapter number can become the verse number, and the verse number is also valid in the current chapter).

				temp_valid = @validate_ref passage.start_context.translations, {b:end_obj.b, c: start_obj.c, v: end_obj.c}
				temp_valid = @validate_ref passage.start_context.translations, {b:end_obj.b, c: start_obj.c, v: end_obj.v} if temp_valid.valid
				return @range_change_cv_end passage, accum if temp_valid.valid

Otherwise, snap start/end chapters/verses if they're too high or low.

			@range_validate valid, start_obj, end_obj, passage
		else

Is it not valid because the end is before the start and the end is an integer (Matt 15-6) or a cv (Matt 15-6:2) (since anything else resets our expectations)?

Only go with a cv if it's the chapter that's too low (to avoid doing weird things with 31:30-31:1).

			if ((valid.messages.end_chapter_before_start or valid.messages.end_verse_before_start) and (end.type is "integer" or end.type is "v") or (valid.messages.end_chapter_before_start and end.type is "cv"))
				new_end = @range_check_new_end passage.start_context.translations, start_obj, end_obj, valid

If that's the case, then reparse the current passage object after correcting the end value, which is an integer.

				return @range_change_end passage, accum, new_end if new_end > 0

If someone enters "Jer 33-11", they probably mean "Jer.33.11"; as above, this may be too clever for its own good.

			if @options.end_range_digits_strategy is "verse" and start_obj.v is undefined and (end.type is "integer" or end.type is "v")
				temp_value = if end.type is "v" then @pluck "integer", end.value else end.value
				temp_valid = @validate_ref passage.start_context.translations, {b: start_obj.b, c: start_obj.c, v: temp_value}
				return @range_change_integer_end passage, accum if temp_valid.valid

Otherwise, if we couldn't fix the range, then treat the range as a sequence.

			[passage.original_type, passage.type] = [passage.type, "sequence"]

Construct the sequence value in the format expected.

			[passage.original_value, passage.value] = [[start, end], [[start], [end]]]

Don't use the context object because we've changed it in this function.

			return @handle_obj passage, accum, passage.start_context

We've already reset the indices to match the indices of the contained objects.

		passage.absolute_indices ?= @get_absolute_indices passage.indices
		passage.passages = [start: start_obj, end: end_obj, valid: valid]
		passage.passages[0].translations = passage.start_context.translations if passage.start_context.translations?
		accum.push passage
		[accum, context]

For Ps 122-23, treat the 23 as 123.

	range_change_end: (passage, accum, new_end) ->
		[start, end] = passage.value
		if end.type is "integer"
			end.original_value = end.value
			end.value = new_end
		else if end.type is "v"
			new_obj = @pluck "integer", end.value
			new_obj.original_value = new_obj.value
			new_obj.value = new_end
		else if end.type is "cv"

Get the chapter object and assign it (in place) the new value.

			new_obj = @pluck "c", end.value
			new_obj.original_value = new_obj.value
			new_obj.value = new_end
		@handle_obj passage, accum, passage.start_context

For "Jer 33-11", treat the "11" as a verse.

	range_change_integer_end: (passage, accum) ->
		[start, end] = passage.value
		passage.original_type = passage.type
		passage.original_value = [start, end]

The start.type is only bc, c, or integer; we're just adding a v for the first two.

		passage.type = if start.type is "integer" then "cv" else start.type + "v"

Create the object in the expected format if it's not already a verse.

		passage.value[0] = {type: "c", value: [start], indices: start.indices} if start.type is "integer"
		passage.value[1] = {type: "v", value: [end], indices: end.indices} if end.type is "integer"
		@handle_obj passage, accum, passage.start_context

In cases like "John 10:22-42 vs 27", treat it as "10:22-42,47" instead of "10:22-42:27".

	range_change_cv_end: (passage, accum) ->
		[start, end] = passage.value
		passage.original_type = passage.type
		passage.original_value = [start, end]
		passage.type = "sequence"
		[new_range_end, new_sequence_end] = end.value

If a translation sequence needs to come back here and reuse it, make sure it can get the old object (end)--it only looks in accum, not in deep objects.

		new_range_end = @shallow_clone new_range_end

Was "c" but change it to "v" to serve as the end of a range.

		new_range_end.original_type = new_range_end.type
		new_range_end.type = "v"

Change it into a sequence consisting of a range and a free verse.

		passage.value = [
			[{type: "range", value: [start, new_range_end], indices: [start.indices[0], new_range_end.indices[1]]}]
			[new_sequence_end]
		]
		@sequence passage, accum, passage.start_context

	range_validate: (valid, start_obj, end_obj, passage) ->

If it's valid but the end range goes too high, snap it back to the appropriate chapter or verse.

		if valid.messages.end_chapter_not_exist

end_chapter_not_exist gives the highest chapter for the book.

			end_obj.original_c = end_obj.c
			end_obj.c = valid.messages.end_chapter_not_exist

If we've snapped it back to the last chapter and there's a verse, also snap to the end of that chapter. If we've already overshot the chapter, there's no reason to think we've gotten the verse right; Gen 50:1-51:1 = Gen 50:1-26 = Gen 50. If there's no verse, we don't need to worry about it.

			if end_obj.v?

end_verse_not_exist gives the maximum verse for the chapter.

				end_obj.v = @validate_ref(passage.start_context.translations, {b: end_obj.b, c: end_obj.c, v: 999}).messages.end_verse_not_exist

If the end verse is too high, snap back to the maximum verse.

		else if valid.messages.end_verse_not_exist
			end_obj.original_v = end_obj.v
			end_obj.v = valid.messages.end_verse_not_exist
		end_obj.v = valid.messages.end_verse_is_zero if valid.messages.end_verse_is_zero and @options.zero_verse_strategy isnt "allow"
		end_obj.c = valid.messages.end_chapter_is_zero if valid.messages.end_chapter_is_zero
		[start_obj.c, start_obj.v] = @fix_start_zeroes valid, start_obj.c, start_obj.v
		true

If the start chapter or verse is 0, convert it to a 1. valid.valid is false if the zero_*_strategy is error.

	fix_start_zeroes: (valid, c, v) ->
		if valid.valid
			c = valid.messages.start_chapter_is_zero if valid.messages.start_chapter_is_zero
			v = valid.messages.start_verse_is_zero if valid.messages.start_verse_is_zero and @options.zero_verse_strategy isnt "allow"
		[c, v]

If a new end chapter/verse in a range may be necessary, calculate it.

	range_check_new_end: (translations, start_obj, end_obj, valid) ->
		new_end = 0
		type = null

See whether a digit might be omitted (e.g., Gen 22-4 = Gen 22-24)

		if valid.messages.end_chapter_before_start then type = "c"
		else if valid.messages.end_verse_before_start then type = "v"
		new_end = @range_get_new_end_value(start_obj, end_obj, valid, type) if type?
		if new_end > 0
			obj_to_validate = b: end_obj.b, c: end_obj.c, v: end_obj.v
			obj_to_validate[type] = new_end
			new_valid = @validate_ref translations, obj_to_validate
			new_end = 0 unless new_valid.valid
		new_end

If a sequence has an end chapter/verse that's before the the start, check to see whether it can be salvaged: Gen 28-9 = Gen 28-29; Ps 101-24 = Ps 101-124. The key parameter is either c (for chapter) or v (for verse).

	range_get_new_end_value: (start_obj, end_obj, valid, key) ->

Return 0 unless it's salvageable.

		new_end = 0
		return new_end if ((key is "c" and valid.messages.end_chapter_is_zero) or (key is "v" and valid.messages.end_verse_is_zero))

54-5, not 54-43, 54-3, or 54-4.

		if start_obj[key] >= 10 and end_obj[key] < 10 and start_obj[key] - 10 * Math.floor(start_obj[key] / 10) < end_obj[key]

Add the start tens digit to the original end value: 54-5 = 54 through 50 + 5.

			new_end = end_obj[key] + 10 * Math.floor(start_obj[key] / 10)

123-40, not 123-22 or 123-23; 123-4 is taken care of in the first case.

		else if start_obj[key] >= 100 and end_obj[key] < 100 and start_obj[key] - 100 < end_obj[key]

Add 100 to the original end value: 100-12 = 100 through 100 + 12.

			new_end = end_obj[key] + 100
		new_end

Translations

Even a single translation ("NIV") appears as part of a translation sequence. Here we handle the sequence and apply the translations to any previous passages lacking an explicit translation: in "Matt 1, 5 ESV," both Matt 1 and 5 get applied, but in "Matt 1 NIV, 5 ESV," NIV only applies to Matt 1, and ESV only applies to Matt 5.

	translation_sequence: (passage, accum, context) ->
		translations = []

First get all the translations in the sequence; the first one is separate from the others (which may not exist).

		translations.push translation: @books[passage.value[0].value].parsed
		for val in passage.value[1]

val at this point is an array.

			val = @books[@pluck("translation", val).value].parsed

And now val is the literal, lower-cased match.

			translations.push translation: val if val?

We need some metadata to do this right.

		for translation in translations

Do we know anything about this translation? If so, use that. If not, use the default

			alias = if @translations.aliases[translation.translation]? then translation.translation else "default"

osis is what we'll eventually use in output.

			translation.osis = @translations.aliases[alias].osis

alias is what we use internally to get bcv data for the translation.

			translation.alias = @translations.aliases[alias].alias

Now we need to go back and find the earliest already-parsed passage without a translation. We start with 0 because the below loop will never yield a 0.

		if accum.length > 0
			use_i = 0

Start with the most recent and go backward--we don't want to overlap another translation_sequence.

			for i in [accum.length - 1 .. 0]

With a new translation comes the possibility that a previously invalid reference will become valid, so reset it to its original type. For example, a multi-book range may be correct in a different translation because the books are in a different order.

				accum[i].type = accum[i].original_type if accum[i].original_type?
				accum[i].value = accum[i].original_value if accum[i].original_value?
				continue unless accum[i].type == "translation_sequence"

If we made it here, then we hit a translation sequence, and we know that the item following it is the first one we care about.

				use_i = i + 1
				break

Include the translations in the start context.

use_i == accum.length if there are two translations sequences in a row separated by, e.g., numbers ("Matt 1 ESV 2-3 NIV").

			if use_i < accum.length
				accum[use_i].start_context.translations = translations

The objects in accum are replaced in-place, so we don't need to try to merge them back. We re-parse them because the translation may cause previously valid (or invalid) references to flip the other way--if the new translation includes (or doesn't) the Deuterocanonicals, for example. We ignore the new_accum, but we definitely care about the new context.

				[new_accum, context] = @handle_array accum.slice(use_i), [], accum[use_i].start_context

We may need these indices later, depending on how we want to output the data.

		passage.absolute_indices ?= @get_absolute_indices passage.indices

Include the translation_sequence object so that we can handle any later translation_sequence objects without overlapping this one.

		accum.push passage

Don't carry over the translations into any later references; translations only apply backwards.

		delete context.translations
		[accum, context]

Utilities

Pluck the object or value matching a type from an array.

	pluck: (type, passages) ->
		for passage in passages
			continue unless passage.type? and passage.type is type
			return @pluck("integer", passage.value) if type is "c" or type is "v"
			return passage
		null

Make a shallow clone of an object. Nested objects are referenced, not cloned.

	shallow_clone: (obj) ->
		out = {}
		for own key, val of obj
			out[key] = val
		out

Split a string on a regexp. This is purely for cross-browser (read: IE) compatibility, which

Given a string and initial index, calculate indices for parts of the string. For example, a string that starts at index 10 might have a book that pushes it to index 12 starting at its third character.

	calculate_indices: (match, adjust) ->

This gets switched out the first time in the loop; the first item is never a book even if a book is the first part of the string--there's an empty string before it.

		switch_type = "book"
		indices = []
		match_index = 0
		adjust = parseInt adjust, 10

It would be easier to do for part in match.split /[\x1e\x1f]/, but IE doesn't return empty matches when using split, throwing off the rest of the logic.

		parts = [match]
		for character in ["\x1e", "\x1f"]
			temp = []
			for part in parts
				temp = temp.concat part.split(character)
			parts = temp

		for part in parts

Start off assuming it's not a book.

			switch_type = if switch_type is "book" then "rest" else "book"

Empty strings don't move the index. This could happen with consecutive books.

			part_length = part.length
			continue if part_length == 0

If it's a book, then get the start index of the actual book, add the length of the actual string, then subtract the length of the integer id and the two surrounding characters.

			if switch_type == "book"

Remove any stray extra indicators.

				part = part.replace /\/[a-z]$/, ""

Get the length of the id + the surrounding characters. We want the end to be the position, not the length. If the part starts at position 0 and is one character (i.e., three characters total, or \x1f0\x1f), end should be 1, since it occupies positions 0, 1, and 2, and we want the last character to be part of the next index so that we keep track of the end. For example, with "Genesis" at start index 0, the index starting at position 6 ("s") should be 4. Keep the adjust as-is, but set it next.

				end_index = match_index + part_length
				if indices.length > 0 and indices[indices.length - 1].index == adjust
					indices[indices.length - 1].end = end_index
				else
					indices.push start: match_index, end: end_index, index: adjust

If the part is one character (three characters total) starting at match_index 0, we want the next match_index to be 3; it occupies positions 0, 1, and 2. Similarly, if it's two characters, it should be four characters total.

				match_index += part_length + 2

Use the known start_index from the book, subtracting the current index in the match, to get the new. So if the previous match_index == 5 and the book's id is 0, the book's start_index == 10, and the book's length == 7, we want the next adjust to be 10 + 7 - 8 = 9 (the 8 is the match_index where the new adjust starts): 4(+5) = 9, 5(+5) = 10, 6(+5) = 11, 7(+5) = 12, 8(+9) = 17.

				adjust = @books[part].start_index + @books[part].value.length - match_index
				indices.push start: end_index + 1, end: end_index + 1, index: adjust
			else

The - 1 is because we want the end to be the position of the last character. If the part starts at position 0 and is three characters long, the end should be two, since it occupies positions 0, 1, and 2.

				end_index = match_index + part_length - 1
				if indices.length > 0 and indices[indices.length - 1].index == adjust
					indices[indices.length - 1].end = end_index
				else
					indices.push start: match_index, end: end_index, index: adjust
				match_index += part_length
		indices

Find the absolute string indices of start and end points.

	get_absolute_indices: ([start, end]) ->
		start_out = null
		end_out = null

@indices contains the absolute indices for each range of indices in the string.

		for index in @indices

If we haven't found the absolute start index yet, set it.

			if start_out is null and index.start <= start <= index.end
				start_out = start + index.index

This may be in the same loop iteration as start. The + 1 matches Twitter's implementation of indices, where start is the character index and end is the character after the index. So Gen is [0, 3].

			if index.start <= end <= index.end
				end_out = end + index.index + 1
				break
		[start_out, end_out]

Validators

Given a start and optional end bcv object, validate that the verse exists and is valid. It returns an array with validity for each translations.

	validate_ref: (translations, start, end) ->

The translation key is optional; if it doesn't exist, assume the default translation.

		translations or= [{translation: "default", osis: "", alias: "default"}]
		translation = translations[0]

Only true if translations isn't the right type.

		return {valid: false, messages: {translation_invalid: true}} unless translation?
		valid = true
		messages = {}

translation is a translation object, but all we care about is the string.

		translation.alias ?= "default"

Only true if translations isn't the right type.

		return {valid: false, messages: {translation_invalid: true}} unless translation.alias?

Not a fatal error because we assume that translations match the default unless we know differently. But we still record it because we may want to know about it later.

		unless @translations[translation.alias]?
			translation.alias = "default"
			messages.translation_unknown = true
		[valid, messages] = @validate_start_ref translation.alias, start, valid, messages
		[valid, messages] = @validate_end_ref translation.alias, start, end, valid, messages if end
		valid: valid, messages: messages

Make sure that the start ref exists in the given translation.

	validate_start_ref: (translation, start, valid, messages) ->

Matt

		if @translations[translation].order[start.b]?
			start.c ?= 1
			start.c = parseInt start.c, 10

Matt five

			if isNaN start.c
				valid = false
				messages.start_chapter_not_numeric = true
				return [valid, messages]

Matt 0

			if start.c == 0
				messages.start_chapter_is_zero = 1
				if @options.zero_chapter_strategy is "error" then valid = false
				else start.c = 1

Matt 5

			if start.c > 0 and @translations[translation].chapters[start.b][start.c - 1]?

Matt 5:10

				if start.v?
					start.v = parseInt start.v, 10

Matt 5:ten

					if isNaN start.v
						valid = false
						messages.start_verse_not_numeric = true

Matt 5:0

					else if start.v == 0
						messages.start_verse_is_zero = 1
						if @options.zero_verse_strategy is "error" then valid = false
						else if @options.zero_verse_strategy is "upgrade" then start.v = 1

Matt 5:100

					else if start.v > @translations[translation].chapters[start.b][start.c - 1]
						valid = false
						messages.start_verse_not_exist = @translations[translation].chapters[start.b][start.c - 1]

Matt 50

			else
				valid = false
				if start.c != 1 and @translations[translation].chapters[start.b].length == 1
					messages.start_chapter_not_exist_in_single_chapter_book = 1
				else if start.c > 0
					messages.start_chapter_not_exist = @translations[translation].chapters[start.b].length

None 2:1

		else
			valid = false
			messages.start_book_not_exist = true
		[valid, messages]

The end ref pretty much just has to be after the start ref; beyond the book, we don't require that the chapter or verse exists. This is useful when people get end verses wrong.

	validate_end_ref: (translation, start, end, valid, messages) ->
		end.c = parseInt end.c, 10 if end.c?
		end.v = parseInt end.v, 10 if end.v?

Matt 0

		if end.c? and not(isNaN end.c) and end.c == 0
			messages.end_chapter_is_zero = 1
			if @options.zero_chapter_strategy is "error" then valid = false
			else end.c = 1

Matt-Mark

		if @translations[translation].order[end.b]?

Mark 4-Matt 5, None 4-Matt 5

			if @translations[translation].order[start.b]? and @translations[translation].order[start.b] > @translations[translation].order[end.b]
				valid = false
				messages.end_book_before_start = true

Matt 5-6

			if start.b == end.b and end.c? and not isNaN end.c

Matt-Matt 4

				start.c ?= 1

Matt 5-4

				if not isNaN(parseInt start.c, 10) and start.c > end.c
					valid = false
					messages.end_chapter_before_start = true

Matt 5:7-5:8

				else if start.c == end.c and end.v? and not isNaN end.v

Matt 5-5:8

					start.v ?= 1

Matt 5:8-7

					if not isNaN(parseInt start.v, 10) and start.v > end.v
						valid = false
						messages.end_verse_before_start = true
			if end.c? and not isNaN end.c
				if not @translations[translation].chapters[end.b][end.c - 1]?
					if @translations[translation].chapters[end.b].length is 1
						messages.end_chapter_not_exist_in_single_chapter_book = 1
					else if end.c > 0
						messages.end_chapter_not_exist = @translations[translation].chapters[end.b].length
			if end.v? and not isNaN end.v
				end.c ?= @translations[translation].chapters[end.b].length
				if end.v > @translations[translation].chapters[end.b][end.c - 1]
					messages.end_verse_not_exist = @translations[translation].chapters[end.b][end.c - 1]
				else if end.v == 0
					messages.end_verse_is_zero = 1
					if @options.zero_verse_strategy is "error" then valid = false
					else if @options.zero_verse_strategy is "upgrade" then end.v = 1

Matt 5:1-None 6

		else
			valid = false
			messages.end_book_not_exist = true

Matt 2-four

		if end.c? and isNaN end.c
			valid = false
			messages.end_chapter_not_numeric = true

Matt 5:7-eight

		if end.v? and isNaN end.v
			valid = false
			messages.end_verse_not_numeric = true
		[valid, messages]