diff --git a/libwccl/CMakeLists.txt b/libwccl/CMakeLists.txt
index 67043e6687af132c66f3f37d391db5786ce4aa93..662d94d5bdb1c125433b1b425db3824a48f24477 100644
--- a/libwccl/CMakeLists.txt
+++ b/libwccl/CMakeLists.txt
@@ -35,6 +35,7 @@ SET(libwccl_STAT_SRC
 	ops/or.cpp
 	ops/predicate.cpp
 	ops/regex.cpp
+	ops/relativeposition.cpp
 	ops/tolower.cpp
 	ops/toupper.cpp
 	parser/Parser.cpp
diff --git a/libwccl/ops/relativeposition.cpp b/libwccl/ops/relativeposition.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0a0d73d000886ea93740905d01a2f1cd7b71461f
--- /dev/null
+++ b/libwccl/ops/relativeposition.cpp
@@ -0,0 +1,43 @@
+#include <libwccl/ops/relativeposition.h>
+#include <sstream>
+#include <libwccl/ops/constant.h>
+
+namespace Wccl {
+
+RelativePosition::BaseRetValPtr RelativePosition::apply_internal(
+	const FunExecContext &context) const
+{
+	static const Constant<Position> nowhere((Position(Position::Nowhere)));
+	const RetValPtr& orig_pos = pos_expr_->apply(context);
+	if(orig_pos->get_value() == Position::Nowhere) {
+		return nowhere.apply(context);
+	}
+	const SentenceContext& sc = context.sentence_context();
+	return RetValPtr(new Position(offset_ + sc.get_rel_position(*orig_pos)));
+}
+
+std::string RelativePosition::to_string(const Corpus2::Tagset &tagset) const
+{
+	std::stringstream ss;
+	ss << pos_expr_->to_string(tagset);
+	if(offset_ >= 0) {
+		ss << " + " << offset_;
+	} else {
+		ss << " - " << -offset_;
+	}
+	return ss.str();
+}
+
+std::string RelativePosition::to_raw_string() const
+{
+	std::stringstream ss;
+	ss << pos_expr_->to_raw_string();
+	if(offset_ >= 0) {
+		ss << " + " << offset_;
+	} else {
+		ss << " - " << -offset_;
+	}
+	return ss.str();
+}
+
+} /* end ns Wccl */
diff --git a/libwccl/ops/relativeposition.h b/libwccl/ops/relativeposition.h
new file mode 100644
index 0000000000000000000000000000000000000000..2fb9961b4bf416a0784bb441a44eef0bcdf9dc11
--- /dev/null
+++ b/libwccl/ops/relativeposition.h
@@ -0,0 +1,57 @@
+#ifndef LIBWCCL_OPS_RELATIVEPOSITION_H
+#define LIBWCCL_OPS_RELATIVEPOSITION_H
+
+#include <libwccl/ops/functions.h>
+#include <libwccl/ops/formatters.h>
+#include <libwccl/values/position.h>
+
+namespace Wccl {
+
+/**
+ * Operator that takes a Position and an offset and returns relative
+ * Position, shifted by the offset from the original one.
+ */
+class RelativePosition : public Function<Position> {
+public:
+	typedef boost::shared_ptr<Function<Position> > PosFunctionPtr;
+
+	RelativePosition(const PosFunctionPtr& pos_expr, int offset)
+		: pos_expr_(pos_expr),
+		  offset_(offset)
+	{
+		BOOST_ASSERT(pos_expr_);
+	}
+
+	virtual std::string to_string(const Corpus2::Tagset& tagset) const;
+
+	virtual std::string to_raw_string() const;
+
+	virtual const std::string raw_operator_name() const {
+		return "+";
+	}
+
+protected:
+	const PosFunctionPtr pos_expr_;
+	const int offset_;
+
+	/**
+	 * Takes the value of a Position from argument expression, and returns
+	 * a Position relative to it, shifted by the offset that this
+	 * RelativePosition object is representing.
+	 * The result is not being trimmed to boundaries of the current
+	 * sentence (a Position pointing outside of a sentence is still
+	 * a valid Position).
+	 * If "nowhere" is given, "nowhere" is returned (shifted "nowhere"
+	 * still points to "nowhere").
+	 * If "begin" or "end" are given, they are first represented as a normal
+	 * Position Value (i.e. a Position relative to the current Position
+	 * in the SentenceContext acted upon) which is then shifted normally.
+	 * @returns Position that is shifted by the represented offset relative
+	 * to the Position being passed as argument to Operator.
+	 */
+	virtual BaseRetValPtr apply_internal(const FunExecContext& context) const;
+};
+
+} /* end ns Wccl */
+
+#endif // LIBWCCL_OPS_RELATIVEPOSITION_H
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 6b6035e7ce2cdef86b634fc107b50d91af4ad47f..d965a98803a0f21a4e099c2827cc7792dc86b1ff 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -13,6 +13,7 @@ add_executable(tests
 	position.cpp
 	positionpredicates.cpp
 	regex.cpp
+	relativeposition.cpp
 	strsetfunctions.cpp
 	values.cpp
 	varaccess.cpp
diff --git a/tests/relativeposition.cpp b/tests/relativeposition.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..00d70a454d0ef3abe2366774230ab4cd46147212
--- /dev/null
+++ b/tests/relativeposition.cpp
@@ -0,0 +1,164 @@
+#include <boost/test/unit_test.hpp>
+#include <boost/bind.hpp>
+#include <boost/shared_ptr.hpp>
+#include <libcorpus2/sentence.h>
+
+#include <libwccl/ops/constant.h>
+#include <libwccl/ops/relativeposition.h>
+
+using namespace Wccl;
+
+BOOST_AUTO_TEST_SUITE(relative_position)
+
+struct PosPredFix
+{
+	PosPredFix()
+		: s(boost::make_shared<Corpus2::Sentence>()),
+		  sc(s),
+		  tagset(),
+		  cx(sc, boost::make_shared<Variables>()),
+		  pos_one(1),
+		  pos_minus_one(-1),
+		  nowhere(Position::Nowhere),
+		  begin(Position::Begin),
+		  end(Position::End),
+		  pos_one_constant(new Constant<Position>(pos_one)),
+		  pos_minus_one_constant(new Constant<Position>(pos_minus_one)),
+		  nowhere_constant(new Constant<Position>(nowhere)),
+		  begin_constant(new Constant<Position>(begin)),
+		  end_constant(new Constant<Position>(end))
+	{
+		Corpus2::Token* the_token = new Corpus2::Token("ZZ", PwrNlp::Whitespace::ManySpaces);
+		Corpus2::Tag t1(Corpus2::mask_t(0));
+		Corpus2::Lexeme l1("aaa", t1);
+		Corpus2::Lexeme l2("bbb", t1);
+		the_token->add_lexeme(l1);
+		the_token->add_lexeme(l2);
+		s->append(the_token);
+		s->append(the_token->clone());
+	}
+
+	boost::shared_ptr<Corpus2::Sentence> s;
+	SentenceContext sc;
+	Corpus2::Tagset tagset;
+
+	FunExecContext cx;
+	Position pos_one;
+	Position pos_minus_one;
+	Position nowhere;
+	Position begin;
+	Position end;
+	boost::shared_ptr<Function<Position> > pos_one_constant;
+	boost::shared_ptr<Function<Position> > pos_minus_one_constant;
+	boost::shared_ptr<Function<Position> > nowhere_constant;
+	boost::shared_ptr<Function<Position> > begin_constant;
+	boost::shared_ptr<Function<Position> > end_constant;
+
+};
+
+BOOST_FIXTURE_TEST_CASE(rel_nowhere, PosPredFix)
+{
+	for(int offset = -2; offset < 3; offset++)
+	{
+		RelativePosition relpos(nowhere_constant, offset);
+		BOOST_CHECK_EQUAL(
+			Position::Nowhere,
+			relpos.apply(cx)->get_value());
+		sc.advance();
+		BOOST_CHECK_EQUAL(
+			Position::Nowhere,
+			relpos.apply(cx)->get_value());
+		sc.advance();
+		BOOST_CHECK_EQUAL(
+			Position::Nowhere,
+			relpos.apply(cx)->get_value());
+		sc.goto_start();
+	}
+}
+
+BOOST_FIXTURE_TEST_CASE(rel_begin, PosPredFix)
+{
+	for(int offset = -2; offset < 3; offset++)
+	{
+		RelativePosition relpos(begin_constant, offset);
+		BOOST_CHECK_EQUAL(
+			offset,
+			cx.sentence_context().get_abs_position(*relpos.apply(cx)));
+		sc.advance();
+		BOOST_CHECK_EQUAL(
+			offset,
+			cx.sentence_context().get_abs_position(*relpos.apply(cx)));
+		sc.advance();
+		BOOST_CHECK_EQUAL(
+			offset,
+			cx.sentence_context().get_abs_position(*relpos.apply(cx)));
+		sc.goto_start();
+	}
+}
+
+BOOST_FIXTURE_TEST_CASE(rel_end, PosPredFix)
+{
+	for(int i = -2; i < 3; i++)
+	{
+		RelativePosition relpos(end_constant, i);
+		BOOST_CHECK_EQUAL(
+			sc.size() - 1 + i,
+			cx.sentence_context().get_abs_position(*relpos.apply(cx)));
+		sc.advance();
+		BOOST_CHECK_EQUAL(
+			sc.size() - 1 + i,
+			cx.sentence_context().get_abs_position(*relpos.apply(cx)));
+		sc.advance();
+		BOOST_CHECK_EQUAL(
+			sc.size() - 1 + i,
+			cx.sentence_context().get_abs_position(*relpos.apply(cx)));
+		sc.goto_start();
+	}
+}
+
+BOOST_FIXTURE_TEST_CASE(rel_normal_pos, PosPredFix)
+{
+	for(int offset = -2; offset < 3; offset++)
+	{
+		for(int pos = -2; pos < 3; pos++)
+		{
+			boost::shared_ptr<Constant<Position> > position(new Constant<Position>(Position(pos)));
+			RelativePosition relpos(position, offset);
+			BOOST_CHECK_EQUAL(
+				offset + pos,
+				relpos.apply(cx)->get_value());
+			sc.advance();
+			BOOST_CHECK_EQUAL(
+				offset + pos,
+				relpos.apply(cx)->get_value());
+			sc.advance();
+			BOOST_CHECK_EQUAL(
+				offset + pos,
+				relpos.apply(cx)->get_value());
+			sc.goto_start();
+		}
+	}
+}
+
+//------ to_string test cases -------
+
+BOOST_FIXTURE_TEST_CASE(relpos_to_string, PosPredFix)
+{
+	RelativePosition relpos(begin_constant, 4);
+	BOOST_CHECK_EQUAL("begin + 4", relpos.to_string(tagset));
+	RelativePosition relpos_min1(pos_minus_one_constant, -1);
+	BOOST_CHECK_EQUAL("-1 - 1", relpos_min1.to_string(tagset));
+	RelativePosition relpos_zero(end_constant, 0);
+	BOOST_CHECK_EQUAL("end + 0", relpos_zero.to_string(tagset));
+}
+
+BOOST_FIXTURE_TEST_CASE(relpos_to_raw_string, PosPredFix)
+{
+	RelativePosition relpos(nowhere_constant, 42);
+	BOOST_CHECK_EQUAL("nowhere + 42", relpos.to_raw_string());
+	RelativePosition relpos_min1(pos_one_constant, -1);
+	BOOST_CHECK_EQUAL("1 - 1", relpos_min1.to_raw_string());
+	RelativePosition relpos_zero(end_constant, 0);
+	BOOST_CHECK_EQUAL("end + 0", relpos_zero.to_raw_string());
+}
+BOOST_AUTO_TEST_SUITE_END()